@fsegurai/ngx-sidenav
Version:
Angular component for creating a resizeable sidenav with drag and drop functionality.
824 lines • 83.6 kB
JavaScript
import * as i0 from '@angular/core';
import { input, ViewEncapsulation, Component, inject, ViewContainerRef, Directive, signal, computed, Injectable, output, viewChild, HostListener, HostBinding } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import * as i1 from '@angular/material/icon';
import { MatIconModule } from '@angular/material/icon';
import * as i2 from '@angular/material/tooltip';
import { MatTooltipModule } from '@angular/material/tooltip';
import { NgOptimizedImage } from '@angular/common';
class LinkItem {
constructor() {
// ? === Public inputs signals ===
this.routerLink = input(null, ...(ngDevMode ? [{ debugName: "routerLink" }] : []));
this.routerLinkActiveOptions = input({ exact: true }, ...(ngDevMode ? [{ debugName: "routerLinkActiveOptions" }] : []));
this.isTitle = input(false, ...(ngDevMode ? [{ debugName: "isTitle" }] : []));
this.isParent = input(false, ...(ngDevMode ? [{ debugName: "isParent" }] : []));
this.isDisabled = input(false, ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: LinkItem, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.1.3", type: LinkItem, isStandalone: true, selector: "ngx-sidenav-link-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, routerLinkActiveOptions: { classPropertyName: "routerLinkActiveOptions", publicName: "routerLinkActiveOptions", isSignal: true, isRequired: false, transformFunction: null }, isTitle: { classPropertyName: "isTitle", publicName: "isTitle", isSignal: true, isRequired: false, transformFunction: null }, isParent: { classPropertyName: "isParent", publicName: "isParent", isSignal: true, isRequired: false, transformFunction: null }, isDisabled: { classPropertyName: "isDisabled", publicName: "isDisabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<a class=\"link-item\"\n [class.is-parent]=\"isParent()\"\n [class.is-title]=\"isTitle()\"\n [routerLink]=\"routerLink()\"\n routerLinkActive=\"active\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\">\n\n <span class=\"link-content-container\">\n <ng-content></ng-content>\n </span>\n</a>", styles: [".link-item{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px;box-sizing:border-box;padding:var(--sidenav-link-padding, 8px 12px);margin-left:var(--sidenav-link-margin-left, 12px);min-width:calc(var(--min-expanded-sidenav-width, 180px) - 32px);text-decoration:none;color:var(--sidenav-link-text, #222);background:var(--sidenav-link-bg, transparent);transition:background .18s cubic-bezier(.4,0,.2,1),color .18s cubic-bezier(.4,0,.2,1),border-color .18s cubic-bezier(.4,0,.2,1);cursor:pointer;outline:none;border-left:2px solid var(--sidenav-link-border, #bdbdbd)}.link-item.is-title,.link-item.is-parent{border-left:none;font-weight:700;color:var(--sidenav-link-title-text, inherit);background:var(--sidenav-link-title-bg, transparent);margin:.5rem 0;text-decoration:none!important;cursor:default;pointer-events:none}.link-item.is-parent:not(.is-title){color:var(--sidenav-link-parent-text, inherit);background:var(--sidenav-link-parent-bg, inherit);cursor:pointer;pointer-events:auto}.link-item:not(.is-title):not(.is-parent){cursor:pointer;pointer-events:auto}.link-item:not(.is-title):not(.is-parent):hover,.link-item:not(.is-title):not(.is-parent):focus-visible{background:var(--sidenav-link-hover-bg);border-left-color:var(--sidenav-link-hover-border);color:var(--sidenav-link-hover-text);text-decoration:none;outline:2px solid var(--sidenav-link-focus);outline-offset:2px}.link-item .link-content-container{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.link-item.active{font-weight:600;background:var(--sidenav-link-active-bg);color:var(--sidenav-link-active-text);border-left-color:var(--sidenav-link-active-border);border-left-width:3px;outline:2px solid var(--sidenav-link-active-outline);outline-offset:1px}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: RouterLinkActive, selector: "[routerLinkActive]", inputs: ["routerLinkActiveOptions", "ariaCurrentWhenActive", "routerLinkActive"], outputs: ["isActiveChange"], exportAs: ["routerLinkActive"] }], encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: LinkItem, decorators: [{
type: Component,
args: [{ selector: 'ngx-sidenav-link-item', imports: [
RouterLink,
RouterLinkActive,
], encapsulation: ViewEncapsulation.None, template: "<a class=\"link-item\"\n [class.is-parent]=\"isParent()\"\n [class.is-title]=\"isTitle()\"\n [routerLink]=\"routerLink()\"\n routerLinkActive=\"active\"\n [routerLinkActiveOptions]=\"routerLinkActiveOptions()\">\n\n <span class=\"link-content-container\">\n <ng-content></ng-content>\n </span>\n</a>", styles: [".link-item{display:flex;flex-direction:row;align-items:center;justify-content:flex-start;gap:8px;box-sizing:border-box;padding:var(--sidenav-link-padding, 8px 12px);margin-left:var(--sidenav-link-margin-left, 12px);min-width:calc(var(--min-expanded-sidenav-width, 180px) - 32px);text-decoration:none;color:var(--sidenav-link-text, #222);background:var(--sidenav-link-bg, transparent);transition:background .18s cubic-bezier(.4,0,.2,1),color .18s cubic-bezier(.4,0,.2,1),border-color .18s cubic-bezier(.4,0,.2,1);cursor:pointer;outline:none;border-left:2px solid var(--sidenav-link-border, #bdbdbd)}.link-item.is-title,.link-item.is-parent{border-left:none;font-weight:700;color:var(--sidenav-link-title-text, inherit);background:var(--sidenav-link-title-bg, transparent);margin:.5rem 0;text-decoration:none!important;cursor:default;pointer-events:none}.link-item.is-parent:not(.is-title){color:var(--sidenav-link-parent-text, inherit);background:var(--sidenav-link-parent-bg, inherit);cursor:pointer;pointer-events:auto}.link-item:not(.is-title):not(.is-parent){cursor:pointer;pointer-events:auto}.link-item:not(.is-title):not(.is-parent):hover,.link-item:not(.is-title):not(.is-parent):focus-visible{background:var(--sidenav-link-hover-bg);border-left-color:var(--sidenav-link-hover-border);color:var(--sidenav-link-hover-text);text-decoration:none;outline:2px solid var(--sidenav-link-focus);outline-offset:2px}.link-item .link-content-container{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.link-item.active{font-weight:600;background:var(--sidenav-link-active-bg);color:var(--sidenav-link-active-text);border-left-color:var(--sidenav-link-active-border);border-left-width:3px;outline:2px solid var(--sidenav-link-active-outline);outline-offset:1px}\n"] }]
}] });
class ContentAreaDirective {
constructor() {
this._viewContainerRef = inject(ViewContainerRef);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ContentAreaDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.1.3", type: ContentAreaDirective, isStandalone: true, selector: "[ngxSidenavContentArea]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: ContentAreaDirective, decorators: [{
type: Directive,
args: [{
selector: '[ngxSidenavContentArea]',
}]
}] });
/**
* Determines if the given item is a MenuStackItem.
*
* @param {SidenavStackItem} item - The item to check.
* @return {boolean} True if the item is a MenuStackItem, otherwise false.
*/
function isMenuStackItem(item) {
return item.type === 'menu';
}
/**
* Determines if the given stack item is a ComponentStackItem.
*
* @param {SidenavStackItem} item - The stack item to evaluate.
* @return {boolean} - Returns true if the item is of type ComponentStackItem, otherwise false.
*/
function isComponentStackItem(item) {
return item.type === 'component';
}
// LocalStorage keys
const STORAGE_KEY_WIDTH = 'ngx-sidenav-width';
const STORAGE_KEY_EXPANDED = 'ngx-sidenav-expanded';
const STORAGE_KEY_STACK = 'ngx-sidenav-stack';
const STORAGE_KEY_CLEAR_ON_RELOAD = 'ngx-sidenav-clear-on-reload';
class SidenavService {
// ! === Private constants ===
#DEBOUNCE_DELAY; // Debounce delay for localStorage writes
#TRANSITION_DURATION; // Default transition duration in milliseconds
// ! === Private reactive signals ===
#minWidth; // Minimum width of the sidenav
#maxWidth; // Maximum width of the sidenav
#isExpanded;
#sidenavWidth;
#stack;
// ! === Private internals ===
#shouldPersistStack;
#stackItemCounter;
#contentArea;
constructor() {
// ? Public constants
this.COLLAPSE_WIDTH = 30; // Minimum width for collapse
this.INITIAL_WIDTH = 300; // Default initial width of the sidenav
// ! === Private constants ===
this.#DEBOUNCE_DELAY = 300; // Debounce delay for localStorage writes
this.#TRANSITION_DURATION = 400; // Default transition duration in milliseconds
// ! === Private reactive signals ===
this.#minWidth = signal(250, ...(ngDevMode ? [{ debugName: "#minWidth" }] : [])); // Minimum width of the sidenav
this.#maxWidth = signal(600, ...(ngDevMode ? [{ debugName: "#maxWidth" }] : [])); // Maximum width of the sidenav
this.#isExpanded = signal(true, ...(ngDevMode ? [{ debugName: "#isExpanded" }] : []));
this.#sidenavWidth = signal(this.INITIAL_WIDTH, ...(ngDevMode ? [{ debugName: "#sidenavWidth" }] : []));
this.#stack = signal([], ...(ngDevMode ? [{ debugName: "#stack" }] : []));
// ? === Public readonly signals ===
this.minWidth = this.#minWidth.asReadonly();
this.maxWidth = this.#maxWidth.asReadonly();
this.isExpanded = this.#isExpanded.asReadonly();
this.sidenavWidth = this.#sidenavWidth.asReadonly();
this.currentMenu = computed(() => {
const current = this.#stack().at(-1); // Get the last item in the stack
// Return early if there's no item or if it's not a valid MenuStackItem
return current && isMenuStackItem(current) ? current : undefined;
}, ...(ngDevMode ? [{ debugName: "currentMenu" }] : []));
this.canGoBack = computed(() => this.#stack().length > 1, ...(ngDevMode ? [{ debugName: "canGoBack" }] : []));
// ? === Public non-reactive state flags ===
this.isSlidingInFromRight = false;
this.isSlidingInFromLeft = false;
// ! === Private internals ===
this.#shouldPersistStack = true;
this.#stackItemCounter = 0;
/**
* A Map object that stores timeout identifiers returned by the `setTimeout` function.
* The keys in the Map are string identifiers, representing unique names or categories for each timeout.
* The values are the actual timeout identifiers, as returned by the `setTimeout` function.
* This allows for efficient tracking and management of multiple timeouts,
* such as clearing or updating them when necessary.
*/
this.#timeouts = new Map();
// Clear-once flag logic
const shouldClear = localStorage.getItem(STORAGE_KEY_CLEAR_ON_RELOAD) === 'true';
if (shouldClear) {
localStorage.removeItem(STORAGE_KEY_STACK);
localStorage.removeItem(STORAGE_KEY_CLEAR_ON_RELOAD); // Reset flag
this.#shouldPersistStack = false; // 👈 prevent saving again
}
// Restore basic config
const storedWidth = Number(localStorage.getItem(STORAGE_KEY_WIDTH));
if (!isNaN(storedWidth))
this.#sidenavWidth.set(storedWidth);
this.#isExpanded.set(localStorage.getItem(STORAGE_KEY_EXPANDED) === 'true');
// Save stack when the user exits
window.addEventListener('beforeunload', () => {
if (this.#shouldPersistStack)
this.#saveStack();
});
}
/**
* Marks the application or specific data to be cleared on the next reload
* by setting a flag in the local storage.
*
* @return {void} This method does not return a value.
*/
clearOnNextReload() {
localStorage.setItem(STORAGE_KEY_CLEAR_ON_RELOAD, 'true');
}
/**
* Configures the parameters for the sidenav, including minimum width, maximum width,
* and initial width. Updates the sidenav properties and optionally stores the
* initial width in local storage.
*
* @param {Object} config - Configuration object for the sidenav.
* @param {number} [config.minWidth] - Specifies the minimum width of the sidenav.
* @param {number} [config.maxWidth] - Specifies the maximum width of the sidenav.
* @param {number} [config.initialWidth] - Specifies the initial width of the sidenav.
* @return {void}
*/
setConfig({ minWidth, maxWidth, initialWidth }) {
if (minWidth)
this.#minWidth.set(minWidth);
if (maxWidth)
this.#maxWidth.set(maxWidth);
if (initialWidth) {
this.#sidenavWidth.set(initialWidth);
localStorage.setItem(STORAGE_KEY_WIDTH, String(initialWidth));
}
else if (!this.#sidenavWidth()) {
this.#sidenavWidth.set(this.INITIAL_WIDTH);
localStorage.setItem(STORAGE_KEY_WIDTH, String(this.#sidenavWidth()));
}
}
/**
* Adjusts the width of the sidenav, constraining it within the defined minimum and maximum bounds.
* The adjusted width is stored both in a state variable and in local storage.
*
* @param {number} width - The desired width of the sidenav to be set.
* @return {void} This method does not return a value.
*/
setSidenavWidth(width) {
const clampedWidth = Math.min(Math.max(width, this.#minWidth()), this.#maxWidth());
this.#sidenavWidth.set(clampedWidth);
this.#debouncedSave(STORAGE_KEY_WIDTH, clampedWidth);
}
/**
* Toggles the state of the sidenav between expanded and collapsed.
* Updates the local storage to reflect the new state.
*
* @return {void} Does not return a value.
*/
toggleSidenav() {
this.#isExpanded.update(v => {
this.#debouncedSave(STORAGE_KEY_EXPANDED, !v);
return !v;
});
}
/**
* Sets the dynamic content area by assigning the provided host to an internal property.
*
* @param {ContentAreaDirective} host The ContentAreaDirective instance to be set as the dynamic content area.
* @return {void} No return value.
*/
setDynamicContentArea(host) {
this.#contentArea = host;
}
/**
* Pushes a new stack item onto the sidenav stack. The item can be either a menu stack item
* or a component stack item. Based on the type of the item, the method processes and adds it
* to the stack accordingly. Additionally, certain UI elements and animations are updated or triggered.
*
* @param {SidenavStackItem} item - The stack item to be added. This can either represent a menu
* (with title, badge, items) or a component (with data and behavior).
* @return {Promise<void>} - A promise that resolves once the push operation and associated
* animations have been completed.
*/
push(item) {
const stackItem = item.type === 'menu'
? this.#createMenuStackItem(item.items, item.title, item.badge, item.badgeColor)
: this.#createComponentStackItem(item.component, item.componentData);
this.#stack.update(stack => [...stack, stackItem]);
if (isMenuStackItem(stackItem)) {
this.#clearContent();
}
else {
this.#setContent(stackItem.component);
}
if (this.#shouldPersistStack)
this.#saveStack();
return this.#animateInFromTheRight();
}
/**
* Removes the top item from the navigation stack and updates the display.
* If the stack contains only one or no items, it resolves immediately without making changes.
* Adjusts the content displayed based on the type of the new top item in the stack.
* Persists the stack state if configured to do so.
* Executes an animation to visually represent the stack update.
*
* @return {Promise<void>} A promise that resolves when the operation is complete.
*/
pop() {
if (this.#stack().length <= 1)
return Promise.resolve();
this.#stack.update(stack => stack.slice(0, -1));
const previous = this.#stack().at(-1);
if (!previous)
return Promise.resolve();
if (isMenuStackItem(previous)) {
this.#clearContent();
}
else if (isComponentStackItem(previous)) {
this.#setContent(previous.component);
}
if (this.#shouldPersistStack)
this.#saveStack();
return this.#animateInFromTheLeft();
}
/**
* Restores the stack from local storage if available; otherwise, initializes the stack from the provided root items.
*
* @param {StackMenuItem[]} rootItems - An array of StackMenuItem objects representing the root stack menu items.
* @return {void} Does not return any value.
*/
restoreStackHybrid(rootItems) {
const raw = localStorage.getItem(STORAGE_KEY_STACK);
if (raw)
this.restoreStackFromStorage();
if (!this.#stack().length)
this.restoreStackFromMenuRoot(rootItems);
}
/**
* Restores the stack state from the browser's localStorage. The method retrieves a serialized stack,
* validates its type, and deserializes appropriate items, reconstructing the stack content.
* Invalid or unparsable data will result in an empty stack being restored.
*
* @return {void} Does not return any value.
*/
restoreStackFromStorage() {
const raw = localStorage.getItem(STORAGE_KEY_STACK);
if (!raw)
return;
try {
const parsed = JSON.parse(raw);
// Validate type safety
if (!Array.isArray(parsed)) {
console.error('Invalid stack format in localStorage:', parsed);
return;
}
this.#clearContent();
this.#stack.set(parsed
.filter(item => item.type === 'menu')
.map(item => this.#deserializeMenuItem(item)));
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
console.warn('Failed to restore stack from localStorage:', errorMessage);
this.#stack.set([]);
}
}
/**
* Restores the menu stack to reflect the current application route based on the given menu root items.
* It clears the existing menu stack, rebuilds it starting from the root items, and persists the updated stack.
*
* @param {StackMenuItem[]} rootItems - An array of StackMenuItem objects representing the root menu structure.
* @return {void} This method does not return a value.
*/
restoreStackFromMenuRoot(rootItems) {
const currentRoute = location.pathname;
// Always reset the stack
this.#clearContent();
this.#stack.set([]);
// Always start with the root
this.#stack.update(stack => [...stack, this.#createMenuStackItem(rootItems)]);
const path = this.#findPathToRoute(rootItems, currentRoute);
if (!path.length) {
this.#saveStack();
return;
}
let currentChildren = rootItems;
for (const item of path) {
const match = currentChildren.find(i => i.route === item.route || i.label === item.label);
if (!match)
continue;
if (match.children?.length) {
this.#stack.update(stack => [
...stack,
this.#createMenuStackItem(match.children, match.label, match.badge, match.badgeColor),
]);
currentChildren = match.children;
}
}
this.#saveStack(); // Persist
}
/**
* Sets the content of the content area by creating and attaching the provided component.
* It ensures the content area is not null and the component is a valid constructor function
* before attempting to create it.
*
* @param {Component<unknown>} component - The component to be created and attached to the content area.
* @return {void} Does not return a value.
*/
#setContent(component) {
if (!this.#contentArea)
return;
this.#contentArea._viewContainerRef.clear();
// Type guard to ensure a component is a valid constructor function
if (!component || typeof component !== 'function') {
console.warn('Attempted to create a component with an invalid type:', component);
return;
}
this.#contentArea._viewContainerRef.createComponent(component);
}
/**
* Clears the content of the private content area by removing all child views from the view container reference.
*
* @return {void} This method does not return any value.
*/
#clearContent() {
this.#contentArea?._viewContainerRef.clear();
}
/**
* Animates the element by sliding it in from the left.
* Sets the `isSlidingInFromLeft` flag to true during the animation.
* Waits for the duration specified by `sidenavTransitionDuration` before resetting the flag.
*
* @return {Promise<void>} A promise that resolves after the animation is complete.
*/
async #animateInFromTheLeft() {
this.isSlidingInFromLeft = true;
await this.#sleep(this.#TRANSITION_DURATION);
this.isSlidingInFromLeft = false;
}
/**
* Handles the animation by sliding in an element from the right side.
* Temporarily sets the sliding state to true, waits for the defined transition duration,
* and then resets the sliding state to false.
*
* @return {Promise<void>} A promise that resolves when the animation is complete.
*/
async #animateInFromTheRight() {
this.isSlidingInFromRight = true;
await this.#sleep(this.#TRANSITION_DURATION);
this.isSlidingInFromRight = false;
}
/**
* Pauses the execution for the specified duration.
*
* @param {number} ms - The duration to sleep in milliseconds.
* @return {Promise<void>} A Promise that resolves after the specified duration.
*/
#sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Finds the path to a specific route within a nested structure of menu items.
*
* @param {StackMenuItem[]} items - The array of menu items to search through.
* @param {string} route - The route to find within the menu items.
* @param {StackMenuItem[]} path - The current path being constructed (default is an empty array).
* @return {StackMenuItem[]} - An array representing the path to the specified route. If the route is not found, it returns an empty array.
*/
#findPathToRoute(items, route, path = []) {
for (const item of items) {
const newPath = [...path, item];
if (item.route === route) {
return newPath;
}
if (item.children?.length) {
const found = this.#findPathToRoute(item.children, route, newPath);
if (found.length)
return found;
}
}
return [];
}
/**
* Creates and returns a menu stack item object with specified properties.
* Performance optimized with counter-based IDs instead of UUIDs.
*/
#createMenuStackItem(items, title, badge, badgeColor) {
return {
type: 'menu',
items,
title,
badge,
badgeColor,
id: `menu-${++this.#stackItemCounter}`, // Much faster than crypto.randomUUID()
timestamp: Date.now(),
};
}
/**
* Creates a new component stack item with the specified component and optional data.
* Performance optimized with counter-based IDs.
*/
#createComponentStackItem(component, data) {
return {
type: 'component',
component,
componentData: data,
id: `component-${++this.#stackItemCounter}`, // Much faster than crypto.randomUUID()
timestamp: Date.now(),
};
}
/**
* Deserializes a SerializableStackItem object and converts it into a MenuStackItem.
* Uses fallback ID generation for better performance.
*/
#deserializeMenuItem(item) {
return {
type: 'menu',
items: item.items,
title: item.title,
badge: item.badge,
badgeColor: item.badgeColor,
id: item.id || `menu-${++this.#stackItemCounter}`, // Use counter instead of crypto.randomUUID()
timestamp: item.timestamp || Date.now(),
};
}
/**
* Persists the current stack of menu items into localStorage if the stack persistence is enabled.
* Filters the stack to include only menu stack items, serializes the necessary properties, and stores the result.
*
* @return {void} This method does not return any value.
*/
#saveStack() {
if (!this.#shouldPersistStack)
return;
const serialized = this.#stack()
.filter(isMenuStackItem)
.map(item => ({
type: 'menu',
title: item.title,
badge: item.badge,
badgeColor: item.badgeColor,
items: item.items,
id: item.id,
timestamp: item.timestamp,
}));
localStorage.setItem(STORAGE_KEY_STACK, JSON.stringify(serialized));
}
/**
* A Map object that stores timeout identifiers returned by the `setTimeout` function.
* The keys in the Map are string identifiers, representing unique names or categories for each timeout.
* The values are the actual timeout identifiers, as returned by the `setTimeout` function.
* This allows for efficient tracking and management of multiple timeouts,
* such as clearing or updating them when necessary.
*/
#timeouts;
/**
* Saves a key-value pair to localStorage with a debounced mechanism to prevent rapid consecutive writes.
*
* @param {string} key - The key under which the value will be stored in localStorage.
* @param {unknown} value - The value to be stored, which will be converted to a string.
* @return {void} This method does not return a value.
*/
#debouncedSave(key, value) {
const existingTimeout = this.#timeouts.get(key);
if (existingTimeout !== undefined) {
clearTimeout(existingTimeout);
}
this.#timeouts.set(key, setTimeout(() => {
const stringValue = typeof value === 'string' ? value : String(value);
localStorage.setItem(key, stringValue);
this.#timeouts.delete(key);
}, this.#DEBOUNCE_DELAY));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SidenavService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SidenavService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SidenavService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [] });
class SidenavMenu {
constructor() {
// ! === Private Services ===
this._sidenav = inject(SidenavService);
// ? === Public inputs signals ===
this.items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
this.parentLabel = input(undefined, ...(ngDevMode ? [{ debugName: "parentLabel" }] : []));
this.parentBadge = input(undefined, ...(ngDevMode ? [{ debugName: "parentBadge" }] : []));
this.parentBadgeColor = input(undefined, ...(ngDevMode ? [{ debugName: "parentBadgeColor" }] : []));
// * === Protected computed signals ===
this.canGoBack = this._sidenav.canGoBack;
this.currentMenu = this._sidenav.currentMenu;
}
onBack() {
this._sidenav.pop();
}
onMenuItemClick(item) {
if (item.children?.length) {
const stackItem = {
type: 'menu',
items: item.children,
title: item.label,
badge: item.badge,
badgeColor: item.badgeColor,
};
// this._sidenav.push(item.children, item.label, item.badge, item.badgeColor);
this._sidenav.push(stackItem);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SidenavMenu, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: SidenavMenu, isStandalone: true, selector: "ngx-sidenav-menu", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, parentLabel: { classPropertyName: "parentLabel", publicName: "parentLabel", isSignal: true, isRequired: false, transformFunction: null }, parentBadge: { classPropertyName: "parentBadge", publicName: "parentBadge", isSignal: true, isRequired: false, transformFunction: null }, parentBadgeColor: { classPropertyName: "parentBadgeColor", publicName: "parentBadgeColor", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@let goBack = canGoBack();\n@let menuItems = items().length ? items() : (currentMenu()?.items || []);\n\n<ul class=\"sidenav-menu\">\n @if (goBack) {\n <li>\n <ngx-sidenav-link-item (click)=\"onBack()\" [isParent]=\"true\">\n <div class=\"sidenav-menu-back-btn\">\n <mat-icon role=\"img\" class=\"material-symbols-outlined\">\n arrow_back\n </mat-icon>\n <span class=\"sidenav-menu-label\">{{ parentLabel() }}</span>\n @if (parentBadge() && parentBadgeColor()) {\n <span [class]=\"`sidenav-menu-badge ${parentBadgeColor()}`\">{{ parentBadge() }}</span>\n }\n </div>\n </ngx-sidenav-link-item>\n </li>\n }\n\n @for (item of menuItems; track item) {\n <li>\n @if (item.route && !(item.children?.length) && (!item.isParent && !item.isTitle)) {\n <ngx-sidenav-link-item\n [routerLink]=\"item.route\"\n [routerLinkActiveOptions]=\"{ exact: true }\">\n <div class=\"sidenav-menu-label-container\">\n @if (item.icon) {\n @switch (item.iconType) {\n @case ('svg') {\n <img\n class=\"sidenav-menu-icon\"\n [ngSrc]=\"item.icon\"\n alt=\"{{ item.label }} Path\"\n width=\"24\"\n height=\"24\"\n priority>\n }\n @case ('material') {\n <mat-icon role=\"img\" class=\"material-symbols-outlined\">\n {{ item.icon }}\n </mat-icon>\n }\n }\n }\n <span class=\"sidenav-menu-label\">{{ item.label }}</span>\n @if (item.badge) {\n <span [class]=\"`sidenav-menu-badge ${item.badgeColor}`\">{{ item.badge }}</span>\n }\n </div>\n </ngx-sidenav-link-item>\n } @else if (item.children?.length) {\n <ngx-sidenav-link-item (click)=\"onMenuItemClick(item)\">\n <div class=\"sidenav-menu-label-container\">\n @if (item.icon) {\n @switch (item.iconType) {\n @case ('svg') {\n <img\n class=\"sidenav-menu-icon\"\n [ngSrc]=\"item.icon\"\n alt=\"{{ item.label }} Path\"\n width=\"24\"\n height=\"24\"\n priority>\n }\n @case ('material') {\n <mat-icon role=\"img\" class=\"material-symbols-outlined\">\n {{ item.icon }}\n </mat-icon>\n }\n }\n }\n <span class=\"sidenav-menu-label\">{{ item.label }}</span>\n <mat-icon role=\"img\" class=\"material-symbols-outlined\">\n chevron_right\n </mat-icon>\n @if (item.badge) {\n <span [class]=\"`sidenav-menu-badge ${item.badgeColor}`\">{{ item.badge }}</span>\n }\n </div>\n </ngx-sidenav-link-item>\n } @else {\n <ngx-sidenav-link-item (click)=\"onBack()\" [isTitle]=\"true\">\n <div class=\"sidenav-menu-title-container\">\n @if (item.icon) {\n @switch (item.iconType) {\n @case ('svg') {\n <img\n class=\"sidenav-menu-icon\"\n [ngSrc]=\"item.icon\"\n alt=\"{{ item.label }} Path\"\n width=\"24\"\n height=\"24\"\n priority>\n }\n @case ('material') {\n <mat-icon role=\"img\" class=\"material-symbols-outlined\">\n {{ item.icon }}\n </mat-icon>\n }\n }\n }\n <span class=\"sidenav-menu-title\">{{ item.label }}</span>\n </div>\n </ngx-sidenav-link-item>\n }\n </li>\n }\n</ul>\n", styles: [".sidenav-menu{list-style:none;margin:0;padding:0}.sidenav-menu li{margin:0;padding:0}.sidenav-menu li>ngx-sidenav-link-item{width:100%;border-radius:4px;margin:2px 0;box-shadow:none;transition:box-shadow .18s cubic-bezier(.4,0,.2,1),background .18s cubic-bezier(.4,0,.2,1)}.sidenav-menu li>ngx-sidenav-link-item:not(.is-title):not(.is-parent){cursor:pointer;pointer-events:auto}.sidenav-menu li>ngx-sidenav-link-item:not(.is-title):not(.is-parent):hover,.sidenav-menu li>ngx-sidenav-link-item:not(.is-title):not(.is-parent):focus-visible{background:var(--sidenav-link-hover-bg);box-shadow:0 1px 4px 0 var(--sidenav-link-hover-shadow, rgba(0, 0, 0, .04));outline:2px solid var(--sidenav-link-focus);outline-offset:2px}.sidenav-menu li>ngx-sidenav-link-item.active{background:var(--sidenav-link-active-bg, #e3f2fd);box-shadow:0 2px 8px 0 var(--sidenav-link-active-shadow, rgba(0, 0, 0, .08));font-weight:600;outline:2px solid var(--sidenav-link-active-outline, #1976d2);outline-offset:1px}.sidenav-menu li>ngx-sidenav-link-item.is-title,.sidenav-menu li>ngx-sidenav-link-item.is-parent{background:var(--sidenav-link-title-bg, transparent);color:var(--sidenav-link-title-text, #444);font-weight:700;cursor:default;pointer-events:none;box-shadow:none;outline:none}.sidenav-menu li>ngx-sidenav-link-item.is-parent:not(.is-title){background:var(--sidenav-link-parent-bg, #f5f5f5);color:var(--sidenav-link-parent-text, #333);cursor:pointer;pointer-events:auto}.sidenav-menu li>ngx-sidenav-link-item .sidenav-menu-label-container{font-weight:500;letter-spacing:.01em;font-size:15px;color:var(--sidenav-link-text, #222);transition:color .18s cubic-bezier(.4,0,.2,1)}.sidenav-menu li>ngx-sidenav-link-item.active .sidenav-menu-label-container{color:var(--sidenav-link-active-text, #1976d2)}.sidenav-menu li>ngx-sidenav-link-item:hover .sidenav-menu-label-container,.sidenav-menu li>ngx-sidenav-link-item:focus-visible .sidenav-menu-label-container{color:var(--sidenav-link-hover-text, #1976d2)}.sidenav-menu li>ngx-sidenav-menu{margin-left:16px;border-left:2px solid var(--sidenav-link-border, #bdbdbd);padding-left:8px}.sidenav-menu .sidenav-menu-back-btn,.sidenav-menu .sidenav-menu-title-container,.sidenav-menu .sidenav-menu-label-container{display:flex;flex-direction:row;align-items:center;gap:8px;text-decoration:none}.sidenav-menu .sidenav-menu-icon{font-size:20px;display:inline-flex;align-items:center}.sidenav-menu .sidenav-menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidenav-menu .sidenav-menu-badge{display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;padding:2px 6px;border-radius:4px;text-transform:var(--badge-text-transform, capitalize);white-space:nowrap;cursor:default;color:color-mix(in srgb,var(--badge-theme, #000) 100%,transparent);background-color:color-mix(in srgb,var(--badge-theme, #000) 20%,transparent);border:1px solid color-mix(in srgb,var(--badge-theme, #000) 60%,rgba(0,0,0,.062745098));backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);box-shadow:inset 0 0 0 1px #0000000a;transition:all .2s ease}.sidenav-menu .sidenav-menu-badge.class,.sidenav-menu .sidenav-menu-badge.undecorated_class{--badge-theme: var(--badge-class)}.sidenav-menu .sidenav-menu-badge.constant,.sidenav-menu .sidenav-menu-badge.const{--badge-theme: var(--badge-constant)}.sidenav-menu .sidenav-menu-badge.decorator{--badge-theme: var(--badge-decorator)}.sidenav-menu .sidenav-menu-badge.directive{--badge-theme: var(--badge-directive)}.sidenav-menu .sidenav-menu-badge.element{--badge-theme: var(--badge-element)}.sidenav-menu .sidenav-menu-badge.enum{--badge-theme: var(--badge-enum)}.sidenav-menu .sidenav-menu-badge.function{--badge-theme: var(--badge-function)}.sidenav-menu .sidenav-menu-badge.interface{--badge-theme: var(--badge-interface)}.sidenav-menu .sidenav-menu-badge.pipe{--badge-theme: var(--badge-pipe)}.sidenav-menu .sidenav-menu-badge.impure-pipe{--badge-theme: var(--badge-impure-pipe)}.sidenav-menu .sidenav-menu-badge.module,.sidenav-menu .sidenav-menu-badge.ng_module{--badge-theme: var(--badge-module)}.sidenav-menu .sidenav-menu-badge.alias,.sidenav-menu .sidenav-menu-badge.type_alias{--badge-theme: var(--badge-alias)}.sidenav-menu .sidenav-menu-badge.block{--badge-theme: var(--badge-block)}.sidenav-menu .sidenav-menu-badge.developer_preview,.sidenav-menu .sidenav-menu-badge.deprecated{--badge-theme: var(--badge-deprecated)}.sidenav-menu .sidenav-menu-title{white-space:normal;overflow-wrap:break-word;word-wrap:break-word;max-width:100%}\n"], dependencies: [{ kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: LinkItem, selector: "ngx-sidenav-link-item", inputs: ["routerLink", "routerLinkActiveOptions", "isTitle", "isParent", "isDisabled"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: NgOptimizedImage, selector: "img[ngSrc]", inputs: ["ngSrc", "ngSrcset", "sizes", "width", "height", "decoding", "loading", "priority", "loaderParams", "disableOptimizedSrcset", "fill", "placeholder", "placeholderConfig", "src", "srcset"] }], encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: SidenavMenu, decorators: [{
type: Component,
args: [{ selector: 'ngx-sidenav-menu', imports: [RouterLink, LinkItem, MatIconModule, NgOptimizedImage], encapsulation: ViewEncapsulation.None, template: "@let goBack = canGoBack();\n@let menuItems = items().length ? items() : (currentMenu()?.items || []);\n\n<ul class=\"sidenav-menu\">\n @if (goBack) {\n <li>\n <ngx-sidenav-link-item (click)=\"onBack()\" [isParent]=\"true\">\n <div class=\"sidenav-menu-back-btn\">\n <mat-icon role=\"img\" class=\"material-symbols-outlined\">\n arrow_back\n </mat-icon>\n <span class=\"sidenav-menu-label\">{{ parentLabel() }}</span>\n @if (parentBadge() && parentBadgeColor()) {\n <span [class]=\"`sidenav-menu-badge ${parentBadgeColor()}`\">{{ parentBadge() }}</span>\n }\n </div>\n </ngx-sidenav-link-item>\n </li>\n }\n\n @for (item of menuItems; track item) {\n <li>\n @if (item.route && !(item.children?.length) && (!item.isParent && !item.isTitle)) {\n <ngx-sidenav-link-item\n [routerLink]=\"item.route\"\n [routerLinkActiveOptions]=\"{ exact: true }\">\n <div class=\"sidenav-menu-label-container\">\n @if (item.icon) {\n @switch (item.iconType) {\n @case ('svg') {\n <img\n class=\"sidenav-menu-icon\"\n [ngSrc]=\"item.icon\"\n alt=\"{{ item.label }} Path\"\n width=\"24\"\n height=\"24\"\n priority>\n }\n @case ('material') {\n <mat-icon role=\"img\" class=\"material-symbols-outlined\">\n {{ item.icon }}\n </mat-icon>\n }\n }\n }\n <span class=\"sidenav-menu-label\">{{ item.label }}</span>\n @if (item.badge) {\n <span [class]=\"`sidenav-menu-badge ${item.badgeColor}`\">{{ item.badge }}</span>\n }\n </div>\n </ngx-sidenav-link-item>\n } @else if (item.children?.length) {\n <ngx-sidenav-link-item (click)=\"onMenuItemClick(item)\">\n <div class=\"sidenav-menu-label-container\">\n @if (item.icon) {\n @switch (item.iconType) {\n @case ('svg') {\n <img\n class=\"sidenav-menu-icon\"\n [ngSrc]=\"item.icon\"\n alt=\"{{ item.label }} Path\"\n width=\"24\"\n height=\"24\"\n priority>\n }\n @case ('material') {\n <mat-icon role=\"img\" class=\"material-symbols-outlined\">\n {{ item.icon }}\n </mat-icon>\n }\n }\n }\n <span class=\"sidenav-menu-label\">{{ item.label }}</span>\n <mat-icon role=\"img\" class=\"material-symbols-outlined\">\n chevron_right\n </mat-icon>\n @if (item.badge) {\n <span [class]=\"`sidenav-menu-badge ${item.badgeColor}`\">{{ item.badge }}</span>\n }\n </div>\n </ngx-sidenav-link-item>\n } @else {\n <ngx-sidenav-link-item (click)=\"onBack()\" [isTitle]=\"true\">\n <div class=\"sidenav-menu-title-container\">\n @if (item.icon) {\n @switch (item.iconType) {\n @case ('svg') {\n <img\n class=\"sidenav-menu-icon\"\n [ngSrc]=\"item.icon\"\n alt=\"{{ item.label }} Path\"\n width=\"24\"\n height=\"24\"\n priority>\n }\n @case ('material') {\n <mat-icon role=\"img\" class=\"material-symbols-outlined\">\n {{ item.icon }}\n </mat-icon>\n }\n }\n }\n <span class=\"sidenav-menu-title\">{{ item.label }}</span>\n </div>\n </ngx-sidenav-link-item>\n }\n </li>\n }\n</ul>\n", styles: [".sidenav-menu{list-style:none;margin:0;padding:0}.sidenav-menu li{margin:0;padding:0}.sidenav-menu li>ngx-sidenav-link-item{width:100%;border-radius:4px;margin:2px 0;box-shadow:none;transition:box-shadow .18s cubic-bezier(.4,0,.2,1),background .18s cubic-bezier(.4,0,.2,1)}.sidenav-menu li>ngx-sidenav-link-item:not(.is-title):not(.is-parent){cursor:pointer;pointer-events:auto}.sidenav-menu li>ngx-sidenav-link-item:not(.is-title):not(.is-parent):hover,.sidenav-menu li>ngx-sidenav-link-item:not(.is-title):not(.is-parent):focus-visible{background:var(--sidenav-link-hover-bg);box-shadow:0 1px 4px 0 var(--sidenav-link-hover-shadow, rgba(0, 0, 0, .04));outline:2px solid var(--sidenav-link-focus);outline-offset:2px}.sidenav-menu li>ngx-sidenav-link-item.active{background:var(--sidenav-link-active-bg, #e3f2fd);box-shadow:0 2px 8px 0 var(--sidenav-link-active-shadow, rgba(0, 0, 0, .08));font-weight:600;outline:2px solid var(--sidenav-link-active-outline, #1976d2);outline-offset:1px}.sidenav-menu li>ngx-sidenav-link-item.is-title,.sidenav-menu li>ngx-sidenav-link-item.is-parent{background:var(--sidenav-link-title-bg, transparent);color:var(--sidenav-link-title-text, #444);font-weight:700;cursor:default;pointer-events:none;box-shadow:none;outline:none}.sidenav-menu li>ngx-sidenav-link-item.is-parent:not(.is-title){background:var(--sidenav-link-parent-bg, #f5f5f5);color:var(--sidenav-link-parent-text, #333);cursor:pointer;pointer-events:auto}.sidenav-menu li>ngx-sidenav-link-item .sidenav-menu-label-container{font-weight:500;letter-spacing:.01em;font-size:15px;color:var(--sidenav-link-text, #222);transition:color .18s cubic-bezier(.4,0,.2,1)}.sidenav-menu li>ngx-sidenav-link-item.active .sidenav-menu-label-container{color:var(--sidenav-link-active-text, #1976d2)}.sidenav-menu li>ngx-sidenav-link-item:hover .sidenav-menu-label-container,.sidenav-menu li>ngx-sidenav-link-item:focus-visible .sidenav-menu-label-container{color:var(--sidenav-link-hover-text, #1976d2)}.sidenav-menu li>ngx-sidenav-menu{margin-left:16px;border-left:2px solid var(--sidenav-link-border, #bdbdbd);padding-left:8px}.sidenav-menu .sidenav-menu-back-btn,.sidenav-menu .sidenav-menu-title-container,.sidenav-menu .sidenav-menu-label-container{display:flex;flex-direction:row;align-items:center;gap:8px;text-decoration:none}.sidenav-menu .sidenav-menu-icon{font-size:20px;display:inline-flex;align-items:center}.sidenav-menu .sidenav-menu-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidenav-menu .sidenav-menu-badge{display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:600;padding:2px 6px;border-radius:4px;text-transform:var(--badge-text-transform, capitalize);white-space:nowrap;cursor:default;color:color-mix(in srgb,var(--badge-theme, #000) 100%,transparent);background-color:color-mix(in srgb,var(--badge-theme, #000) 20%,transparent);border:1px solid color-mix(in srgb,var(--badge-theme, #000) 60%,rgba(0,0,0,.062745098));backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);box-shadow:inset 0 0 0 1px #0000000a;transition:all .2s ease}.sidenav-menu .sidenav-menu-badge.class,.sidenav-menu .sidenav-menu-badge.undecorated_class{--badge-theme: var(--badge-class)}.sidenav-menu .sidenav-menu-badge.constant,.sidenav-menu .sidenav-menu-badge.const{--badge-theme: var(--badge-constant)}.sidenav-menu .sidenav-menu-badge.decorator{--badge-theme: var(--badge-decorator)}.sidenav-menu .sidenav-menu-badge.directive{--badge-theme: var(--badge-directive)}.sidenav-menu .sidenav-menu-badge.element{--badge-theme: var(--badge-element)}.sidenav-menu .sidenav-menu-badge.enum{--badge-theme: var(--badge-enum)}.sidenav-menu .sidenav-menu-badge.function{--badge-theme: var(--badge-function)}.sidenav-menu .sidenav-menu-badge.interface{--badge-theme: var(--badge-interface)}.sidenav-menu .sidenav-menu-badge.pipe{--badge-theme: var(--badge-pipe)}.sidenav-menu .sidenav-menu-badge.impure-pipe{--badge-theme: var(--badge-impure-pipe)}.sidenav-menu .sidenav-menu-badge.module,.sidenav-menu .sidenav-menu-badge.ng_module{--badge-theme: var(--badge-module)}.sidenav-menu .sidenav-menu-badge.alias,.sidenav-menu .sidenav-menu-badge.type_alias{--badge-theme: var(--badge-alias)}.sidenav-menu .sidenav-menu-badge.block{--badge-theme: var(--badge-block)}.sidenav-menu .sidenav-menu-badge.developer_preview,.sidenav-menu .sidenav-menu-badge.deprecated{--badge-theme: var(--badge-deprecated)}.sidenav-menu .sidenav-menu-title{white-space:normal;overflow-wrap:break-word;word-wrap:break-word;max-width:100%}\n"] }]
}] });
class Sidenav {
constructor() {
// ! === Private Services ===
this._sidenav = inject(SidenavService);
// ? === Public inputs signals ===
this.collapseWidth = input(this._sidenav.COLLAPSE_WIDTH, ...(ngDevMode ? [{ debugName: "collapseWidth" }] : [])); // Minimum width for collapse
this.minWidth = input(this._sidenav.minWidth(), ...(ngDevMode ? [{ debugName: "minWidth" }] : [])); // Minimum width for sidenav
this.maxWidth = input(this._sidenav.maxWidth(), ...(ngDevMode ? [{ debugName: "maxWidth" }] : [])); // Maximum width for sidenav
this.initialWidth = input(this._sidenav.INITIAL_WIDTH, ...(ngDevMode ? [{ debugName: "initialWidth" }] : [])); // Initial width for sidenav
this.clearCacheOnReload = input(false, ...(ngDevMode ? [{ debugName: "clearCacheOnReload" }] : [])); // Clear cache on reload
this.position = input('left', ...(ngDevMode ? [{ debugName: "position" }] : []));
this.direction = input('ltr', ...(ngDevMode ? [{ debugName: "direction" }] : [])); // RTL support
this.backdrop = input(false, ...(ngDevMode ? [{ debugName: "backdrop" }] : [])); // Backdrop support
this.canCollapse = input(true, ...(ngDevMode ? [{ debugName: "canCollapse" }] : [])); // Allow collapsing
this.canResize = input(true, ...(ngDevMode ? [{ debugName: "canResize" }] : [])); // Allow resizing
this.menuIconCollapse = input('menu_open', ...(ngDevMode ? [{ debugName: "menuIconCollapse" }] : [])); // Icon for collapsed menu
this.menuIconExpand = input('menu', ...(ngDevMode ? [{ debugName: "menuIconExpand" }] : [])); // Icon for expanded menu
// ? === Public output signals ===
this.opened = output();
this.closed = output();
this.resized = output();
// ! === Private view children ===
this.sidenavContentArea = viewChild(ContentAreaDirective, ...(ngDevMode ? [{ debugName: "sidenavContentArea" }] : []));
// * === Protected computed signals ===
this.currentMenu = this._sidenav.currentMenu;
this.resizingEvent = {
isResizing: false,
startingCursorX: 0,
startingWidth: 0,
};
// ! === Private reactive signals ===
this.#expanded = this._sidenav.isExpanded;
this.#width = this._sidenav.sidenavWidth;
}
// ! === Private reactive signals ===
#expanded;
#width;
ngOnInit() {
const sidenavContentArea = this.sidenavContentArea();
if (!sidenavContentArea)
throw new Error('Sidenav content area is not defined');
this._sidenav.setDynamicContentArea(sidenavContentArea);
this._sidenav.setConfig({
minWidth: this.minWidth(),
maxWidth: this.maxWidth(),
initialWidth: this.initialWidth(),
});
const rootItems = this.currentMenu()?.items ?? [];
this._sidenav.restoreStackHybrid(rootItems); // 👈 key line
this.#updateSidenavVariables(); // Set initial CSS variables for better reactivity
this.#clearSidenavCache();
}
/**
* Getter method to determine if resizing is in progress.
*
* @return {boolean} Returns true if a resizing event is currently active, false otherwise.
*/
get isResizing() {
return this.resizingEvent.isResizing;
}
/**
* Determines if the element is in an expanded state by invoking the `expanded` method.
*
* @return {boolean} Returns true if the element is expanded, otherwise false.
*/
get isExpanded() {
return this.#expanded();
}
/**
* Retrieves the dynamic width of the host element based on the sidenav service
* and the component's expanded or collapsed state.
*
* @return {string} The calculated width of the host element as a string, e.g., "250 px" or a CSS variable value.
*/
get hostWidth() {
// Always use the width from the service for reactivity
const width = this.#width();
return this.isExpanded
? `${width}px`
: 'var(--sidenav-collapsed-width, 56px)';
}
/**
* Determines if the sidenav is positioned to the right.
*
* @return {boolean} Returns true if the sidenav's position is 'right', false otherwise.
*/
get isRight() {
return this.position() === 'right';
}
/**
* Updates the width of the sidenav during a resize operation based on mouse movement.
*
* @param {MouseEvent} event The mouse event containing the current position of the cursor.
* @return {void}
*/
updateSidenavWidth(event) {
if (!this.resizingEvent.isResizing)
return;
const cursorDeltaX = event.clientX - this.resizingEvent.startingCursorX;
const newWidth = this.resizingEvent.startingWidth + cursorDeltaX;
this.#calculateSidenavWidth(newWidth);
}
/**
* Stops the resizing action and resets the cursor style when the mouse button is released.
*
* @return {void} Does not return a value.
*/
stopResizing() {
if (this.resizingEvent.isResizing) {
this.resizingEvent.isResizing = false;
document.body.style.cursor = '';
}
}
/**
* Handles the keydown events for specific elements and performs actions such as resizing a sidebar or toggling navigation.
*
* @param {KeyboardEvent} event The keyboard event triggered by the user.
* @return {void} Does not return a value.
*/
handleKeydown(event) {
if (event.target && event.target.classList.contains('resize-handle')) {
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
const delta = event.key === 'ArrowLeft' ? -10 : 10;
const newWidth = this.#width() + delta;
this.#calculateSidenavWidth(newWidth);
event.preventDefault();
}
}
if (event.key === 'Escape' && this.#expanded()) {
this.toggleNav();
event.preventDefault();
}
}
/**
* Checks the sliding orientation of the side navigation.
*
* @param {'left' | 'right'} sideNav - Specifies the side navigation position (left or right).
* @return {boolean} Returns true if the sliding orientation matches the specified side navigation position, otherwise false.
*/
checkSlideOrientation(sideNav) {
if (this.position() === 'right') {
return sideNav === 'right' ? this._sidenav.isSlidingInFromRight : this._sidenav.isSlidingInFromLeft;
}
return sideNav === 'left' ? this._sidenav.isSlidingInFromLeft : this._sidenav.isSlidingInFromRight;
}
/**
* Toggles the state of the navigation menu by opening it if it is closed or closing it if it is open.
* Emits an event to indicate whether the navigation menu has been opened or closed.
*
* @return {void} This method does not return a value.
*/
toggleNav() {
this._sidenav.toggleSidenav();
if (this.#expanded()) {
this.closed.emit();
}
else {
this.opened.emit();
}
}
/**
* Initializes the resizing process for the sidenav component when it is in an expanded state.
*
* @param {MouseEvent | TouchEvent} event - The event triggered to start resizing, can be a mouse or touch event.
* @return {void} This method does not return a value.
*/
startResizing(event) {
if (!this.isExpanded)
return; // Only allow resizing when expanded
event.preventDefault();
event.stopPropagation();
const clientX = this.#getClientX(event);
this.resizingEvent = {
isResizing: true,
startingCursorX: clientX,
startingWidth: this.#width(),
};
document.body.style.cursor = 'ew-resize';
}
/**
* Updates the CSS variables related to the side navigation layout.
* Sets the values for collapsed width, minimum width, and maximum width
* dynamically using the corresponding methods for calculation.
*
* @return {void} Does not return a value.
*/
#updateSidenavVariables() {
document.documentElement.style.setProperty('--sidenav-collapsed-width', `${this.collapseWidth()}px`);
document.documentElement.style.setProperty('--sidenav-min-width', `${this.minWidth()}px`);
document.documentElement.style.setProperty('--sidenav-max-width', `${this.maxWidth()}px`);
}
/**
* Adjusts the sidenav width to the specified value while respecting the minimum and maximum width constraints.
*
* @param {number} newWidth The new desired width for the sidenav.
* @return {void} This method does not return a value.
*/
#calculateSidenavWidth(newWidth) {
const minWidth = this.minWidth() ?? parseInt(getComputedStyle(document.documentElement).getPropertyValue('--sidenav-min-width'), 10);
const maxWidth = this.maxWidth() ?? parseInt(getComputedStyle(document.documentElement).getPropertyValue('--sidenav-max-width'), 10);
if (minWidth)
newWidth = Math.max(minWidth, newWidth);
if (maxWidth)
newWidth = Math.min(maxWidth, newWidth);
this._sidenav.setSidenavWidth(newWidth);
this.resized.emit(newWidth);
}
/**
* Clears the sidenav cache if the conditions are met.
* Specifically, it checks whether the cache should be cleared on reload
* and triggers a clear operation for the sidenav on the next reload.
*
* @return {void} No return value.
*/
#clearSidenavCache() {
if (this.clearCacheOnReload())
this._sidenav.clearOnNextReload();
}
/**
* Retrieves the clientX coordinate from a MouseEvent or the first touchpoint in a TouchEvent.
*
* @param {MouseEvent | TouchEvent} event - The input event from which the clientX coordinate is extracted. Can be a MouseEvent or a TouchEvent.
* @return {number} The clientX coordinate of the event. If the event is a MouseEvent, returns its clientX value. If the event is a TouchEvent with at least one touchpoint, it returns the clientX value of the first touch. Returns 0 if no valid clientX value is found.
*/
#getClientX(event) {
if (event instanceof MouseEvent) {
return event.clientX;
}
else if (event instanceof TouchEvent && event.touches.length > 0) {
return event.touches[0].clientX;
}
return 0;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: Sidenav, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.1.3", type: Sidenav, isStandalone: true, selector: "ngx-sidenav", inputs: { collapseWidth: { classPropertyName: "collapseWidth", publicName: "collapseWidth", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "maxWidth", isSignal: true, isRequired: false, transformFunction: null }, initialWidth: { classPropertyName: "initialWidth", publicName: "initialWidth", isSignal: true, isRequired: false, transformFunction: null }, clearCacheOnReload: { classPropertyName: "clearCacheOnReload", publicName: "clearCacheOnReload", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, backdrop: { classPropertyName: "backdrop", publicName: "backdrop", isSignal: true, isRequired: false, transformFunction: null }, canCollapse: { classPropertyName: "canCollapse", publicName: "canCollapse", isSignal: true, isRequired: false, transformFunction: null }, canResize: { classPropertyName: "canResize", publicName: "canResize", isSignal: true, isRequired: false, transformFunction: null }, menuIconCollapse: { classPropertyName: "menuIconCollapse", publicName: "menuIconCollapse", isSignal: true, isRequired: false, transformFunction: null }, menuIconExpand: { classPropertyName: "menuIconExpand", publicName: "menuIconExpand", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { opened: "opened", closed: "closed", resized: "resized" }, host: { listeners: { "window:mousemove": "updateSidenavWidth($event)", "window:mouseup": "stopResizing()", "keydown": "handleKeydown($event)" }, properties: { "class.resizing": "this.isResizing", "class.is-expanded": "this.isExpanded", "style.width": "this.hostWidth", "class.sidenav-right": "this.isRight" } }, viewQueries: [{ propertyName: "sidenavContentArea", first: true, predicate: ContentAreaDirective, descendants: true, isSignal: true }], ngImport: i0, template: "@let menu = currentMenu();\n@let resizing = resizingEvent;\n\n<!-- Enhanced Toggle Button - moved outside container to always be visible -->\n@if (menu && canCollapse()) {\n <button\n class=\"sidenav-toggle-btn\"\n [class.expanded]=\"isExpanded\"\n [class.rotated]=\"position() === 'right'\"\n type=\"button\"\n (click)=\"toggleNav()\"\n [attr.aria-label]=\"'Menu Toggle Button'\"\n tabindex=\"0\">\n <mat-icon role=\"button\" class=\"material-symbols-outlined\">\n {{ isExpanded ? menuIconCollapse() : menuIconExpand() }}\n </mat-icon>\n </button>\n}\n\n<div class=\"sidenav-container\"\n [class.is-expanded]=\"isExpanded\"\n [class.slide-in-from-left]=\"checkSlideOrientation('left')\"\n [class.slide-in-from-right]=\"checkSlideOrientation('right')\"\n [attr.dir]=\"direction()\"\n role=\"navigation\"\n [attr.aria-label]=\"'Side navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n [attr.aria-orientation]=\"position() === 'left' ? 'vertical' : 'horizontal'\"\n [attr.aria-modal]=\"backdrop() ? isExpanded : null\"\n [attr.tabindex]=\"isExpanded ? 0 : -1\">\n\n <div class=\"sidenav-content\">\n <!-- Section to load components menu -->\n <ng-template ngxSidenavContentArea></ng-template>\n\n <!-- Section to load items menu -->\n @if (menu) {\n <ngx-sidenav-menu\n [items]=\"menu.items\"\n [parentLabel]=\"menu.title\"\n [parentBadge]=\"menu.badge\"\n [parentBadgeColor]=\"menu.badgeColor\"/>\n }\n </div>\n</div>\n\n<!-- Optional Backdrop for modal mode -->\n@if (backdrop() && isExpanded) {\n <div class=\"sidenav-backdrop\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n (click)=\"toggleNav()\">\n </div>\n}\n\n<!-- Enhanced Resize Handle with better icon -->\n@if (canResize()) {\n <div class=\"resize-handle\"\n tabindex=\"0\"\n [class.resizing]=\"resizing.isResizing\"\n (mousedown)=\"startResizing($event)\"\n (touchstart)=\"startResizing($event)\"\n [attr.aria-label]=\"'Resize navigation panel'\"\n role=\"separator\">\n\n <!-- Always visible resize indicator -->\n <div class=\"resize-indicator\"></div>\n\n @if (isExpanded) {\n <!-- Enhanced resize icon with better styling -->\n <div class=\"resize-icon\" matTooltip=\"Resize navigation panel\" matTooltipPosition=\"above\"\n [matTooltipPosition]=\"position() === 'left' ? 'right' : 'left'\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <!-- Vertical grip dots pattern for better resize indication -->\n <circle cx=\"8\" cy=\"5\" r=\"1\"/>\n <circle cx=\"8\" cy=\"9\" r=\"1\"/>\n <circle cx=\"8\" cy=\"13\" r=\"1\"/>\n <circle cx=\"8\" cy=\"17\" r=\"1\"/>\n <circle cx=\"8\" cy=\"21\" r=\"1\"/>\n\n <circle cx=\"12\" cy=\"3\" r=\"1\"/>\n <circle cx=\"12\" cy=\"7\" r=\"1\"/>\n <circle cx=\"12\" cy=\"11\" r=\"1\"/>\n <circle cx=\"12\" cy=\"15\" r=\"1\"/>\n <circle cx=\"12\" cy=\"19\" r=\"1\"/>\n <circle cx=\"12\" cy=\"23\" r=\"1\"/>\n\n <circle cx=\"16\" cy=\"5\" r=\"1\"/>\n <circle cx=\"16\" cy=\"9\" r=\"1\"/>\n <circle cx=\"16\" cy=\"13\" r=\"1\"/>\n <circle cx=\"16\" cy=\"17\" r=\"1\"/>\n <circle cx=\"16\" cy=\"21\" r=\"1\"/>\n </svg>\n </div>\n }\n </div>\n}\n", styles: [":host{height:100vh;display:block;position:relative;box-sizing:border-box;min-width:var(--sidenav-collapsed-width, 56px);max-width:var(--sidenav-max-width, 400px);transition:width .3s cubic-bezier(.02,.68,.63,.98),min-width .3s cubic-bezier(.02,.68,.63,.98),max-width .3s cubic-bezier(.02,.68,.63,.98);will-change:width,min-width,max-width;background:none;border-right:none;border-left:none;box-shadow:none;padding-inline:var(--sidenav-padding-inline, 1rem);padding-block:var(--sidenav-padding-block, 2rem)}:host.is-expanded{min-width:var(--sidenav-min-width, 180px);overflow:visible}:host.is-expanded .sidenav-container{visibility:visible}:host.is-expanded .toggle-button{right:-14px}:host:not(.is-expanded) .toggle-button{transform:translate(50%)}:host:not(.is-expanded) .sidenav-container{visibility:hidden;pointer-events:none}:host.resizing{transition:none;-webkit-user-select:none;user-select:none}:host.sidenav-right{right:0;left:auto;border-right:none}:host.sidenav-right .toggle-button{left:-14px;right:auto}:host.sidenav-right .resize-handle{left:0;right:auto;border-radius:0 4px 4px 0}.sidenav-container{position:relative;height:100%;background:var(--surface-container);transition:all .3s cubic-bezier(.4,0,.2,1)}.sidenav-container .sidenav-content{height:100%;overflow-y:auto;overflow-x:hidden}.sidenav-container .sidenav-content::-webkit-scrollbar{width:8px}.sidenav-container .sidenav-content::-webkit-scrollbar-track{background:transparent;border-radius:4px}.sidenav-container .sidenav-content::-webkit-scrollbar-thumb{background:var(--outline-variant, #c7c5d0);border-radius:4px}.sidenav-container .sidenav-content::-webkit-scrollbar-thumb:hover{background:var(--outline, #79747e)}.sidenav-toggle-btn{position:absolute;top:45px;right:-17px;z-index:1005;display:flex;align-items:center;gap:8px;padding:5px;background:var(--surface-container-high, #f3f3f3);border:1px solid var(--outline-variant, #c7c5d0);border-radius:20px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 2px 8px #0000001a}.sidenav-toggle-btn:hover{background:var(--surface-container-highest, #eaddff);box-shadow:0 4px 16px #00000026;transform:scale(1.05)}.sidenav-toggle-btn:focus{outline:2px solid var(--primary, #6750a4);outline-offset:2px}.sidenav-toggle-btn.rotated{transform:rotate(180deg)}.sidenav-toggle-btn .back-label{font-size:.875rem;font-weight:500}.sidenav-backdrop{position:fixed;top:0;left:0;width:100vw;height:100vh;background:#00000052;z-index:999;cursor:pointer;animation:fadeIn .3s cubic-bezier(.4,0,.2,1)}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.resize-handle{position:absolute;top:0;right:-8px;width:16px;height:100%;cursor:col-resize;z-index:1002;display:flex;align-items:center;justify-content:center;background:transparent;transition:all .2s cubic-bezier(.4,0,.2,1)}.resize-handle .resize-indicator{position:absolute;width:3px;height:100%;background:var(--outline-variant, #c7c5d0);border-radius:2px;opacity:.7;transition:all .2s cubic-bezier(.4,0,.2,1)}.resize-handle .resize-icon{width:32px;height:48px;background:var(--surface-container-high, #f3f3f3);border:1px solid var(--outline-variant, #c7c5d0);border-radius:16px;display:flex;align-items:center;justify-content:center;opacity:0;transform:scale(.8);transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 4px 12px #00000026;z-index:1}.resize-handle .resize-icon svg{width:20px;height:20px;color:var(--on-surface-variant, #49454f)}.resize-handle:hover .resize-indicator,.resize-handle:focus .resize-indicator{background:var(--primary, #6750a4);opacity:1;width:4px}.resize-handle:hover .resize-icon,.resize-handle:focus .resize-icon{opacity:1;transform:scale(1);background:var(--primary-container, #eaddff);border-color:var(--primary, #6750a4)}.resize-handle:hover .resize-icon svg,.resize-handle:focus .resize-icon svg{color:var(--on-primary-container, #21005d)}.resize-handle:focus{outline:none}.resize-handle:focus .resize-icon{outline:2px solid var(--primary, #6750a4);outline-offset:2px}.resize-handle.resizing .resize-indicator{background:var(--primary, #6750a4);opacity:1;width:4px}.resize-handle.resizing .resize-icon{opacity:1;transform:scale(1.1);background:var(--primary, #6750a4);border-color:var(--primary, #6750a4)}.resize-handle.resizing .resize-icon svg{color:var(--on-primary, #ffffff)}.resize-handle.mobile-enhanced{width:20px;right:-10px}.resize-handle.mobile-enhanced .resize-grip{position:relative;display:flex;align-items:center;justify-content:center}.resize-handle.mobile-enhanced .resize-icon-enhanced{width:40px;height:56px;background:var(--surface-container-high, #f3f3f3);border:1px solid var(--outline-variant, #c7c5d0);border-radius:20px;display:flex;align-items:center;justify-content:center;opacity:0;transform:scale(.8);transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 4px 12px #00000026}.resize-handle.mobile-enhanced .resize-icon-enhanced svg{width:24px;height:24px;color:var(--on-surface-variant, #49454f)}.resize-handle.mobile-enhanced:hover .resize-icon-enhanced{opacity:1;transform:scale(1);background:var(--primary-container, #eaddff);border-color:var(--primary, #6750a4)}.resize-handle.mobile-enhanced:hover .resize-icon-enhanced svg{color:var(--on-primary-container, #21005d)}.resize-handle.mobile-enhanced.resizing .resize-icon-enhanced{opacity:1;transform:scale(1.1);background:var(--primary, #6750a4);border-color:var(--primary, #6750a4)}.resize-handle.mobile-enhanced.resizing .resize-icon-enhanced svg{color:var(--on-primary, #ffffff)}@media (max-width: 768px){.resize-handle.hide-on-mobile{display:none}}@media (max-width: 768px){:host .sidenav-container{border-radius:0 16px 16px 0;box-shadow:0 8px 32px #0000003d;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}:host .resize-handle{display:none}}@media (prefers-reduced-motion: reduce){.sidenav-container,.toggle-button,.resize-handle,.resize-icon-enhanced{transition-duration:.1s}.sidenav-backdrop{animation-duration:.1s}}\n"], dependencies: [{ kind: "directive", type: ContentAreaDirective, selector: "[ngxSidenavContentArea]" }, { kind: "component", type: SidenavMenu, selector: "ngx-sidenav-menu", inputs: ["items", "parentLabel", "parentBadge", "parentBadgeColor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.3", ngImport: i0, type: Sidenav, decorators: [{
type: Component,
args: [{ selector: 'ngx-sidenav', imports: [ContentAreaDirective, SidenavMenu, MatIconModule, MatTooltipModule], template: "@let menu = currentMenu();\n@let resizing = resizingEvent;\n\n<!-- Enhanced Toggle Button - moved outside container to always be visible -->\n@if (menu && canCollapse()) {\n <button\n class=\"sidenav-toggle-btn\"\n [class.expanded]=\"isExpanded\"\n [class.rotated]=\"position() === 'right'\"\n type=\"button\"\n (click)=\"toggleNav()\"\n [attr.aria-label]=\"'Menu Toggle Button'\"\n tabindex=\"0\">\n <mat-icon role=\"button\" class=\"material-symbols-outlined\">\n {{ isExpanded ? menuIconCollapse() : menuIconExpand() }}\n </mat-icon>\n </button>\n}\n\n<div class=\"sidenav-container\"\n [class.is-expanded]=\"isExpanded\"\n [class.slide-in-from-left]=\"checkSlideOrientation('left')\"\n [class.slide-in-from-right]=\"checkSlideOrientation('right')\"\n [attr.dir]=\"direction()\"\n role=\"navigation\"\n [attr.aria-label]=\"'Side navigation'\"\n [attr.aria-expanded]=\"isExpanded\"\n [attr.aria-orientation]=\"position() === 'left' ? 'vertical' : 'horizontal'\"\n [attr.aria-modal]=\"backdrop() ? isExpanded : null\"\n [attr.tabindex]=\"isExpanded ? 0 : -1\">\n\n <div class=\"sidenav-content\">\n <!-- Section to load components menu -->\n <ng-template ngxSidenavContentArea></ng-template>\n\n <!-- Section to load items menu -->\n @if (menu) {\n <ngx-sidenav-menu\n [items]=\"menu.items\"\n [parentLabel]=\"menu.title\"\n [parentBadge]=\"menu.badge\"\n [parentBadgeColor]=\"menu.badgeColor\"/>\n }\n </div>\n</div>\n\n<!-- Optional Backdrop for modal mode -->\n@if (backdrop() && isExpanded) {\n <div class=\"sidenav-backdrop\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n (click)=\"toggleNav()\">\n </div>\n}\n\n<!-- Enhanced Resize Handle with better icon -->\n@if (canResize()) {\n <div class=\"resize-handle\"\n tabindex=\"0\"\n [class.resizing]=\"resizing.isResizing\"\n (mousedown)=\"startResizing($event)\"\n (touchstart)=\"startResizing($event)\"\n [attr.aria-label]=\"'Resize navigation panel'\"\n role=\"separator\">\n\n <!-- Always visible resize indicator -->\n <div class=\"resize-indicator\"></div>\n\n @if (isExpanded) {\n <!-- Enhanced resize icon with better styling -->\n <div class=\"resize-icon\" matTooltip=\"Resize navigation panel\" matTooltipPosition=\"above\"\n [matTooltipPosition]=\"position() === 'left' ? 'right' : 'left'\">\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <!-- Vertical grip dots pattern for better resize indication -->\n <circle cx=\"8\" cy=\"5\" r=\"1\"/>\n <circle cx=\"8\" cy=\"9\" r=\"1\"/>\n <circle cx=\"8\" cy=\"13\" r=\"1\"/>\n <circle cx=\"8\" cy=\"17\" r=\"1\"/>\n <circle cx=\"8\" cy=\"21\" r=\"1\"/>\n\n <circle cx=\"12\" cy=\"3\" r=\"1\"/>\n <circle cx=\"12\" cy=\"7\" r=\"1\"/>\n <circle cx=\"12\" cy=\"11\" r=\"1\"/>\n <circle cx=\"12\" cy=\"15\" r=\"1\"/>\n <circle cx=\"12\" cy=\"19\" r=\"1\"/>\n <circle cx=\"12\" cy=\"23\" r=\"1\"/>\n\n <circle cx=\"16\" cy=\"5\" r=\"1\"/>\n <circle cx=\"16\" cy=\"9\" r=\"1\"/>\n <circle cx=\"16\" cy=\"13\" r=\"1\"/>\n <circle cx=\"16\" cy=\"17\" r=\"1\"/>\n <circle cx=\"16\" cy=\"21\" r=\"1\"/>\n </svg>\n </div>\n }\n </div>\n}\n", styles: [":host{height:100vh;display:block;position:relative;box-sizing:border-box;min-width:var(--sidenav-collapsed-width, 56px);max-width:var(--sidenav-max-width, 400px);transition:width .3s cubic-bezier(.02,.68,.63,.98),min-width .3s cubic-bezier(.02,.68,.63,.98),max-width .3s cubic-bezier(.02,.68,.63,.98);will-change:width,min-width,max-width;background:none;border-right:none;border-left:none;box-shadow:none;padding-inline:var(--sidenav-padding-inline, 1rem);padding-block:var(--sidenav-padding-block, 2rem)}:host.is-expanded{min-width:var(--sidenav-min-width, 180px);overflow:visible}:host.is-expanded .sidenav-container{visibility:visible}:host.is-expanded .toggle-button{right:-14px}:host:not(.is-expanded) .toggle-button{transform:translate(50%)}:host:not(.is-expanded) .sidenav-container{visibility:hidden;pointer-events:none}:host.resizing{transition:none;-webkit-user-select:none;user-select:none}:host.sidenav-right{right:0;left:auto;border-right:none}:host.sidenav-right .toggle-button{left:-14px;right:auto}:host.sidenav-right .resize-handle{left:0;right:auto;border-radius:0 4px 4px 0}.sidenav-container{position:relative;height:100%;background:var(--surface-container);transition:all .3s cubic-bezier(.4,0,.2,1)}.sidenav-container .sidenav-content{height:100%;overflow-y:auto;overflow-x:hidden}.sidenav-container .sidenav-content::-webkit-scrollbar{width:8px}.sidenav-container .sidenav-content::-webkit-scrollbar-track{background:transparent;border-radius:4px}.sidenav-container .sidenav-content::-webkit-scrollbar-thumb{background:var(--outline-variant, #c7c5d0);border-radius:4px}.sidenav-container .sidenav-content::-webkit-scrollbar-thumb:hover{background:var(--outline, #79747e)}.sidenav-toggle-btn{position:absolute;top:45px;right:-17px;z-index:1005;display:flex;align-items:center;gap:8px;padding:5px;background:var(--surface-container-high, #f3f3f3);border:1px solid var(--outline-variant, #c7c5d0);border-radius:20px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 2px 8px #0000001a}.sidenav-toggle-btn:hover{background:var(--surface-container-highest, #eaddff);box-shadow:0 4px 16px #00000026;transform:scale(1.05)}.sidenav-toggle-btn:focus{outline:2px solid var(--primary, #6750a4);outline-offset:2px}.sidenav-toggle-btn.rotated{transform:rotate(180deg)}.sidenav-toggle-btn .back-label{font-size:.875rem;font-weight:500}.sidenav-backdrop{position:fixed;top:0;left:0;width:100vw;height:100vh;background:#00000052;z-index:999;cursor:pointer;animation:fadeIn .3s cubic-bezier(.4,0,.2,1)}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.resize-handle{position:absolute;top:0;right:-8px;width:16px;height:100%;cursor:col-resize;z-index:1002;display:flex;align-items:center;justify-content:center;background:transparent;transition:all .2s cubic-bezier(.4,0,.2,1)}.resize-handle .resize-indicator{position:absolute;width:3px;height:100%;background:var(--outline-variant, #c7c5d0);border-radius:2px;opacity:.7;transition:all .2s cubic-bezier(.4,0,.2,1)}.resize-handle .resize-icon{width:32px;height:48px;background:var(--surface-container-high, #f3f3f3);border:1px solid var(--outline-variant, #c7c5d0);border-radius:16px;display:flex;align-items:center;justify-content:center;opacity:0;transform:scale(.8);transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 4px 12px #00000026;z-index:1}.resize-handle .resize-icon svg{width:20px;height:20px;color:var(--on-surface-variant, #49454f)}.resize-handle:hover .resize-indicator,.resize-handle:focus .resize-indicator{background:var(--primary, #6750a4);opacity:1;width:4px}.resize-handle:hover .resize-icon,.resize-handle:focus .resize-icon{opacity:1;transform:scale(1);background:var(--primary-container, #eaddff);border-color:var(--primary, #6750a4)}.resize-handle:hover .resize-icon svg,.resize-handle:focus .resize-icon svg{color:var(--on-primary-container, #21005d)}.resize-handle:focus{outline:none}.resize-handle:focus .resize-icon{outline:2px solid var(--primary, #6750a4);outline-offset:2px}.resize-handle.resizing .resize-indicator{background:var(--primary, #6750a4);opacity:1;width:4px}.resize-handle.resizing .resize-icon{opacity:1;transform:scale(1.1);background:var(--primary, #6750a4);border-color:var(--primary, #6750a4)}.resize-handle.resizing .resize-icon svg{color:var(--on-primary, #ffffff)}.resize-handle.mobile-enhanced{width:20px;right:-10px}.resize-handle.mobile-enhanced .resize-grip{position:relative;display:flex;align-items:center;justify-content:center}.resize-handle.mobile-enhanced .resize-icon-enhanced{width:40px;height:56px;background:var(--surface-container-high, #f3f3f3);border:1px solid var(--outline-variant, #c7c5d0);border-radius:20px;display:flex;align-items:center;justify-content:center;opacity:0;transform:scale(.8);transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 4px 12px #00000026}.resize-handle.mobile-enhanced .resize-icon-enhanced svg{width:24px;height:24px;color:var(--on-surface-variant, #49454f)}.resize-handle.mobile-enhanced:hover .resize-icon-enhanced{opacity:1;transform:scale(1);background:var(--primary-container, #eaddff);border-color:var(--primary, #6750a4)}.resize-handle.mobile-enhanced:hover .resize-icon-enhanced svg{color:var(--on-primary-container, #21005d)}.resize-handle.mobile-enhanced.resizing .resize-icon-enhanced{opacity:1;transform:scale(1.1);background:var(--primary, #6750a4);border-color:var(--primary, #6750a4)}.resize-handle.mobile-enhanced.resizing .resize-icon-enhanced svg{color:var(--on-primary, #ffffff)}@media (max-width: 768px){.resize-handle.hide-on-mobile{display:none}}@media (max-width: 768px){:host .sidenav-container{border-radius:0 16px 16px 0;box-shadow:0 8px 32px #0000003d;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px)}:host .resize-handle{display:none}}@media (prefers-reduced-motion: reduce){.sidenav-container,.toggle-button,.resize-handle,.resize-icon-enhanced{transition-duration:.1s}.sidenav-backdrop{animation-duration:.1s}}\n"] }]
}], propDecorators: { isResizing: [{
type: HostBinding,
args: ['class.resizing']
}], isExpanded: [{
type: HostBinding,
args: ['class.is-expanded']
}], hostWidth: [{
type: HostBinding,
args: ['style.width']
}], isRight: [{
type: HostBinding,
args: ['class.sidenav-right']
}], updateSidenavWidth: [{
type: HostListener,
args: ['window:mousemove', ['$event']]
}], stopResizing: [{
type: HostListener,
args: ['window:mouseup']
}], handleKeydown: [{
type: HostListener,
args: ['keydown', ['$event']]
}] } });
/*
* Public API Surface of code-editor
*/
/**
* Generated bundle index. Do not edit.
*/
export { ContentAreaDirective, LinkItem, Sidenav, SidenavMenu, SidenavService, isComponentStackItem, isMenuStackItem };
//# sourceMappingURL=fsegurai-ngx-sidenav.mjs.map