@neural-ui/core 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, computed, input, output, effect, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
2
+ import { inject, ElementRef, computed, input, output, effect, HostListener, ChangeDetectionStrategy, ViewEncapsulation, Component } from '@angular/core';
3
3
  import { DOCUMENT } from '@angular/common';
4
4
  import { NeuUrlStateService } from '@neural-ui/core/url-state';
5
5
  import { NeuIconComponent } from '@neural-ui/core/icon';
@@ -48,8 +48,10 @@ function unlockDocumentScroll(document) {
48
48
  */
49
49
  class NeuSidebarComponent {
50
50
  document = inject(DOCUMENT);
51
+ elementRef = inject(ElementRef);
51
52
  urlState = inject(NeuUrlStateService);
52
53
  openParam = computed(() => this.urlState.getParam(this.urlParam()), ...(ngDevMode ? [{ debugName: "openParam" }] : /* istanbul ignore next */ []));
54
+ previousActiveElement = null;
53
55
  /** Posición del sidebar: izquierda o derecha de la pantalla / Sidebar position: left or right of the screen */
54
56
  side = input('left', ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
55
57
  /** QueryParam que controla el estado. Default: 'menu' (?menu=open) / QueryParam that controls the state. Default: 'menu' (?menu=open) */
@@ -68,8 +70,15 @@ class NeuSidebarComponent {
68
70
  ariaLabel = input('Menú de navegación', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
69
71
  /** Etiqueta accesible para el botón cerrar / Accessible label for the close button */
70
72
  closeLabel = input('Cerrar menú de navegación', ...(ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []));
73
+ /**
74
+ * Modo colapsado: el sidebar muestra solo iconos (icon-only en desktop).
75
+ * Útil para sidebars persistentes en modo compact / Collapsed mode: sidebar shows only icons.
76
+ */
77
+ collapsed = input(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : /* istanbul ignore next */ []));
71
78
  /** Emite cuando el usuario cierra el sidebar (overlay click o botón) / Emits when the user closes the sidebar (overlay click or button) */
72
79
  closeRequested = output();
80
+ /** Emite cuando el estado collapsed cambia / Emits when collapsed state changes */
81
+ collapsedChange = output();
73
82
  /** Signal reactivo: true si el sidebar debe mostrarse / Reactive signal: true if the sidebar should be shown */
74
83
  isOpen = computed(() => {
75
84
  if (this.persistent())
@@ -87,6 +96,27 @@ class NeuSidebarComponent {
87
96
  unlockDocumentScroll(this.document);
88
97
  });
89
98
  });
99
+ // Focus management: save previous element and move focus to sidebar when opening in drawer mode
100
+ effect(() => {
101
+ const isOpenNow = this.isOpen();
102
+ const isPersistent = this.persistent();
103
+ if (isPersistent) {
104
+ return;
105
+ }
106
+ if (isOpenNow) {
107
+ // Save the current active element
108
+ this.previousActiveElement = this.document.activeElement;
109
+ // Move focus to first focusable element inside the sidebar
110
+ this.focusFirstElement();
111
+ }
112
+ else if (this.previousActiveElement && this.previousActiveElement.focus) {
113
+ // Restore focus to the element that had it before opening
114
+ setTimeout(() => {
115
+ this.previousActiveElement?.focus();
116
+ this.previousActiveElement = null;
117
+ }, 0);
118
+ }
119
+ });
90
120
  }
91
121
  /** Abre el sidebar — añade ?{urlParam}=open a la URL / Opens the sidebar — adds ?{urlParam}=open to the URL */
92
122
  open(replaceUrl = false) {
@@ -97,8 +127,67 @@ class NeuSidebarComponent {
97
127
  this.urlState.setParam(this.urlParam(), null, true);
98
128
  this.closeRequested.emit();
99
129
  }
130
+ /** Alterna el estado collapsed / Toggles collapsed state */
131
+ toggleCollapsed() {
132
+ const newState = !this.collapsed();
133
+ this.collapsedChange.emit(newState);
134
+ }
135
+ /** Obtiene los elementos focusables dentro del sidebar / Gets focusable elements inside sidebar */
136
+ getFocusableElements() {
137
+ const focusableSelectors = [
138
+ 'button',
139
+ 'a[href]',
140
+ 'input:not([disabled])',
141
+ 'select:not([disabled])',
142
+ 'textarea:not([disabled])',
143
+ '[tabindex]:not([tabindex="-1"])',
144
+ ].join(',');
145
+ const aside = this.elementRef.nativeElement.querySelector('aside.neu-sidebar');
146
+ if (!aside) {
147
+ return [];
148
+ }
149
+ return Array.from(aside.querySelectorAll(focusableSelectors));
150
+ }
151
+ /** Mueve el foco al primer elemento focusable / Moves focus to first focusable element */
152
+ focusFirstElement() {
153
+ setTimeout(() => {
154
+ const focusable = this.getFocusableElements();
155
+ if (focusable.length > 0) {
156
+ focusable[0].focus();
157
+ }
158
+ }, 0);
159
+ }
160
+ /** Focus trap: cicla entre elementos focusables con Tab/Shift+Tab / Focus trap cycling */
161
+ onKeyDown(event) {
162
+ if (!this.isOpen() || this.persistent()) {
163
+ return;
164
+ }
165
+ if (event.key !== 'Tab') {
166
+ return;
167
+ }
168
+ const focusable = this.getFocusableElements();
169
+ if (focusable.length === 0) {
170
+ return;
171
+ }
172
+ const activeElement = this.document.activeElement;
173
+ const currentIndex = focusable.indexOf(activeElement);
174
+ if (event.shiftKey) {
175
+ // Shift+Tab — go to previous
176
+ if (currentIndex <= 0) {
177
+ event.preventDefault();
178
+ focusable[focusable.length - 1].focus();
179
+ }
180
+ }
181
+ else {
182
+ // Tab — go to next
183
+ if (currentIndex >= focusable.length - 1) {
184
+ event.preventDefault();
185
+ focusable[0].focus();
186
+ }
187
+ }
188
+ }
100
189
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NeuSidebarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
101
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: NeuSidebarComponent, isStandalone: true, selector: "neu-sidebar", inputs: { side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, urlParam: { classPropertyName: "urlParam", publicName: "urlParam", isSignal: true, isRequired: false, transformFunction: null }, persistent: { classPropertyName: "persistent", publicName: "persistent", isSignal: true, isRequired: false, transformFunction: null }, hideHeader: { classPropertyName: "hideHeader", publicName: "hideHeader", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closeRequested: "closeRequested" }, ngImport: i0, template: `
190
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: NeuSidebarComponent, isStandalone: true, selector: "neu-sidebar", inputs: { side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, urlParam: { classPropertyName: "urlParam", publicName: "urlParam", isSignal: true, isRequired: false, transformFunction: null }, persistent: { classPropertyName: "persistent", publicName: "persistent", isSignal: true, isRequired: false, transformFunction: null }, hideHeader: { classPropertyName: "hideHeader", publicName: "hideHeader", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closeRequested: "closeRequested", collapsedChange: "collapsedChange" }, host: { listeners: { "keydown": "onKeyDown($event)" } }, ngImport: i0, template: `
102
191
  <!-- Overlay de fondo — solo visible en modo overlay -->
103
192
  @if (!persistent()) {
104
193
  <div
@@ -114,8 +203,10 @@ class NeuSidebarComponent {
114
203
  class="neu-sidebar"
115
204
  [class.neu-sidebar--open]="isOpen()"
116
205
  [class.neu-sidebar--persistent]="persistent()"
206
+ [class.neu-sidebar--collapsed]="collapsed()"
117
207
  [class.neu-sidebar--right]="side() === 'right'"
118
- role="navigation"
208
+ [attr.role]="persistent() ? 'navigation' : 'dialog'"
209
+ [attr.aria-modal]="!persistent()"
119
210
  [attr.aria-label]="ariaLabel()"
120
211
  [attr.aria-hidden]="!isOpen() && !persistent()"
121
212
  [attr.inert]="!isOpen() && !persistent() ? '' : null"
@@ -149,7 +240,7 @@ class NeuSidebarComponent {
149
240
  <ng-content select="[neu-sidebar-footer]" />
150
241
  </div>
151
242
  </aside>
152
- `, isInline: true, styles: [".neu-sidebar__overlay{position:fixed;inset:0;background:var(--neu-overlay-bg-soft);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:var(--neu-z-overlay);opacity:0;pointer-events:none;transition:opacity var(--neu-transition-slow)}.neu-sidebar__overlay--visible{opacity:1;pointer-events:all}.neu-sidebar{position:fixed;top:0;left:0;height:100dvh;width:100%;background:var(--neu-sidebar-bg, var(--neu-surface));border-right:1px solid var(--neu-border);z-index:var(--neu-z-sidebar);display:flex;flex-direction:column;transform:translate(-100%);transition:transform var(--neu-transition-slow),box-shadow var(--neu-transition-slow);overflow:hidden}@media(min-width:400px){.neu-sidebar{width:var(--neu-sidebar-width, 260px)}}.neu-sidebar--open:not(.neu-sidebar--persistent):not(.neu-sidebar--right){transform:translate(0);box-shadow:var(--neu-shadow-lg)}.neu-sidebar--right{left:auto;right:0;border-right:none;border-left:1px solid var(--neu-border);transform:translate(100%)}.neu-sidebar--right.neu-sidebar--open:not(.neu-sidebar--persistent){transform:translate(0);box-shadow:var(--neu-shadow-lg)}.neu-sidebar--persistent{position:sticky;top:var(--neu-header-height);height:calc(100dvh - var(--neu-header-height));transform:none;box-shadow:none;border-right:1px solid var(--neu-border);flex-shrink:0;z-index:var(--neu-z-base)}.neu-sidebar__header{display:flex;align-items:center;justify-content:space-between;padding:0 var(--neu-space-5);min-height:var(--neu-header-height, 64px);border-bottom:1px solid var(--neu-border);flex-shrink:0}.neu-sidebar__title{font-family:var(--neu-font-sans);font-size:var(--neu-text-sm);font-weight:700;letter-spacing:-.01em;color:var(--neu-text)}.neu-sidebar__close{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;background:transparent;border:1px solid transparent;border-radius:var(--neu-radius);color:var(--neu-text-muted);cursor:pointer;flex-shrink:0;transition:background-color var(--neu-transition),color var(--neu-transition),border-color var(--neu-transition)}.neu-sidebar__close:hover{background:var(--neu-surface-2);color:var(--neu-text);border-color:var(--neu-border)}.neu-sidebar__close:focus-visible{outline:none;box-shadow:var(--neu-focus-ring-strong)}.neu-sidebar__content{flex:1;overflow-y:auto;overflow-x:hidden;padding:var(--neu-space-3) 0;scrollbar-width:thin;scrollbar-color:var(--neu-surface-3) transparent}.neu-sidebar__content::-webkit-scrollbar{width:4px}.neu-sidebar__content::-webkit-scrollbar-track{background:transparent}.neu-sidebar__content::-webkit-scrollbar-thumb{background:var(--neu-surface-3);border-radius:var(--neu-radius-full)}.neu-sidebar__footer{padding:var(--neu-space-4) var(--neu-space-5);border-top:1px solid var(--neu-border);flex-shrink:0}.neu-sidebar__footer:empty{display:none}\n"], dependencies: [{ kind: "component", type: NeuIconComponent, selector: "neu-icon", inputs: ["name", "strokeWidth", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
243
+ `, isInline: true, styles: [".neu-sidebar__overlay{position:fixed;inset:0;background:var(--neu-overlay-bg-soft);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:var(--neu-z-overlay);opacity:0;pointer-events:none;transition:opacity var(--neu-transition-slow)}.neu-sidebar__overlay--visible{opacity:1;pointer-events:all}.neu-sidebar{position:fixed;top:0;left:0;height:100dvh;width:100%;background:var(--neu-sidebar-bg, var(--neu-surface));border-right:1px solid var(--neu-border);z-index:var(--neu-z-sidebar);display:flex;flex-direction:column;transform:translate(-100%);transition:transform var(--neu-transition-slow),box-shadow var(--neu-transition-slow);overflow:hidden}@media(min-width:400px){.neu-sidebar{width:var(--neu-sidebar-width, 260px)}}.neu-sidebar--open:not(.neu-sidebar--persistent):not(.neu-sidebar--right){transform:translate(0);box-shadow:var(--neu-shadow-lg)}.neu-sidebar--right{left:auto;right:0;border-right:none;border-left:1px solid var(--neu-border);transform:translate(100%)}.neu-sidebar--right.neu-sidebar--open:not(.neu-sidebar--persistent){transform:translate(0);box-shadow:var(--neu-shadow-lg)}.neu-sidebar--persistent{position:sticky;top:var(--neu-header-height);height:calc(100dvh - var(--neu-header-height));transform:none;box-shadow:none;border-right:1px solid var(--neu-border);flex-shrink:0;z-index:var(--neu-z-base)}.neu-sidebar__header{display:flex;align-items:center;justify-content:space-between;padding:0 var(--neu-space-5);min-height:var(--neu-header-height, 64px);border-bottom:1px solid var(--neu-border);flex-shrink:0}.neu-sidebar__title{font-family:var(--neu-font-sans);font-size:var(--neu-text-sm);font-weight:700;letter-spacing:-.01em;color:var(--neu-text)}.neu-sidebar__close{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;background:transparent;border:1px solid transparent;border-radius:var(--neu-radius);color:var(--neu-text-muted);cursor:pointer;flex-shrink:0;transition:background-color var(--neu-transition),color var(--neu-transition),border-color var(--neu-transition)}.neu-sidebar__close:hover{background:var(--neu-surface-2);color:var(--neu-text);border-color:var(--neu-border)}.neu-sidebar__close:focus-visible{outline:none;box-shadow:var(--neu-focus-ring-strong)}.neu-sidebar__content{flex:1;overflow-y:auto;overflow-x:hidden;padding:var(--neu-space-3) 0;scrollbar-width:thin;scrollbar-color:var(--neu-surface-3) transparent}.neu-sidebar__content::-webkit-scrollbar{width:4px}.neu-sidebar__content::-webkit-scrollbar-track{background:transparent}.neu-sidebar__content::-webkit-scrollbar-thumb{background:var(--neu-surface-3);border-radius:var(--neu-radius-full)}.neu-sidebar__footer{padding:var(--neu-space-4) var(--neu-space-5);border-top:1px solid var(--neu-border);flex-shrink:0}.neu-sidebar__footer:empty{display:none}.neu-sidebar--collapsed{width:var(--neu-sidebar-collapsed-width, 60px)!important}.neu-sidebar--collapsed .neu-sidebar__header{flex-direction:column;justify-content:center;padding:0;min-height:var(--neu-header-height, 64px)}.neu-sidebar--collapsed .neu-sidebar__header .neu-sidebar__title{display:none}.neu-sidebar--collapsed .neu-sidebar__content{padding:var(--neu-space-2) 0}.neu-sidebar--collapsed a span:not([neu-sidebar-header]),.neu-sidebar--collapsed button span:not([neu-sidebar-header]){max-width:0;overflow:hidden;opacity:0;transition:max-width var(--neu-transition),opacity var(--neu-transition)}.neu-sidebar--collapsed .neu-sidebar__footer{padding:var(--neu-space-2) var(--neu-space-3);min-height:auto}.neu-sidebar--collapsed .neu-sidebar__footer span{display:none}.neu-sidebar--collapsed .neu-sidebar__close{width:40px;height:40px}\n"], dependencies: [{ kind: "component", type: NeuIconComponent, selector: "neu-icon", inputs: ["name", "strokeWidth", "size"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
153
244
  }
154
245
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: NeuSidebarComponent, decorators: [{
155
246
  type: Component,
@@ -169,8 +260,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
169
260
  class="neu-sidebar"
170
261
  [class.neu-sidebar--open]="isOpen()"
171
262
  [class.neu-sidebar--persistent]="persistent()"
263
+ [class.neu-sidebar--collapsed]="collapsed()"
172
264
  [class.neu-sidebar--right]="side() === 'right'"
173
- role="navigation"
265
+ [attr.role]="persistent() ? 'navigation' : 'dialog'"
266
+ [attr.aria-modal]="!persistent()"
174
267
  [attr.aria-label]="ariaLabel()"
175
268
  [attr.aria-hidden]="!isOpen() && !persistent()"
176
269
  [attr.inert]="!isOpen() && !persistent() ? '' : null"
@@ -204,8 +297,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
204
297
  <ng-content select="[neu-sidebar-footer]" />
205
298
  </div>
206
299
  </aside>
207
- `, styles: [".neu-sidebar__overlay{position:fixed;inset:0;background:var(--neu-overlay-bg-soft);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:var(--neu-z-overlay);opacity:0;pointer-events:none;transition:opacity var(--neu-transition-slow)}.neu-sidebar__overlay--visible{opacity:1;pointer-events:all}.neu-sidebar{position:fixed;top:0;left:0;height:100dvh;width:100%;background:var(--neu-sidebar-bg, var(--neu-surface));border-right:1px solid var(--neu-border);z-index:var(--neu-z-sidebar);display:flex;flex-direction:column;transform:translate(-100%);transition:transform var(--neu-transition-slow),box-shadow var(--neu-transition-slow);overflow:hidden}@media(min-width:400px){.neu-sidebar{width:var(--neu-sidebar-width, 260px)}}.neu-sidebar--open:not(.neu-sidebar--persistent):not(.neu-sidebar--right){transform:translate(0);box-shadow:var(--neu-shadow-lg)}.neu-sidebar--right{left:auto;right:0;border-right:none;border-left:1px solid var(--neu-border);transform:translate(100%)}.neu-sidebar--right.neu-sidebar--open:not(.neu-sidebar--persistent){transform:translate(0);box-shadow:var(--neu-shadow-lg)}.neu-sidebar--persistent{position:sticky;top:var(--neu-header-height);height:calc(100dvh - var(--neu-header-height));transform:none;box-shadow:none;border-right:1px solid var(--neu-border);flex-shrink:0;z-index:var(--neu-z-base)}.neu-sidebar__header{display:flex;align-items:center;justify-content:space-between;padding:0 var(--neu-space-5);min-height:var(--neu-header-height, 64px);border-bottom:1px solid var(--neu-border);flex-shrink:0}.neu-sidebar__title{font-family:var(--neu-font-sans);font-size:var(--neu-text-sm);font-weight:700;letter-spacing:-.01em;color:var(--neu-text)}.neu-sidebar__close{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;background:transparent;border:1px solid transparent;border-radius:var(--neu-radius);color:var(--neu-text-muted);cursor:pointer;flex-shrink:0;transition:background-color var(--neu-transition),color var(--neu-transition),border-color var(--neu-transition)}.neu-sidebar__close:hover{background:var(--neu-surface-2);color:var(--neu-text);border-color:var(--neu-border)}.neu-sidebar__close:focus-visible{outline:none;box-shadow:var(--neu-focus-ring-strong)}.neu-sidebar__content{flex:1;overflow-y:auto;overflow-x:hidden;padding:var(--neu-space-3) 0;scrollbar-width:thin;scrollbar-color:var(--neu-surface-3) transparent}.neu-sidebar__content::-webkit-scrollbar{width:4px}.neu-sidebar__content::-webkit-scrollbar-track{background:transparent}.neu-sidebar__content::-webkit-scrollbar-thumb{background:var(--neu-surface-3);border-radius:var(--neu-radius-full)}.neu-sidebar__footer{padding:var(--neu-space-4) var(--neu-space-5);border-top:1px solid var(--neu-border);flex-shrink:0}.neu-sidebar__footer:empty{display:none}\n"] }]
208
- }], ctorParameters: () => [], propDecorators: { side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], urlParam: [{ type: i0.Input, args: [{ isSignal: true, alias: "urlParam", required: false }] }], persistent: [{ type: i0.Input, args: [{ isSignal: true, alias: "persistent", required: false }] }], hideHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideHeader", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], closeRequested: [{ type: i0.Output, args: ["closeRequested"] }] } });
300
+ `, styles: [".neu-sidebar__overlay{position:fixed;inset:0;background:var(--neu-overlay-bg-soft);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);z-index:var(--neu-z-overlay);opacity:0;pointer-events:none;transition:opacity var(--neu-transition-slow)}.neu-sidebar__overlay--visible{opacity:1;pointer-events:all}.neu-sidebar{position:fixed;top:0;left:0;height:100dvh;width:100%;background:var(--neu-sidebar-bg, var(--neu-surface));border-right:1px solid var(--neu-border);z-index:var(--neu-z-sidebar);display:flex;flex-direction:column;transform:translate(-100%);transition:transform var(--neu-transition-slow),box-shadow var(--neu-transition-slow);overflow:hidden}@media(min-width:400px){.neu-sidebar{width:var(--neu-sidebar-width, 260px)}}.neu-sidebar--open:not(.neu-sidebar--persistent):not(.neu-sidebar--right){transform:translate(0);box-shadow:var(--neu-shadow-lg)}.neu-sidebar--right{left:auto;right:0;border-right:none;border-left:1px solid var(--neu-border);transform:translate(100%)}.neu-sidebar--right.neu-sidebar--open:not(.neu-sidebar--persistent){transform:translate(0);box-shadow:var(--neu-shadow-lg)}.neu-sidebar--persistent{position:sticky;top:var(--neu-header-height);height:calc(100dvh - var(--neu-header-height));transform:none;box-shadow:none;border-right:1px solid var(--neu-border);flex-shrink:0;z-index:var(--neu-z-base)}.neu-sidebar__header{display:flex;align-items:center;justify-content:space-between;padding:0 var(--neu-space-5);min-height:var(--neu-header-height, 64px);border-bottom:1px solid var(--neu-border);flex-shrink:0}.neu-sidebar__title{font-family:var(--neu-font-sans);font-size:var(--neu-text-sm);font-weight:700;letter-spacing:-.01em;color:var(--neu-text)}.neu-sidebar__close{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;background:transparent;border:1px solid transparent;border-radius:var(--neu-radius);color:var(--neu-text-muted);cursor:pointer;flex-shrink:0;transition:background-color var(--neu-transition),color var(--neu-transition),border-color var(--neu-transition)}.neu-sidebar__close:hover{background:var(--neu-surface-2);color:var(--neu-text);border-color:var(--neu-border)}.neu-sidebar__close:focus-visible{outline:none;box-shadow:var(--neu-focus-ring-strong)}.neu-sidebar__content{flex:1;overflow-y:auto;overflow-x:hidden;padding:var(--neu-space-3) 0;scrollbar-width:thin;scrollbar-color:var(--neu-surface-3) transparent}.neu-sidebar__content::-webkit-scrollbar{width:4px}.neu-sidebar__content::-webkit-scrollbar-track{background:transparent}.neu-sidebar__content::-webkit-scrollbar-thumb{background:var(--neu-surface-3);border-radius:var(--neu-radius-full)}.neu-sidebar__footer{padding:var(--neu-space-4) var(--neu-space-5);border-top:1px solid var(--neu-border);flex-shrink:0}.neu-sidebar__footer:empty{display:none}.neu-sidebar--collapsed{width:var(--neu-sidebar-collapsed-width, 60px)!important}.neu-sidebar--collapsed .neu-sidebar__header{flex-direction:column;justify-content:center;padding:0;min-height:var(--neu-header-height, 64px)}.neu-sidebar--collapsed .neu-sidebar__header .neu-sidebar__title{display:none}.neu-sidebar--collapsed .neu-sidebar__content{padding:var(--neu-space-2) 0}.neu-sidebar--collapsed a span:not([neu-sidebar-header]),.neu-sidebar--collapsed button span:not([neu-sidebar-header]){max-width:0;overflow:hidden;opacity:0;transition:max-width var(--neu-transition),opacity var(--neu-transition)}.neu-sidebar--collapsed .neu-sidebar__footer{padding:var(--neu-space-2) var(--neu-space-3);min-height:auto}.neu-sidebar--collapsed .neu-sidebar__footer span{display:none}.neu-sidebar--collapsed .neu-sidebar__close{width:40px;height:40px}\n"] }]
301
+ }], ctorParameters: () => [], propDecorators: { side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], urlParam: [{ type: i0.Input, args: [{ isSignal: true, alias: "urlParam", required: false }] }], persistent: [{ type: i0.Input, args: [{ isSignal: true, alias: "persistent", required: false }] }], hideHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideHeader", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }], collapsed: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsed", required: false }] }], closeRequested: [{ type: i0.Output, args: ["closeRequested"] }], collapsedChange: [{ type: i0.Output, args: ["collapsedChange"] }], onKeyDown: [{
302
+ type: HostListener,
303
+ args: ['keydown', ['$event']]
304
+ }] } });
209
305
 
210
306
  /**
211
307
  * Generated bundle index. Do not edit.
@@ -1 +1 @@
1
- {"version":3,"file":"neural-ui-core-sidebar.mjs","sources":["../../../../projects/ui-core/sidebar/neu-sidebar.component.ts","../../../../projects/ui-core/sidebar/neural-ui-core-sidebar.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n output,\n} from '@angular/core';\nimport { DOCUMENT } from '@angular/common';\nimport { NeuUrlStateService } from '@neural-ui/core/url-state';\nimport { NeuIconComponent } from '@neural-ui/core/icon';\n\nlet overlayScrollLockCount = 0;\nlet previousHtmlOverflow = '';\nlet previousBodyOverflow = '';\n\nfunction lockDocumentScroll(document: Document): void {\n if (overlayScrollLockCount === 0) {\n previousHtmlOverflow = document.documentElement.style.overflow;\n previousBodyOverflow = document.body.style.overflow;\n document.documentElement.style.overflow = 'hidden';\n document.body.style.overflow = 'hidden';\n }\n\n overlayScrollLockCount += 1;\n}\n\nfunction unlockDocumentScroll(document: Document): void {\n if (overlayScrollLockCount === 0) {\n return;\n }\n\n overlayScrollLockCount -= 1;\n\n if (overlayScrollLockCount === 0) {\n document.documentElement.style.overflow = previousHtmlOverflow;\n document.body.style.overflow = previousBodyOverflow;\n }\n}\n\n/**\n * NeuralUI Sidebar Component\n *\n * El estado abierto/cerrado se gestiona automáticamente desde la URL / The open/closed state is automatically managed from the URL\n * via NeuUrlStateService (?menu=open por defecto). / via NeuUrlStateService (?menu=open by default).\n *\n * Modos:\n * - overlay (default): panel flotante sobre el contenido con backdrop / floating panel above content with backdrop\n * - persistent: sidebar fijo integrado en el layout (desktop) / fixed sidebar integrated in the layout (desktop)\n *\n * Uso:\n * <neu-sidebar urlParam=\"menu\" [persistent]=\"isDesktop()\">\n * <span neu-sidebar-header>Mi App</span>\n * <nav>...</nav>\n * <div neu-sidebar-footer>...</div>\n * </neu-sidebar>\n *\n * Abrir desde cualquier parte: / Open from anywhere:\n * inject(NeuUrlStateService).setParam('menu', 'open', false);\n */\n@Component({\n selector: 'neu-sidebar',\n imports: [NeuIconComponent],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <!-- Overlay de fondo — solo visible en modo overlay -->\n @if (!persistent()) {\n <div\n class=\"neu-sidebar__overlay\"\n [class.neu-sidebar__overlay--visible]=\"isOpen()\"\n (click)=\"close()\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel lateral -->\n <aside\n class=\"neu-sidebar\"\n [class.neu-sidebar--open]=\"isOpen()\"\n [class.neu-sidebar--persistent]=\"persistent()\"\n [class.neu-sidebar--right]=\"side() === 'right'\"\n role=\"navigation\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-hidden]=\"!isOpen() && !persistent()\"\n [attr.inert]=\"!isOpen() && !persistent() ? '' : null\"\n >\n <!-- Cabecera -->\n @if (!hideHeader()) {\n <div class=\"neu-sidebar__header\">\n <div class=\"neu-sidebar__title\">\n <ng-content select=\"[neu-sidebar-header]\" />\n </div>\n @if (!persistent()) {\n <button\n class=\"neu-sidebar__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeLabel()\"\n type=\"button\"\n >\n <neu-icon name=\"lucideX\" size=\"18px\" aria-hidden=\"true\" />\n </button>\n }\n </div>\n }\n\n <!-- Contenido -->\n <div class=\"neu-sidebar__content\">\n <ng-content />\n </div>\n\n <!-- Footer -->\n <div class=\"neu-sidebar__footer\">\n <ng-content select=\"[neu-sidebar-footer]\" />\n </div>\n </aside>\n `,\n styleUrl: './neu-sidebar.component.scss',\n})\nexport class NeuSidebarComponent {\n private readonly document = inject(DOCUMENT);\n private readonly urlState = inject(NeuUrlStateService);\n private readonly openParam = computed(() => this.urlState.getParam(this.urlParam()));\n\n /** Posición del sidebar: izquierda o derecha de la pantalla / Sidebar position: left or right of the screen */\n side = input<'left' | 'right'>('left');\n\n /** QueryParam que controla el estado. Default: 'menu' (?menu=open) / QueryParam that controls the state. Default: 'menu' (?menu=open) */\n urlParam = input<string>('menu');\n\n /**\n * Modo persistente: el sidebar está siempre visible como parte del layout.\n * Usar en desktop (≥768px). El overlay y el toggle por URL no aplican.\n */\n persistent = input<boolean>(false);\n\n /**\n * Ocultar la cabecera del sidebar. Útil cuando el header ya está en el layout\n * principal y el sidebar persistente no necesita su propio header.\n */\n hideHeader = input<boolean>(false);\n\n /** Etiqueta accesible para el <aside> / Accessible label for the <aside> */\n ariaLabel = input<string>('Menú de navegación');\n\n /** Etiqueta accesible para el botón cerrar / Accessible label for the close button */\n closeLabel = input<string>('Cerrar menú de navegación');\n\n /** Emite cuando el usuario cierra el sidebar (overlay click o botón) / Emits when the user closes the sidebar (overlay click or button) */\n closeRequested = output<void>();\n\n /** Signal reactivo: true si el sidebar debe mostrarse / Reactive signal: true if the sidebar should be shown */\n readonly isOpen = computed(() => {\n if (this.persistent()) return true;\n return this.openParam()() === 'open';\n });\n\n constructor() {\n effect((onCleanup) => {\n const shouldLockScroll = !this.persistent() && this.isOpen();\n\n if (!shouldLockScroll) {\n return;\n }\n\n lockDocumentScroll(this.document);\n\n onCleanup(() => {\n unlockDocumentScroll(this.document);\n });\n });\n }\n\n /** Abre el sidebar — añade ?{urlParam}=open a la URL / Opens the sidebar — adds ?{urlParam}=open to the URL */\n open(replaceUrl = false): void {\n this.urlState.setParam(this.urlParam(), 'open', replaceUrl);\n }\n\n /** Cierra el sidebar — elimina el parámetro de la URL / Closes the sidebar — removes the URL parameter */\n close(): void {\n this.urlState.setParam(this.urlParam(), null, true);\n this.closeRequested.emit();\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public_api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"neural-ui-core-sidebar.mjs","sources":["../../../../projects/ui-core/sidebar/neu-sidebar.component.ts","../../../../projects/ui-core/sidebar/neural-ui-core-sidebar.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n ViewEncapsulation,\n computed,\n effect,\n inject,\n input,\n output,\n ElementRef,\n HostListener,\n} from '@angular/core';\nimport { DOCUMENT } from '@angular/common';\nimport { NeuUrlStateService } from '@neural-ui/core/url-state';\nimport { NeuIconComponent } from '@neural-ui/core/icon';\n\nlet overlayScrollLockCount = 0;\nlet previousHtmlOverflow = '';\nlet previousBodyOverflow = '';\n\nfunction lockDocumentScroll(document: Document): void {\n if (overlayScrollLockCount === 0) {\n previousHtmlOverflow = document.documentElement.style.overflow;\n previousBodyOverflow = document.body.style.overflow;\n document.documentElement.style.overflow = 'hidden';\n document.body.style.overflow = 'hidden';\n }\n\n overlayScrollLockCount += 1;\n}\n\nfunction unlockDocumentScroll(document: Document): void {\n if (overlayScrollLockCount === 0) {\n return;\n }\n\n overlayScrollLockCount -= 1;\n\n if (overlayScrollLockCount === 0) {\n document.documentElement.style.overflow = previousHtmlOverflow;\n document.body.style.overflow = previousBodyOverflow;\n }\n}\n\n/**\n * NeuralUI Sidebar Component\n *\n * El estado abierto/cerrado se gestiona automáticamente desde la URL / The open/closed state is automatically managed from the URL\n * via NeuUrlStateService (?menu=open por defecto). / via NeuUrlStateService (?menu=open by default).\n *\n * Modos:\n * - overlay (default): panel flotante sobre el contenido con backdrop / floating panel above content with backdrop\n * - persistent: sidebar fijo integrado en el layout (desktop) / fixed sidebar integrated in the layout (desktop)\n *\n * Uso:\n * <neu-sidebar urlParam=\"menu\" [persistent]=\"isDesktop()\">\n * <span neu-sidebar-header>Mi App</span>\n * <nav>...</nav>\n * <div neu-sidebar-footer>...</div>\n * </neu-sidebar>\n *\n * Abrir desde cualquier parte: / Open from anywhere:\n * inject(NeuUrlStateService).setParam('menu', 'open', false);\n */\n@Component({\n selector: 'neu-sidebar',\n imports: [NeuIconComponent],\n encapsulation: ViewEncapsulation.None,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <!-- Overlay de fondo — solo visible en modo overlay -->\n @if (!persistent()) {\n <div\n class=\"neu-sidebar__overlay\"\n [class.neu-sidebar__overlay--visible]=\"isOpen()\"\n (click)=\"close()\"\n aria-hidden=\"true\"\n ></div>\n }\n\n <!-- Panel lateral -->\n <aside\n class=\"neu-sidebar\"\n [class.neu-sidebar--open]=\"isOpen()\"\n [class.neu-sidebar--persistent]=\"persistent()\"\n [class.neu-sidebar--collapsed]=\"collapsed()\"\n [class.neu-sidebar--right]=\"side() === 'right'\"\n [attr.role]=\"persistent() ? 'navigation' : 'dialog'\"\n [attr.aria-modal]=\"!persistent()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-hidden]=\"!isOpen() && !persistent()\"\n [attr.inert]=\"!isOpen() && !persistent() ? '' : null\"\n >\n <!-- Cabecera -->\n @if (!hideHeader()) {\n <div class=\"neu-sidebar__header\">\n <div class=\"neu-sidebar__title\">\n <ng-content select=\"[neu-sidebar-header]\" />\n </div>\n @if (!persistent()) {\n <button\n class=\"neu-sidebar__close\"\n (click)=\"close()\"\n [attr.aria-label]=\"closeLabel()\"\n type=\"button\"\n >\n <neu-icon name=\"lucideX\" size=\"18px\" aria-hidden=\"true\" />\n </button>\n }\n </div>\n }\n\n <!-- Contenido -->\n <div class=\"neu-sidebar__content\">\n <ng-content />\n </div>\n\n <!-- Footer -->\n <div class=\"neu-sidebar__footer\">\n <ng-content select=\"[neu-sidebar-footer]\" />\n </div>\n </aside>\n `,\n styleUrl: './neu-sidebar.component.scss',\n})\nexport class NeuSidebarComponent {\n private readonly document = inject(DOCUMENT);\n private readonly elementRef = inject(ElementRef);\n private readonly urlState = inject(NeuUrlStateService);\n private readonly openParam = computed(() => this.urlState.getParam(this.urlParam()));\n private previousActiveElement: HTMLElement | null = null;\n\n /** Posición del sidebar: izquierda o derecha de la pantalla / Sidebar position: left or right of the screen */\n side = input<'left' | 'right'>('left');\n\n /** QueryParam que controla el estado. Default: 'menu' (?menu=open) / QueryParam that controls the state. Default: 'menu' (?menu=open) */\n urlParam = input<string>('menu');\n\n /**\n * Modo persistente: el sidebar está siempre visible como parte del layout.\n * Usar en desktop (≥768px). El overlay y el toggle por URL no aplican.\n */\n persistent = input<boolean>(false);\n\n /**\n * Ocultar la cabecera del sidebar. Útil cuando el header ya está en el layout\n * principal y el sidebar persistente no necesita su propio header.\n */\n hideHeader = input<boolean>(false);\n\n /** Etiqueta accesible para el <aside> / Accessible label for the <aside> */\n ariaLabel = input<string>('Menú de navegación');\n\n /** Etiqueta accesible para el botón cerrar / Accessible label for the close button */\n closeLabel = input<string>('Cerrar menú de navegación');\n\n /**\n * Modo colapsado: el sidebar muestra solo iconos (icon-only en desktop).\n * Útil para sidebars persistentes en modo compact / Collapsed mode: sidebar shows only icons.\n */\n collapsed = input<boolean>(false);\n\n /** Emite cuando el usuario cierra el sidebar (overlay click o botón) / Emits when the user closes the sidebar (overlay click or button) */\n closeRequested = output<void>();\n\n /** Emite cuando el estado collapsed cambia / Emits when collapsed state changes */\n collapsedChange = output<boolean>();\n\n /** Signal reactivo: true si el sidebar debe mostrarse / Reactive signal: true if the sidebar should be shown */\n readonly isOpen = computed(() => {\n if (this.persistent()) return true;\n return this.openParam()() === 'open';\n });\n\n constructor() {\n effect((onCleanup) => {\n const shouldLockScroll = !this.persistent() && this.isOpen();\n\n if (!shouldLockScroll) {\n return;\n }\n\n lockDocumentScroll(this.document);\n\n onCleanup(() => {\n unlockDocumentScroll(this.document);\n });\n });\n\n // Focus management: save previous element and move focus to sidebar when opening in drawer mode\n effect(() => {\n const isOpenNow = this.isOpen();\n const isPersistent = this.persistent();\n\n if (isPersistent) {\n return;\n }\n\n if (isOpenNow) {\n // Save the current active element\n this.previousActiveElement = this.document.activeElement as HTMLElement;\n\n // Move focus to first focusable element inside the sidebar\n this.focusFirstElement();\n } else if (this.previousActiveElement && this.previousActiveElement.focus) {\n // Restore focus to the element that had it before opening\n setTimeout(() => {\n this.previousActiveElement?.focus();\n this.previousActiveElement = null;\n }, 0);\n }\n });\n }\n\n /** Abre el sidebar — añade ?{urlParam}=open a la URL / Opens the sidebar — adds ?{urlParam}=open to the URL */\n open(replaceUrl = false): void {\n this.urlState.setParam(this.urlParam(), 'open', replaceUrl);\n }\n\n /** Cierra el sidebar — elimina el parámetro de la URL / Closes the sidebar — removes the URL parameter */\n close(): void {\n this.urlState.setParam(this.urlParam(), null, true);\n this.closeRequested.emit();\n }\n\n /** Alterna el estado collapsed / Toggles collapsed state */\n toggleCollapsed(): void {\n const newState = !this.collapsed();\n this.collapsedChange.emit(newState);\n }\n\n /** Obtiene los elementos focusables dentro del sidebar / Gets focusable elements inside sidebar */\n private getFocusableElements(): HTMLElement[] {\n const focusableSelectors = [\n 'button',\n 'a[href]',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n ].join(',');\n\n const aside = this.elementRef.nativeElement.querySelector('aside.neu-sidebar');\n if (!aside) {\n return [];\n }\n\n return Array.from(aside.querySelectorAll(focusableSelectors)) as HTMLElement[];\n }\n\n /** Mueve el foco al primer elemento focusable / Moves focus to first focusable element */\n private focusFirstElement(): void {\n setTimeout(() => {\n const focusable = this.getFocusableElements();\n if (focusable.length > 0) {\n focusable[0].focus();\n }\n }, 0);\n }\n\n /** Focus trap: cicla entre elementos focusables con Tab/Shift+Tab / Focus trap cycling */\n @HostListener('keydown', ['$event'])\n onKeyDown(event: KeyboardEvent): void {\n if (!this.isOpen() || this.persistent()) {\n return;\n }\n\n if (event.key !== 'Tab') {\n return;\n }\n\n const focusable = this.getFocusableElements();\n if (focusable.length === 0) {\n return;\n }\n\n const activeElement = this.document.activeElement as HTMLElement;\n const currentIndex = focusable.indexOf(activeElement);\n\n if (event.shiftKey) {\n // Shift+Tab — go to previous\n if (currentIndex <= 0) {\n event.preventDefault();\n focusable[focusable.length - 1].focus();\n }\n } else {\n // Tab — go to next\n if (currentIndex >= focusable.length - 1) {\n event.preventDefault();\n focusable[0].focus();\n }\n }\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public_api';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neural-ui/core",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Modern Angular UI component library built with signals, standalone components, and OnPush change detection.",
5
5
  "author": "PedroMorenoTrujillo",
6
6
  "keywords": [
@@ -22,8 +22,10 @@ import * as _angular_core from '@angular/core';
22
22
  */
23
23
  declare class NeuSidebarComponent {
24
24
  private readonly document;
25
+ private readonly elementRef;
25
26
  private readonly urlState;
26
27
  private readonly openParam;
28
+ private previousActiveElement;
27
29
  /** Posición del sidebar: izquierda o derecha de la pantalla / Sidebar position: left or right of the screen */
28
30
  side: _angular_core.InputSignal<"left" | "right">;
29
31
  /** QueryParam que controla el estado. Default: 'menu' (?menu=open) / QueryParam that controls the state. Default: 'menu' (?menu=open) */
@@ -42,8 +44,15 @@ declare class NeuSidebarComponent {
42
44
  ariaLabel: _angular_core.InputSignal<string>;
43
45
  /** Etiqueta accesible para el botón cerrar / Accessible label for the close button */
44
46
  closeLabel: _angular_core.InputSignal<string>;
47
+ /**
48
+ * Modo colapsado: el sidebar muestra solo iconos (icon-only en desktop).
49
+ * Útil para sidebars persistentes en modo compact / Collapsed mode: sidebar shows only icons.
50
+ */
51
+ collapsed: _angular_core.InputSignal<boolean>;
45
52
  /** Emite cuando el usuario cierra el sidebar (overlay click o botón) / Emits when the user closes the sidebar (overlay click or button) */
46
53
  closeRequested: _angular_core.OutputEmitterRef<void>;
54
+ /** Emite cuando el estado collapsed cambia / Emits when collapsed state changes */
55
+ collapsedChange: _angular_core.OutputEmitterRef<boolean>;
47
56
  /** Signal reactivo: true si el sidebar debe mostrarse / Reactive signal: true if the sidebar should be shown */
48
57
  readonly isOpen: _angular_core.Signal<boolean>;
49
58
  constructor();
@@ -51,8 +60,16 @@ declare class NeuSidebarComponent {
51
60
  open(replaceUrl?: boolean): void;
52
61
  /** Cierra el sidebar — elimina el parámetro de la URL / Closes the sidebar — removes the URL parameter */
53
62
  close(): void;
63
+ /** Alterna el estado collapsed / Toggles collapsed state */
64
+ toggleCollapsed(): void;
65
+ /** Obtiene los elementos focusables dentro del sidebar / Gets focusable elements inside sidebar */
66
+ private getFocusableElements;
67
+ /** Mueve el foco al primer elemento focusable / Moves focus to first focusable element */
68
+ private focusFirstElement;
69
+ /** Focus trap: cicla entre elementos focusables con Tab/Shift+Tab / Focus trap cycling */
70
+ onKeyDown(event: KeyboardEvent): void;
54
71
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<NeuSidebarComponent, never>;
55
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<NeuSidebarComponent, "neu-sidebar", never, { "side": { "alias": "side"; "required": false; "isSignal": true; }; "urlParam": { "alias": "urlParam"; "required": false; "isSignal": true; }; "persistent": { "alias": "persistent"; "required": false; "isSignal": true; }; "hideHeader": { "alias": "hideHeader"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "closeLabel": { "alias": "closeLabel"; "required": false; "isSignal": true; }; }, { "closeRequested": "closeRequested"; }, never, ["[neu-sidebar-header]", "*", "[neu-sidebar-footer]"], true, never>;
72
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<NeuSidebarComponent, "neu-sidebar", never, { "side": { "alias": "side"; "required": false; "isSignal": true; }; "urlParam": { "alias": "urlParam"; "required": false; "isSignal": true; }; "persistent": { "alias": "persistent"; "required": false; "isSignal": true; }; "hideHeader": { "alias": "hideHeader"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "ariaLabel"; "required": false; "isSignal": true; }; "closeLabel": { "alias": "closeLabel"; "required": false; "isSignal": true; }; "collapsed": { "alias": "collapsed"; "required": false; "isSignal": true; }; }, { "closeRequested": "closeRequested"; "collapsedChange": "collapsedChange"; }, never, ["[neu-sidebar-header]", "*", "[neu-sidebar-footer]"], true, never>;
56
73
  }
57
74
 
58
75
  export { NeuSidebarComponent };