@softwarity/rail-nav 1.0.16 → 1.0.18

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.
package/README.md CHANGED
@@ -137,11 +137,93 @@ Navigation item with MD3 pill animation.
137
137
  | `label` | `string` | - | Label text (below icon when collapsed, beside when expanded) |
138
138
  | `badge` | `string \| number \| boolean` | - | Badge value. Use `true` for a small dot badge |
139
139
  | `active` | `boolean` | `false` | Whether this item is active (for non-router usage) |
140
+ | `for` | `TemplateRef \| null` | `null` | Template projected as a contextual drawer when the item is hovered or clicked. Aliased to `for` for `mat-datepicker-toggle`-style ergonomics. See [Contextual drawer](#contextual-drawer). |
140
141
 
141
142
  | Output | Type | Description |
142
143
  |--------|------|-------------|
143
144
  | `itemClick` | `void` | Emitted when clicked (automatically collapses rail) |
144
145
 
146
+ ### RailnavSeparatorComponent
147
+
148
+ Visual separator between groups of `<rail-nav-item>`. Hairline 1px rule, vertically centered in a fixed-height host so the spacing stays consistent across collapsed / expanded modes.
149
+
150
+ ```html
151
+ <rail-nav>
152
+ <rail-nav-item label="Home">...</rail-nav-item>
153
+ <rail-nav-separator />
154
+ <rail-nav-item label="Trash">...</rail-nav-item>
155
+ </rail-nav>
156
+ ```
157
+
158
+ Color overridable via `--rail-nav-separator-color`.
159
+
160
+ ### RailnavSpacerComponent
161
+
162
+ Flexible spacer (`flex: 1 1 auto`). Place between two groups of items to push everything after it to the bottom of the rail — the classic pattern for primary nav on top and Settings / Profile anchored at the bottom.
163
+
164
+ ```html
165
+ <rail-nav>
166
+ <rail-nav-item label="Home">...</rail-nav-item>
167
+ <rail-nav-item label="Inbox">...</rail-nav-item>
168
+ <rail-nav-spacer />
169
+ <rail-nav-item label="Settings">...</rail-nav-item>
170
+ </rail-nav>
171
+ ```
172
+
173
+ ## Contextual drawer
174
+
175
+ Any rail item can declare a contextual side drawer with `[for]="someTemplate"`. The library renders the template in a CDK overlay positioned right next to the rail, handles hover-intent (200ms), close debounce (300ms for cursor transit), outside-tap dismiss, auto-close when the rail is expanded, and re-targets the same overlay when the cursor moves between trigger items.
176
+
177
+ ```ts
178
+ import {
179
+ RailnavComponent,
180
+ RailnavContainerComponent,
181
+ RailnavContentComponent,
182
+ RailnavItemComponent,
183
+ } from '@softwarity/rail-nav';
184
+
185
+ @Component({
186
+ imports: [
187
+ RailnavComponent,
188
+ RailnavContainerComponent,
189
+ RailnavContentComponent,
190
+ RailnavItemComponent,
191
+ ],
192
+ template: `
193
+ <rail-nav-container>
194
+ <rail-nav>
195
+ <rail-nav-item label="Workspaces" [for]="wsTpl">
196
+ <mat-icon>workspaces</mat-icon>
197
+ </rail-nav-item>
198
+ </rail-nav>
199
+ <rail-nav-content>...</rail-nav-content>
200
+ </rail-nav-container>
201
+
202
+ <ng-template #wsTpl>
203
+ <a routerLink="/ws/1">Workspace 1</a>
204
+ <a routerLink="/ws/2">Workspace 2</a>
205
+ </ng-template>
206
+ `,
207
+ })
208
+ export class AppComponent {}
209
+ ```
210
+
211
+ The drawer panel applies its own chrome (background, shadow, rounded right corner) and a default nav-list layout (flex column, padding, gap, fixed width) — your template only needs the children. Both desktop hover and mobile tap open the drawer; the lib swallows the synthetic mouse events touch devices fire after a tap so the drawer doesn't close instantly.
212
+
213
+ ### Drawer CSS custom properties
214
+
215
+ Override per rail (or globally) — all read with sane Material defaults as fallback.
216
+
217
+ | Variable | Default | Description |
218
+ |----------|---------|-------------|
219
+ | `--rail-nav-drawer-width` | `240px` | Fixed drawer width |
220
+ | `--rail-nav-drawer-padding` | `12px` | Inner padding |
221
+ | `--rail-nav-drawer-gap` | `4px` | Vertical gap between children |
222
+ | `--rail-nav-drawer-surface` | `var(--mat-sys-surface)` | Background color |
223
+ | `--rail-nav-drawer-on-surface` | `var(--mat-sys-on-surface)` | Text color |
224
+ | `--rail-nav-drawer-shadow` | `var(--mat-sys-level2)` | Elevation |
225
+ | `--rail-nav-drawer-radius` | `0 12px 12px 0` | Outer border-radius |
226
+
145
227
  ## SCSS Theming
146
228
 
147
229
  Use the SCSS mixin to customize colors following the Angular Material pattern:
@@ -1,11 +1,197 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Directive, input, signal, contentChild, computed, effect, Component, ContentChild, inject, output, ChangeDetectionStrategy } from '@angular/core';
2
+ import { inject, ViewContainerRef, ElementRef, effect, DestroyRef, Injectable, Directive, input, signal, contentChild, computed, Component, ContentChild, output, ChangeDetectionStrategy } from '@angular/core';
3
3
  import { MatSidenav, MatSidenavContainer, MatDrawerContainer, MatSidenavContent } from '@angular/material/sidenav';
4
4
  import * as i1 from '@angular/material/core';
5
5
  import { MatRippleModule } from '@angular/material/core';
6
6
  import { NgTemplateOutlet } from '@angular/common';
7
+ import { Overlay } from '@angular/cdk/overlay';
8
+ import { TemplatePortal } from '@angular/cdk/portal';
7
9
  import { RouterLink, RouterLinkActive } from '@angular/router';
8
10
 
11
+ /**
12
+ * One per `RailnavComponent` (provided on its `providers`). Owns the single
13
+ * CDK overlay used to render the currently-open `<rail-nav-drawer>`, and the
14
+ * close timer that lets the cursor travel between the trigger item and the
15
+ * drawer without the drawer closing on transit.
16
+ *
17
+ * - Anchors the overlay to the rail's host element (right edge), so the drawer
18
+ * sits next to the collapsed rail regardless of which trigger item opened it.
19
+ * - Auto-closes when the rail enters `expanded()` mode (mutual exclusion).
20
+ * - Mouse enter/leave on the overlay panel itself cancels/arms the close timer,
21
+ * so the drawer stays open while the cursor is over it.
22
+ */
23
+ class RailnavDrawerOrchestrator {
24
+ /** Window after which we close if nothing cancels — covers cursor transit. */
25
+ static { this.LEAVE_DELAY_MS = 300; }
26
+ /** Post-open grace period during which `armClose()` is a no-op. Touch devices
27
+ * dispatch a burst of synthetic mouse events (with `pointerType === 'mouse'`)
28
+ * right after `click` — this window swallows them. */
29
+ static { this.SYNTHETIC_GUARD_MS = 500; }
30
+ constructor() {
31
+ this.overlay = inject(Overlay);
32
+ this.vcr = inject(ViewContainerRef);
33
+ this.rail = inject(RailnavComponent);
34
+ this.railEl = inject((ElementRef));
35
+ /** Timestamp of the last `open()` — used to gate close-on-leave so that
36
+ * synthetic `mouseleave` events dispatched right after a touch tap
37
+ * (with `pointerType === 'mouse'`) don't shut the drawer instantly. */
38
+ this.lastOpenAt = 0;
39
+ // Arrow-functions so the listener references stay stable across add/remove.
40
+ // Touch / pen pointers are ignored — those flows are click-driven.
41
+ this.onOverlayEnter = (event) => {
42
+ if (event.pointerType !== 'mouse')
43
+ return;
44
+ this.clearCloseTimer();
45
+ };
46
+ this.onOverlayLeave = (event) => {
47
+ if (event.pointerType !== 'mouse')
48
+ return;
49
+ this.armClose();
50
+ };
51
+ // Mutual exclusion with the rail's expanded mode: expanding closes any open
52
+ // drawer, and `open()` short-circuits while expanded. The expanded rail is
53
+ // itself the alternative nav surface, no point stacking a drawer on top.
54
+ effect(() => {
55
+ if (this.rail.expanded())
56
+ this.closeNow();
57
+ });
58
+ inject(DestroyRef).onDestroy(() => {
59
+ this.clearCloseTimer();
60
+ this.overlayRef?.dispose();
61
+ });
62
+ }
63
+ /** Opens or re-targets the overlay to the given template. No-op while the
64
+ * rail is expanded. Cancels any pending close timer (e.g. user hovered back
65
+ * from a quick traverse). */
66
+ open(template) {
67
+ if (this.rail.expanded())
68
+ return;
69
+ this.clearCloseTimer();
70
+ this.lastOpenAt = performance.now();
71
+ if (this.currentTemplate === template && this.overlayRef?.hasAttached()) {
72
+ return;
73
+ }
74
+ this.detach();
75
+ this.currentTemplate = template;
76
+ const positionStrategy = this.overlay
77
+ .position()
78
+ .flexibleConnectedTo(this.railEl)
79
+ .withPositions([
80
+ { originX: 'end', originY: 'top', overlayX: 'start', overlayY: 'top' },
81
+ ]);
82
+ this.overlayRef = this.overlay.create({
83
+ positionStrategy,
84
+ hasBackdrop: false,
85
+ panelClass: 'rail-nav-drawer-panel',
86
+ // Reposition on every scroll so the drawer stays glued to the rail
87
+ // when the page scrolls (default `noop` strategy leaves the overlay
88
+ // fixed to the viewport, which visually decouples it from the rail).
89
+ scrollStrategy: this.overlay.scrollStrategies.reposition(),
90
+ });
91
+ // Tap / click outside the overlay closes it — except when the click
92
+ // lands in the rail itself (re-clicking a `[for]` item should re-target
93
+ // or no-op, not close). Crucial on touch where hover-leave doesn't fire.
94
+ this.outsideClickSub = this.overlayRef.outsidePointerEvents().subscribe((event) => {
95
+ const target = event.target;
96
+ if (target && this.railEl.nativeElement.contains(target))
97
+ return;
98
+ this.closeNow();
99
+ });
100
+ // Keep the drawer open while the cursor is over it; close shortly after
101
+ // it leaves (matching the trigger's own leave debounce). pointer* events
102
+ // let us ignore the synthetic mouseleave fired around touch taps.
103
+ this.overlayRef.overlayElement.addEventListener('pointerenter', this.onOverlayEnter);
104
+ this.overlayRef.overlayElement.addEventListener('pointerleave', this.onOverlayLeave);
105
+ // Inline-style the panel: the projected template content lives outside any
106
+ // component tree, so component-encapsulated styles can't reach it without
107
+ // ::ng-deep / Encapsulation.None. Setting styles directly on overlayElement
108
+ // sidesteps the issue entirely. Each value reads a CSS custom property so
109
+ // consumers can override via `--rail-nav-drawer-*` on the rail.
110
+ //
111
+ // The lib applies both the chrome (background / shadow / radius) AND the
112
+ // default nav-list layout (flex column / padding / gap / min-width) so the
113
+ // consumer's template can be just a flat list of children — they stack
114
+ // vertically with proper spacing out of the box.
115
+ const el = this.overlayRef.overlayElement;
116
+ el.style.background = 'var(--rail-nav-drawer-surface, var(--mat-sys-surface))';
117
+ el.style.color = 'var(--rail-nav-drawer-on-surface, var(--mat-sys-on-surface))';
118
+ el.style.boxShadow = 'var(--rail-nav-drawer-shadow, var(--mat-sys-level2))';
119
+ el.style.borderRadius = 'var(--rail-nav-drawer-radius, 0 12px 12px 0)';
120
+ el.style.overflow = 'auto';
121
+ el.style.boxSizing = 'border-box';
122
+ el.style.display = 'flex';
123
+ el.style.flexDirection = 'column';
124
+ el.style.padding = 'var(--rail-nav-drawer-padding, 12px)';
125
+ el.style.gap = 'var(--rail-nav-drawer-gap, 4px)';
126
+ // Fixed width so the drawer doesn't shrink-fit its content (which would
127
+ // make each trigger's drawer a different size) and absorbs the layout
128
+ // variance between MatButton variants (e.g. text vs tonal padding).
129
+ el.style.width = 'var(--rail-nav-drawer-width, 240px)';
130
+ // Subtle fade-in on every (re-)open: applied via Web Animations API so we
131
+ // don't ship CSS keyframes globally. Re-targeting the overlay to another
132
+ // template re-runs this animation, giving a soft cross-fade feel.
133
+ el.animate([
134
+ { opacity: 0, transform: 'translateX(-4px)' },
135
+ { opacity: 1, transform: 'translateX(0)' },
136
+ ], { duration: 180, easing: 'ease-out' });
137
+ this.overlayRef.attach(new TemplatePortal(template, this.vcr));
138
+ // Match the rail's full height so the drawer sits flush against it
139
+ // regardless of viewport / layout. ResizeObserver keeps it in sync.
140
+ this.applyHeight();
141
+ this.resizeObserver?.disconnect();
142
+ this.resizeObserver = new ResizeObserver(() => this.applyHeight());
143
+ this.resizeObserver.observe(this.railEl.nativeElement);
144
+ }
145
+ applyHeight() {
146
+ if (!this.overlayRef)
147
+ return;
148
+ const rect = this.railEl.nativeElement.getBoundingClientRect();
149
+ this.overlayRef.overlayElement.style.height = `${rect.height}px`;
150
+ }
151
+ /** Arms the close timer. Cancelled if the cursor returns to the trigger or
152
+ * enters the overlay before it fires. Suppressed during the post-open guard
153
+ * window so synthetic mouseleave events fired right after a touch tap
154
+ * don't shut the drawer instantly. */
155
+ armClose() {
156
+ if (performance.now() - this.lastOpenAt < RailnavDrawerOrchestrator.SYNTHETIC_GUARD_MS) {
157
+ return;
158
+ }
159
+ this.clearCloseTimer();
160
+ this.closeTimer = setTimeout(() => this.closeNow(), RailnavDrawerOrchestrator.LEAVE_DELAY_MS);
161
+ }
162
+ /** Force-close immediately, e.g. on a click inside the drawer that navigates. */
163
+ closeNow() {
164
+ this.clearCloseTimer();
165
+ this.detach();
166
+ }
167
+ detach() {
168
+ this.resizeObserver?.disconnect();
169
+ this.resizeObserver = undefined;
170
+ this.outsideClickSub?.unsubscribe();
171
+ this.outsideClickSub = undefined;
172
+ if (!this.overlayRef)
173
+ return;
174
+ this.overlayRef.overlayElement.removeEventListener('pointerenter', this.onOverlayEnter);
175
+ this.overlayRef.overlayElement.removeEventListener('pointerleave', this.onOverlayLeave);
176
+ // dispose (not just detach) so the next `open()` builds a fresh overlay
177
+ // with its own position strategy + listeners — no stale state to leak.
178
+ this.overlayRef.dispose();
179
+ this.overlayRef = undefined;
180
+ this.currentTemplate = undefined;
181
+ }
182
+ clearCloseTimer() {
183
+ if (this.closeTimer !== undefined) {
184
+ clearTimeout(this.closeTimer);
185
+ this.closeTimer = undefined;
186
+ }
187
+ }
188
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavDrawerOrchestrator, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
189
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavDrawerOrchestrator }); }
190
+ }
191
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavDrawerOrchestrator, decorators: [{
192
+ type: Injectable
193
+ }], ctorParameters: () => [] });
194
+
9
195
  /** Directive to mark custom branding content */
10
196
  class RailnavBrandingDirective {
11
197
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavBrandingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
@@ -58,7 +244,7 @@ class RailnavComponent extends MatSidenav {
58
244
  this.expanded.set(true);
59
245
  }
60
246
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
61
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: RailnavComponent, isStandalone: true, selector: "rail-nav", inputs: { railPosition: { classPropertyName: "railPosition", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, hideDefaultHeader: { classPropertyName: "hideDefaultHeader", publicName: "hideDefaultHeader", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, autoCollapse: { classPropertyName: "autoCollapse", publicName: "autoCollapse", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.expanded": "expanded()", "class.position-end": "railPosition() === \"end\"" }, classAttribute: "mat-drawer mat-sidenav" }, queries: [{ propertyName: "customBranding", first: true, predicate: RailnavBrandingDirective, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
247
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: RailnavComponent, isStandalone: true, selector: "rail-nav", inputs: { railPosition: { classPropertyName: "railPosition", publicName: "position", isSignal: true, isRequired: false, transformFunction: null }, hideDefaultHeader: { classPropertyName: "hideDefaultHeader", publicName: "hideDefaultHeader", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, subtitle: { classPropertyName: "subtitle", publicName: "subtitle", isSignal: true, isRequired: false, transformFunction: null }, autoCollapse: { classPropertyName: "autoCollapse", publicName: "autoCollapse", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.expanded": "expanded()", "class.position-end": "railPosition() === \"end\"" }, classAttribute: "mat-drawer mat-sidenav" }, providers: [RailnavDrawerOrchestrator], queries: [{ propertyName: "customBranding", first: true, predicate: RailnavBrandingDirective, descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: `
62
248
  <!-- Default branding template (title/subtitle) -->
63
249
  <ng-template #defaultBrandingTpl>
64
250
  <div class="rail-branding" [class.position-end]="railPosition() === 'end'">
@@ -97,7 +283,7 @@ class RailnavComponent extends MatSidenav {
97
283
  <nav class="rail-items">
98
284
  <ng-content />
99
285
  </nav>
100
- `, isInline: true, styles: [":host{display:block;position:absolute;top:0;bottom:0;left:0;z-index:100;width:var(--rail-nav-collapsed-width, 72px);border:none!important;outline:none!important;box-shadow:none;background:var(--rail-nav-surface-color, var(--mat-sys-surface));transition:width .2s ease;overflow:visible}:host(.expanded){width:var(--rail-nav-expanded-width, fit-content);box-shadow:4px 0 8px #0003}:host(.position-end){left:auto;right:0}:host(.position-end.expanded){box-shadow:-4px 0 8px #0003}.rail-header{display:flex;align-items:center;gap:12px;height:var(--rail-nav-header-height, 64px);padding:16px 16px 16px 24px;box-sizing:border-box;cursor:pointer;color:var(--rail-nav-on-surface, var(--mat-sys-on-surface))}.rail-header.position-end{padding:16px 24px 16px 16px}.rail-header.position-end .rail-burger{margin-left:auto}.rail-header:hover{background:var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high))}.rail-items{display:flex;flex-direction:column;padding:12px 12px 4px;gap:0;box-sizing:border-box;width:100%}.rail-burger{position:relative;width:24px;height:24px;flex-shrink:0}.rail-burger.position-end{transform:scaleX(-1)}.rail-burger svg{position:absolute;top:0;left:0;transition:opacity .3s ease,transform .3s ease}.rail-burger .icon-menu{opacity:1;transform:rotate(0)}.rail-burger .icon-menu-open{opacity:0;transform:rotate(-90deg)}.rail-burger.expanded .icon-menu{opacity:0;transform:rotate(90deg)}.rail-burger.expanded .icon-menu-open{opacity:1;transform:rotate(0)}.rail-burger.position-end .icon-menu{transform:rotate(0)}.rail-burger.position-end .icon-menu-open{transform:rotate(90deg)}.rail-burger.position-end.expanded .icon-menu{transform:rotate(-90deg)}.rail-burger.position-end.expanded .icon-menu-open{transform:rotate(0)}.rail-branding{display:flex;flex-direction:column;justify-content:center;min-width:0;overflow:hidden;text-align:right;padding-top:5px}.rail-branding.position-end{text-align:left}.rail-title{font-size:16px;font-weight:500;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--rail-nav-on-surface, var(--mat-sys-on-surface))}.rail-subtitle{font-size:11px;line-height:1;color:var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:.7}\n"], dependencies: [{ kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i1.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
286
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;position:absolute;top:0;bottom:0;left:0;z-index:100;width:var(--rail-nav-collapsed-width, 72px);border:none!important;outline:none!important;box-shadow:none;background:var(--rail-nav-surface-color, var(--mat-sys-surface));transition:width .2s ease;overflow:visible;--rail-nav-separator-shift: 12px}:host(.expanded){width:var(--rail-nav-expanded-width, fit-content);box-shadow:4px 0 8px #0003;--rail-nav-separator-shift: 0}:host(.position-end){left:auto;right:0}:host(.position-end.expanded){box-shadow:-4px 0 8px #0003}.rail-header{display:flex;align-items:center;gap:12px;height:var(--rail-nav-header-height, 64px);padding:16px 16px 16px 24px;box-sizing:border-box;cursor:pointer;color:var(--rail-nav-on-surface, var(--mat-sys-on-surface))}.rail-header.position-end{padding:16px 24px 16px 16px}.rail-header.position-end .rail-burger{margin-left:auto}.rail-header:hover{background:var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high))}.rail-items{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;padding:12px;gap:0;box-sizing:border-box;width:100%}.rail-burger{position:relative;width:24px;height:24px;flex-shrink:0}.rail-burger.position-end{transform:scaleX(-1)}.rail-burger svg{position:absolute;top:0;left:0;transition:opacity .3s ease,transform .3s ease}.rail-burger .icon-menu{opacity:1;transform:rotate(0)}.rail-burger .icon-menu-open{opacity:0;transform:rotate(-90deg)}.rail-burger.expanded .icon-menu{opacity:0;transform:rotate(90deg)}.rail-burger.expanded .icon-menu-open{opacity:1;transform:rotate(0)}.rail-burger.position-end .icon-menu{transform:rotate(0)}.rail-burger.position-end .icon-menu-open{transform:rotate(90deg)}.rail-burger.position-end.expanded .icon-menu{transform:rotate(-90deg)}.rail-burger.position-end.expanded .icon-menu-open{transform:rotate(0)}.rail-branding{display:flex;flex-direction:column;justify-content:center;min-width:0;overflow:hidden;text-align:right;padding-top:5px}.rail-branding.position-end{text-align:left}.rail-title{font-size:16px;font-weight:500;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--rail-nav-on-surface, var(--mat-sys-on-surface))}.rail-subtitle{font-size:11px;line-height:1;color:var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:.7}\n"], dependencies: [{ kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i1.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
101
287
  }
102
288
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavComponent, decorators: [{
103
289
  type: Component,
@@ -144,7 +330,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
144
330
  'class': 'mat-drawer mat-sidenav',
145
331
  '[class.expanded]': 'expanded()',
146
332
  '[class.position-end]': 'railPosition() === "end"'
147
- }, styles: [":host{display:block;position:absolute;top:0;bottom:0;left:0;z-index:100;width:var(--rail-nav-collapsed-width, 72px);border:none!important;outline:none!important;box-shadow:none;background:var(--rail-nav-surface-color, var(--mat-sys-surface));transition:width .2s ease;overflow:visible}:host(.expanded){width:var(--rail-nav-expanded-width, fit-content);box-shadow:4px 0 8px #0003}:host(.position-end){left:auto;right:0}:host(.position-end.expanded){box-shadow:-4px 0 8px #0003}.rail-header{display:flex;align-items:center;gap:12px;height:var(--rail-nav-header-height, 64px);padding:16px 16px 16px 24px;box-sizing:border-box;cursor:pointer;color:var(--rail-nav-on-surface, var(--mat-sys-on-surface))}.rail-header.position-end{padding:16px 24px 16px 16px}.rail-header.position-end .rail-burger{margin-left:auto}.rail-header:hover{background:var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high))}.rail-items{display:flex;flex-direction:column;padding:12px 12px 4px;gap:0;box-sizing:border-box;width:100%}.rail-burger{position:relative;width:24px;height:24px;flex-shrink:0}.rail-burger.position-end{transform:scaleX(-1)}.rail-burger svg{position:absolute;top:0;left:0;transition:opacity .3s ease,transform .3s ease}.rail-burger .icon-menu{opacity:1;transform:rotate(0)}.rail-burger .icon-menu-open{opacity:0;transform:rotate(-90deg)}.rail-burger.expanded .icon-menu{opacity:0;transform:rotate(90deg)}.rail-burger.expanded .icon-menu-open{opacity:1;transform:rotate(0)}.rail-burger.position-end .icon-menu{transform:rotate(0)}.rail-burger.position-end .icon-menu-open{transform:rotate(90deg)}.rail-burger.position-end.expanded .icon-menu{transform:rotate(-90deg)}.rail-burger.position-end.expanded .icon-menu-open{transform:rotate(0)}.rail-branding{display:flex;flex-direction:column;justify-content:center;min-width:0;overflow:hidden;text-align:right;padding-top:5px}.rail-branding.position-end{text-align:left}.rail-title{font-size:16px;font-weight:500;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--rail-nav-on-surface, var(--mat-sys-on-surface))}.rail-subtitle{font-size:11px;line-height:1;color:var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:.7}\n"] }]
333
+ }, providers: [RailnavDrawerOrchestrator], styles: [":host{display:flex;flex-direction:column;position:absolute;top:0;bottom:0;left:0;z-index:100;width:var(--rail-nav-collapsed-width, 72px);border:none!important;outline:none!important;box-shadow:none;background:var(--rail-nav-surface-color, var(--mat-sys-surface));transition:width .2s ease;overflow:visible;--rail-nav-separator-shift: 12px}:host(.expanded){width:var(--rail-nav-expanded-width, fit-content);box-shadow:4px 0 8px #0003;--rail-nav-separator-shift: 0}:host(.position-end){left:auto;right:0}:host(.position-end.expanded){box-shadow:-4px 0 8px #0003}.rail-header{display:flex;align-items:center;gap:12px;height:var(--rail-nav-header-height, 64px);padding:16px 16px 16px 24px;box-sizing:border-box;cursor:pointer;color:var(--rail-nav-on-surface, var(--mat-sys-on-surface))}.rail-header.position-end{padding:16px 24px 16px 16px}.rail-header.position-end .rail-burger{margin-left:auto}.rail-header:hover{background:var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high))}.rail-items{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;padding:12px;gap:0;box-sizing:border-box;width:100%}.rail-burger{position:relative;width:24px;height:24px;flex-shrink:0}.rail-burger.position-end{transform:scaleX(-1)}.rail-burger svg{position:absolute;top:0;left:0;transition:opacity .3s ease,transform .3s ease}.rail-burger .icon-menu{opacity:1;transform:rotate(0)}.rail-burger .icon-menu-open{opacity:0;transform:rotate(-90deg)}.rail-burger.expanded .icon-menu{opacity:0;transform:rotate(90deg)}.rail-burger.expanded .icon-menu-open{opacity:1;transform:rotate(0)}.rail-burger.position-end .icon-menu{transform:rotate(0)}.rail-burger.position-end .icon-menu-open{transform:rotate(90deg)}.rail-burger.position-end.expanded .icon-menu{transform:rotate(-90deg)}.rail-burger.position-end.expanded .icon-menu-open{transform:rotate(0)}.rail-branding{display:flex;flex-direction:column;justify-content:center;min-width:0;overflow:hidden;text-align:right;padding-top:5px}.rail-branding.position-end{text-align:left}.rail-title{font-size:16px;font-weight:500;line-height:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--rail-nav-on-surface, var(--mat-sys-on-surface))}.rail-subtitle{font-size:11px;line-height:1;color:var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;opacity:.7}\n"] }]
148
334
  }], ctorParameters: () => [], propDecorators: { railPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }], hideDefaultHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideDefaultHeader", required: false }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], subtitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "subtitle", required: false }] }], autoCollapse: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoCollapse", required: false }] }], customBranding: [{ type: i0.ContentChild, args: [i0.forwardRef(() => RailnavBrandingDirective), { isSignal: true }] }] } });
149
335
 
150
336
  class RailnavContainerComponent extends MatSidenavContainer {
@@ -217,6 +403,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
217
403
  }], propDecorators: { position: [{ type: i0.Input, args: [{ isSignal: true, alias: "position", required: false }] }] } });
218
404
 
219
405
  class RailnavItemComponent {
406
+ /** Hover-intent delay (ms) before opening the drawer. Cancelled if the
407
+ * cursor leaves the item first. Click bypasses the delay. */
408
+ static { this.ENTER_DELAY_MS = 200; }
220
409
  constructor() {
221
410
  /** Router link for navigation */
222
411
  this.routerLink = input(...(ngDevMode ? [undefined, { debugName: "routerLink" }] : []));
@@ -226,6 +415,17 @@ class RailnavItemComponent {
226
415
  this.badge = input(...(ngDevMode ? [undefined, { debugName: "badge" }] : []));
227
416
  /** Whether this item is active (for non-router usage) */
228
417
  this.active = input(false, ...(ngDevMode ? [{ debugName: "active" }] : []));
418
+ /** Template projected as a contextual side panel when the item is hovered
419
+ * or clicked. Aliased `for` to mirror Material's `mat-datepicker-toggle`
420
+ * (`<rail-nav-item [for]="myTpl">`). Declare the content as a plain
421
+ * `<ng-template #myTpl>...</ng-template>` anywhere in the host template.
422
+ *
423
+ * When set, the item becomes a trigger; the rail's orchestrator handles
424
+ * overlay rendering, animation, hover-intent and close timer. The panel
425
+ * carries the `rail-nav-drawer-panel` class — style it via the theme mixin
426
+ * `rail-nav.panel()` (or your own global rule). Suppressed while the rail
427
+ * is `expanded()`. */
428
+ this.for = input(null, { ...(ngDevMode ? { debugName: "for" } : {}), alias: 'for' });
229
429
  /** Whether to show a badge */
230
430
  this.hasBadge = computed(() => {
231
431
  const b = this.badge();
@@ -240,17 +440,25 @@ class RailnavItemComponent {
240
440
  this.itemClick = output();
241
441
  /** Reference to parent rail-nav */
242
442
  this.railnav = inject(RailnavComponent);
443
+ /** Owned by the parent rail (one orchestrator per rail). null in tests where
444
+ * the item is rendered outside a real rail. */
445
+ this.orchestrator = inject(RailnavDrawerOrchestrator, { optional: true });
243
446
  /** Whether the rail is expanded */
244
447
  this.expanded = computed(() => this.railnav.expanded(), ...(ngDevMode ? [{ debugName: "expanded" }] : []));
245
448
  /** Position of the rail (start or end) */
246
449
  this.position = computed(() => this.railnav.railPosition(), ...(ngDevMode ? [{ debugName: "position" }] : []));
450
+ inject(DestroyRef).onDestroy(() => this.clearEnterTimeout());
247
451
  }
248
- /** Handle item click - emit event and optionally collapse rail */
452
+ /** Handle item click - emit event, optionally collapse rail, then open drawer.
453
+ * Collapse must come BEFORE openDrawer: while the rail is expanded, the
454
+ * orchestrator suppresses opens (mutual exclusion). Collapsing first ensures
455
+ * the click on an expanded rail still opens the drawer. */
249
456
  onItemClick() {
250
457
  this.itemClick.emit();
251
458
  if (this.railnav.autoCollapse()) {
252
459
  this.railnav.collapse();
253
460
  }
461
+ this.openDrawerIfAny();
254
462
  }
255
463
  /** Handle router link click - optionally collapse rail */
256
464
  onRouterLinkClick() {
@@ -258,8 +466,44 @@ class RailnavItemComponent {
258
466
  this.railnav.collapse();
259
467
  }
260
468
  }
469
+ /** Hover-intent: when the item carries a `for` drawer, open it after a
470
+ * short delay. The delay is cancelled if the cursor leaves before it fires.
471
+ * Ignored on non-mouse pointers (touch/pen) — the click path handles them. */
472
+ onHostPointerEnter(event) {
473
+ if (event.pointerType !== 'mouse')
474
+ return;
475
+ if (!this.for() || !this.orchestrator)
476
+ return;
477
+ if (this.railnav.expanded())
478
+ return;
479
+ this.clearEnterTimeout();
480
+ this.enterTimeout = setTimeout(() => this.openDrawerIfAny(), RailnavItemComponent.ENTER_DELAY_MS);
481
+ }
482
+ /** Arm the orchestrator's close timer when leaving the trigger item. If
483
+ * the cursor reaches the overlay before it fires, the overlay's own
484
+ * `pointerenter` cancels it. Ignored on touch/pen for the same reason. */
485
+ onHostPointerLeave(event) {
486
+ if (event.pointerType !== 'mouse')
487
+ return;
488
+ this.clearEnterTimeout();
489
+ if (this.for())
490
+ this.orchestrator?.armClose();
491
+ }
492
+ openDrawerIfAny() {
493
+ const drawer = this.for();
494
+ if (!drawer || !this.orchestrator)
495
+ return;
496
+ this.clearEnterTimeout();
497
+ this.orchestrator.open(drawer);
498
+ }
499
+ clearEnterTimeout() {
500
+ if (this.enterTimeout !== undefined) {
501
+ clearTimeout(this.enterTimeout);
502
+ this.enterTimeout = undefined;
503
+ }
504
+ }
261
505
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
262
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: RailnavItemComponent, isStandalone: true, selector: "rail-nav-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, host: { properties: { "class.expanded": "expanded()", "class.position-end": "position() === \"end\"" } }, ngImport: i0, template: `
506
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: RailnavItemComponent, isStandalone: true, selector: "rail-nav-item", inputs: { routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null }, for: { classPropertyName: "for", publicName: "for", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, host: { listeners: { "pointerenter": "onHostPointerEnter($event)", "pointerleave": "onHostPointerLeave($event)" }, properties: { "class.expanded": "expanded()", "class.position-end": "position() === \"end\"" } }, ngImport: i0, template: `
263
507
  <ng-template #iconTpl>
264
508
  <div class="rail-item-pill">
265
509
  <div class="rail-item-ripple" matRipple></div>
@@ -297,7 +541,7 @@ class RailnavItemComponent {
297
541
  <ng-container [ngTemplateOutlet]="iconTpl" />
298
542
  </button>
299
543
  }
300
- `, isInline: true, styles: [":host{display:block}.rail-item{display:flex;flex-direction:column;align-items:flex-start;justify-content:flex-start;text-decoration:none;color:var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));cursor:pointer;padding:0;gap:4px;background:none;border:none;font:inherit;min-height:56px;box-sizing:border-box;outline:none;width:100%}.rail-item.position-end{align-items:flex-end}.rail-item:focus-visible .rail-item-pill{border-color:var(--rail-nav-primary, var(--mat-sys-primary))}.rail-item.position-end .rail-item-pill{justify-content:flex-end;padding-left:0;padding-right:10px}.rail-item.expanded{gap:0}.rail-item-pill{position:relative;display:flex;align-items:center;justify-content:flex-start;width:48px;height:32px;border-radius:9999px;border:2px solid transparent;overflow:visible;margin-top:12px;padding-left:10px;box-sizing:border-box;transition:background .2s ease,width .2s ease,height .2s ease,margin .2s ease}.rail-item-ripple{position:absolute;inset:0;border-radius:inherit;overflow:hidden}.rail-item:hover .rail-item-pill{background:var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high))}.rail-item.active .rail-item-pill{background:var(--rail-nav-secondary-container, var(--mat-sys-secondary-container));color:var(--rail-nav-on-secondary-container, var(--mat-sys-on-secondary-container))}.rail-item.expanded .rail-item-pill{width:auto;height:48px;padding:0 16px 0 10px;gap:12px;margin-top:0}.rail-item.expanded.position-end .rail-item-pill{flex-direction:row-reverse;justify-content:flex-start;padding:0 10px 0 16px}:host:first-child .rail-item-pill{margin-top:8px}:host:first-child .rail-item.expanded .rail-item-pill{margin-top:0}.rail-item-icon-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:visible}.rail-item-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px}.rail-item-icon ::ng-deep>*{font-size:24px;width:24px;height:24px}.rail-badge{position:absolute;top:-6px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--rail-nav-error, var(--mat-sys-error));color:var(--rail-nav-on-error, var(--mat-sys-on-error));font-size:11px;font-weight:500;line-height:16px;text-align:center;box-sizing:border-box;z-index:10}.rail-badge.dot{top:-2px;right:-2px;min-width:6px;width:6px;height:6px;padding:0;border-radius:3px}.rail-item-label.label-below{font-size:12px;font-weight:500;line-height:16px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:48px;max-height:16px;opacity:1;transition:opacity .1s ease,max-height .2s ease}.rail-item.expanded .rail-item-label.label-below{opacity:0;max-height:0;pointer-events:none}.rail-item-label.label-inline{font-size:14px;font-weight:500;line-height:24px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:0;opacity:0;pointer-events:none}.rail-item.expanded .rail-item-label.label-inline{width:auto;flex:1;opacity:1;pointer-events:auto;transition:opacity .15s ease .1s}.rail-item.expanded.position-end .rail-item-label.label-inline{text-align:right}\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"] }, { kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i1.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
544
+ `, isInline: true, styles: [":host{display:block}.rail-item{display:flex;flex-direction:column;align-items:flex-start;justify-content:flex-start;text-decoration:none;color:var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));cursor:pointer;padding:0;gap:4px;background:none;border:none;font:inherit;min-height:56px;box-sizing:border-box;outline:none;width:100%;transition:gap .2s ease}.rail-item.position-end{align-items:flex-end}.rail-item:focus-visible .rail-item-pill{border-color:var(--rail-nav-primary, var(--mat-sys-primary))}.rail-item.position-end .rail-item-pill{justify-content:flex-end;padding-left:0;padding-right:10px}.rail-item.expanded{gap:0}.rail-item-pill{position:relative;display:flex;align-items:center;justify-content:flex-start;width:48px;height:32px;border-radius:9999px;border:2px solid transparent;overflow:visible;margin-top:12px;padding-left:10px;box-sizing:border-box;transition:background .2s ease,width .2s ease,height .2s ease,margin .2s ease}.rail-item-ripple{position:absolute;inset:0;border-radius:inherit;overflow:hidden}.rail-item:hover .rail-item-pill{background:var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high))}.rail-item.active .rail-item-pill{background:var(--rail-nav-secondary-container, var(--mat-sys-secondary-container));color:var(--rail-nav-on-secondary-container, var(--mat-sys-on-secondary-container))}.rail-item.expanded .rail-item-pill{width:auto;height:48px;padding:0 16px 0 10px;gap:12px;margin-top:0}.rail-item.expanded.position-end .rail-item-pill{flex-direction:row-reverse;justify-content:flex-start;padding:0 10px 0 16px}:host:first-child .rail-item-pill{margin-top:8px}:host:first-child .rail-item.expanded .rail-item-pill{margin-top:0}:host:last-child .rail-item.expanded .rail-item-pill{margin-bottom:12px}.rail-item-icon-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:visible}.rail-item-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px}.rail-item-icon ::ng-deep>*{font-size:24px;width:24px;height:24px}.rail-badge{position:absolute;top:-6px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--rail-nav-error, var(--mat-sys-error));color:var(--rail-nav-on-error, var(--mat-sys-on-error));font-size:11px;font-weight:500;line-height:16px;text-align:center;box-sizing:border-box;z-index:10}.rail-badge.dot{top:-2px;right:-2px;min-width:6px;width:6px;height:6px;padding:0;border-radius:3px}.rail-item-label.label-below{font-size:12px;font-weight:500;line-height:16px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:48px;max-height:16px;opacity:1;transition:opacity .1s ease,max-height .2s ease}.rail-item.expanded .rail-item-label.label-below{opacity:0;max-height:0;pointer-events:none}.rail-item-label.label-inline{font-size:14px;font-weight:500;line-height:24px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:0;opacity:0;pointer-events:none}.rail-item.expanded .rail-item-label.label-inline{width:auto;flex:1;opacity:1;pointer-events:auto;transition:opacity .15s ease .1s}.rail-item.expanded.position-end .rail-item-label.label-inline{text-align:right}\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"] }, { kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i1.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
301
545
  }
302
546
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavItemComponent, decorators: [{
303
547
  type: Component,
@@ -341,9 +585,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
341
585
  }
342
586
  `, changeDetection: ChangeDetectionStrategy.OnPush, host: {
343
587
  '[class.expanded]': 'expanded()',
344
- '[class.position-end]': 'position() === "end"'
345
- }, styles: [":host{display:block}.rail-item{display:flex;flex-direction:column;align-items:flex-start;justify-content:flex-start;text-decoration:none;color:var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));cursor:pointer;padding:0;gap:4px;background:none;border:none;font:inherit;min-height:56px;box-sizing:border-box;outline:none;width:100%}.rail-item.position-end{align-items:flex-end}.rail-item:focus-visible .rail-item-pill{border-color:var(--rail-nav-primary, var(--mat-sys-primary))}.rail-item.position-end .rail-item-pill{justify-content:flex-end;padding-left:0;padding-right:10px}.rail-item.expanded{gap:0}.rail-item-pill{position:relative;display:flex;align-items:center;justify-content:flex-start;width:48px;height:32px;border-radius:9999px;border:2px solid transparent;overflow:visible;margin-top:12px;padding-left:10px;box-sizing:border-box;transition:background .2s ease,width .2s ease,height .2s ease,margin .2s ease}.rail-item-ripple{position:absolute;inset:0;border-radius:inherit;overflow:hidden}.rail-item:hover .rail-item-pill{background:var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high))}.rail-item.active .rail-item-pill{background:var(--rail-nav-secondary-container, var(--mat-sys-secondary-container));color:var(--rail-nav-on-secondary-container, var(--mat-sys-on-secondary-container))}.rail-item.expanded .rail-item-pill{width:auto;height:48px;padding:0 16px 0 10px;gap:12px;margin-top:0}.rail-item.expanded.position-end .rail-item-pill{flex-direction:row-reverse;justify-content:flex-start;padding:0 10px 0 16px}:host:first-child .rail-item-pill{margin-top:8px}:host:first-child .rail-item.expanded .rail-item-pill{margin-top:0}.rail-item-icon-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:visible}.rail-item-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px}.rail-item-icon ::ng-deep>*{font-size:24px;width:24px;height:24px}.rail-badge{position:absolute;top:-6px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--rail-nav-error, var(--mat-sys-error));color:var(--rail-nav-on-error, var(--mat-sys-on-error));font-size:11px;font-weight:500;line-height:16px;text-align:center;box-sizing:border-box;z-index:10}.rail-badge.dot{top:-2px;right:-2px;min-width:6px;width:6px;height:6px;padding:0;border-radius:3px}.rail-item-label.label-below{font-size:12px;font-weight:500;line-height:16px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:48px;max-height:16px;opacity:1;transition:opacity .1s ease,max-height .2s ease}.rail-item.expanded .rail-item-label.label-below{opacity:0;max-height:0;pointer-events:none}.rail-item-label.label-inline{font-size:14px;font-weight:500;line-height:24px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:0;opacity:0;pointer-events:none}.rail-item.expanded .rail-item-label.label-inline{width:auto;flex:1;opacity:1;pointer-events:auto;transition:opacity .15s ease .1s}.rail-item.expanded.position-end .rail-item-label.label-inline{text-align:right}\n"] }]
346
- }], propDecorators: { routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
588
+ '[class.position-end]': 'position() === "end"',
589
+ // pointer* events let us filter by pointerType: touch devices emit
590
+ // synthetic mouseenter/leave around taps, which would arm the close timer
591
+ // right after a click-to-open and shut the drawer instantly. Hover-intent
592
+ // only applies for real mouse pointers.
593
+ '(pointerenter)': 'onHostPointerEnter($event)',
594
+ '(pointerleave)': 'onHostPointerLeave($event)'
595
+ }, styles: [":host{display:block}.rail-item{display:flex;flex-direction:column;align-items:flex-start;justify-content:flex-start;text-decoration:none;color:var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));cursor:pointer;padding:0;gap:4px;background:none;border:none;font:inherit;min-height:56px;box-sizing:border-box;outline:none;width:100%;transition:gap .2s ease}.rail-item.position-end{align-items:flex-end}.rail-item:focus-visible .rail-item-pill{border-color:var(--rail-nav-primary, var(--mat-sys-primary))}.rail-item.position-end .rail-item-pill{justify-content:flex-end;padding-left:0;padding-right:10px}.rail-item.expanded{gap:0}.rail-item-pill{position:relative;display:flex;align-items:center;justify-content:flex-start;width:48px;height:32px;border-radius:9999px;border:2px solid transparent;overflow:visible;margin-top:12px;padding-left:10px;box-sizing:border-box;transition:background .2s ease,width .2s ease,height .2s ease,margin .2s ease}.rail-item-ripple{position:absolute;inset:0;border-radius:inherit;overflow:hidden}.rail-item:hover .rail-item-pill{background:var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high))}.rail-item.active .rail-item-pill{background:var(--rail-nav-secondary-container, var(--mat-sys-secondary-container));color:var(--rail-nav-on-secondary-container, var(--mat-sys-on-secondary-container))}.rail-item.expanded .rail-item-pill{width:auto;height:48px;padding:0 16px 0 10px;gap:12px;margin-top:0}.rail-item.expanded.position-end .rail-item-pill{flex-direction:row-reverse;justify-content:flex-start;padding:0 10px 0 16px}:host:first-child .rail-item-pill{margin-top:8px}:host:first-child .rail-item.expanded .rail-item-pill{margin-top:0}:host:last-child .rail-item.expanded .rail-item-pill{margin-bottom:12px}.rail-item-icon-wrapper{position:relative;display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:visible}.rail-item-icon{display:flex;align-items:center;justify-content:center;width:24px;height:24px}.rail-item-icon ::ng-deep>*{font-size:24px;width:24px;height:24px}.rail-badge{position:absolute;top:-6px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--rail-nav-error, var(--mat-sys-error));color:var(--rail-nav-on-error, var(--mat-sys-on-error));font-size:11px;font-weight:500;line-height:16px;text-align:center;box-sizing:border-box;z-index:10}.rail-badge.dot{top:-2px;right:-2px;min-width:6px;width:6px;height:6px;padding:0;border-radius:3px}.rail-item-label.label-below{font-size:12px;font-weight:500;line-height:16px;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:48px;max-height:16px;opacity:1;transition:opacity .1s ease,max-height .2s ease}.rail-item.expanded .rail-item-label.label-below{opacity:0;max-height:0;pointer-events:none}.rail-item-label.label-inline{font-size:14px;font-weight:500;line-height:24px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:0;opacity:0;pointer-events:none}.rail-item.expanded .rail-item-label.label-inline{width:auto;flex:1;opacity:1;pointer-events:auto;transition:opacity .15s ease .1s}.rail-item.expanded.position-end .rail-item-label.label-inline{text-align:right}\n"] }]
596
+ }], ctorParameters: () => [], propDecorators: { routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], for: [{ type: i0.Input, args: [{ isSignal: true, alias: "for", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
597
+
598
+ /**
599
+ * Visual separator between groups of `<rail-nav-item>`. Renders a thin
600
+ * horizontal rule with sensible margins so groups of items breathe.
601
+ *
602
+ * <rail-nav>
603
+ * <rail-nav-item label="Home">...</rail-nav-item>
604
+ * <rail-nav-item label="Search">...</rail-nav-item>
605
+ * <rail-nav-separator />
606
+ * <rail-nav-item label="Trash">...</rail-nav-item>
607
+ * </rail-nav>
608
+ *
609
+ * Color overridable via `--rail-nav-separator-color`.
610
+ */
611
+ class RailnavSeparatorComponent {
612
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavSeparatorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
613
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: RailnavSeparatorComponent, isStandalone: true, selector: "rail-nav-separator", ngImport: i0, template: '', isInline: true, styles: [":host{display:flex;align-items:center;height:20px;padding:0 12px;margin-top:var(--rail-nav-separator-shift, 0);box-sizing:border-box;transition:margin-top .2s ease}:host:before{content:\"\";flex:1;height:1px;background:var(--rail-nav-separator-color, var(--mat-sys-outline-variant))}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
614
+ }
615
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavSeparatorComponent, decorators: [{
616
+ type: Component,
617
+ args: [{ selector: 'rail-nav-separator', changeDetection: ChangeDetectionStrategy.OnPush, template: '', styles: [":host{display:flex;align-items:center;height:20px;padding:0 12px;margin-top:var(--rail-nav-separator-shift, 0);box-sizing:border-box;transition:margin-top .2s ease}:host:before{content:\"\";flex:1;height:1px;background:var(--rail-nav-separator-color, var(--mat-sys-outline-variant))}\n"] }]
618
+ }] });
619
+
620
+ /**
621
+ * Flexible spacer between rail items. Placed between two groups of items, it
622
+ * pushes everything after it to the bottom of the rail — the classic pattern
623
+ * for keeping primary nav at the top and secondary entries (Settings,
624
+ * Profile…) anchored at the bottom.
625
+ *
626
+ * <rail-nav>
627
+ * <rail-nav-item label="Home">...</rail-nav-item>
628
+ * <rail-nav-item label="Inbox">...</rail-nav-item>
629
+ * <rail-nav-spacer />
630
+ * <rail-nav-item label="Settings">...</rail-nav-item>
631
+ * </rail-nav>
632
+ *
633
+ * Requires the rail to own its full height (already the case by default).
634
+ */
635
+ class RailnavSpacerComponent {
636
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavSpacerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
637
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: RailnavSpacerComponent, isStandalone: true, selector: "rail-nav-spacer", ngImport: i0, template: '', isInline: true, styles: [":host{display:block;flex:1 1 auto}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
638
+ }
639
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RailnavSpacerComponent, decorators: [{
640
+ type: Component,
641
+ args: [{ selector: 'rail-nav-spacer', changeDetection: ChangeDetectionStrategy.OnPush, template: '', styles: [":host{display:block;flex:1 1 auto}\n"] }]
642
+ }] });
347
643
 
348
644
  /*
349
645
  * Public API Surface of @softwarity/rail-nav
@@ -353,5 +649,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
353
649
  * Generated bundle index. Do not edit.
354
650
  */
355
651
 
356
- export { RailnavBrandingDirective, RailnavComponent, RailnavContainerComponent, RailnavContentComponent, RailnavItemComponent };
652
+ export { RailnavBrandingDirective, RailnavComponent, RailnavContainerComponent, RailnavContentComponent, RailnavItemComponent, RailnavSeparatorComponent, RailnavSpacerComponent };
357
653
  //# sourceMappingURL=softwarity-rail-nav.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"softwarity-rail-nav.mjs","sources":["../../src/lib/railnav.component.ts","../../src/lib/railnav-container.component.ts","../../src/lib/railnav-content.component.ts","../../src/lib/railnav-item.component.ts","../../src/public-api.ts","../../src/softwarity-rail-nav.ts"],"sourcesContent":["import { Component, signal, effect, input, contentChild, ElementRef, Directive, computed } from '@angular/core';\nimport { MatSidenav } from '@angular/material/sidenav';\nimport { MatRippleModule } from '@angular/material/core';\nimport { NgTemplateOutlet } from '@angular/common';\n\n/** Directive to mark custom branding content */\n@Directive({\n selector: '[railNavBranding]'\n})\nexport class RailnavBrandingDirective {}\n\n@Component({\n selector: 'rail-nav',\n imports: [MatRippleModule, NgTemplateOutlet],\n template: `\n <!-- Default branding template (title/subtitle) -->\n <ng-template #defaultBrandingTpl>\n <div class=\"rail-branding\" [class.position-end]=\"railPosition() === 'end'\">\n @if (title()) {\n <span class=\"rail-title\">{{ title() }}</span>\n }\n @if (subtitle()) {\n <span class=\"rail-subtitle\">{{ subtitle() }}</span>\n }\n </div>\n </ng-template>\n <!-- Custom branding template (projected content) -->\n <ng-template #customBrandingTpl>\n <div class=\"rail-branding rail-branding-custom\" [class.position-end]=\"railPosition() === 'end'\">\n <ng-content select=\"[railNavBranding]\" />\n </div>\n </ng-template>\n @if (!hideDefaultHeader()) {\n <div class=\"rail-header\" [class.position-end]=\"railPosition() === 'end'\" matRipple (click)=\"toggleExpanded()\">\n @if (expanded() && hasBranding() && railPosition() === 'end') {\n <ng-container [ngTemplateOutlet]=\"customBranding() ? customBrandingTpl : defaultBrandingTpl\" />\n }\n <div class=\"rail-burger\" [class.expanded]=\"expanded()\" [class.position-end]=\"railPosition() === 'end'\">\n <svg class=\"icon-menu\" xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" fill=\"currentColor\">\n <path d=\"M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z\"/>\n </svg>\n <svg class=\"icon-menu-open\" xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" fill=\"currentColor\">\n <path d=\"M120-240v-80h520v80H120Zm664-40L584-480l200-200 56 56-144 144 144 144-56 56ZM120-440v-80h400v80H120Zm0-200v-80h520v80H120Z\"/>\n </svg>\n </div>\n @if (expanded() && hasBranding() && railPosition() === 'start') {\n <ng-container [ngTemplateOutlet]=\"customBranding() ? customBrandingTpl : defaultBrandingTpl\" />\n }\n </div>\n }\n <nav class=\"rail-items\">\n <ng-content />\n </nav>\n `,\n styles: [`\n :host {\n display: block;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n z-index: 100;\n width: var(--rail-nav-collapsed-width, 72px);\n border: none !important;\n outline: none !important;\n box-shadow: none;\n background: var(--rail-nav-surface-color, var(--mat-sys-surface));\n transition: width 0.2s ease;\n overflow: visible;\n }\n\n :host(.expanded) {\n width: var(--rail-nav-expanded-width, fit-content);\n box-shadow: 4px 0 8px rgba(0,0,0,.2);\n }\n\n :host(.position-end) {\n left: auto;\n right: 0;\n }\n\n :host(.position-end.expanded) {\n box-shadow: -4px 0 8px rgba(0,0,0,.2);\n }\n\n .rail-header {\n display: flex;\n align-items: center;\n gap: 12px;\n height: var(--rail-nav-header-height, 64px);\n padding: 16px 16px 16px 24px;\n box-sizing: border-box;\n cursor: pointer;\n color: var(--rail-nav-on-surface, var(--mat-sys-on-surface));\n }\n\n .rail-header.position-end {\n padding: 16px 24px 16px 16px;\n }\n\n .rail-header.position-end .rail-burger {\n margin-left: auto;\n }\n\n .rail-header:hover {\n background: var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high));\n }\n\n .rail-items {\n display: flex;\n flex-direction: column;\n padding: 12px 12px 4px;\n gap: 0;\n box-sizing: border-box;\n width: 100%;\n }\n\n .rail-burger {\n position: relative;\n width: 24px;\n height: 24px;\n flex-shrink: 0;\n }\n\n .rail-burger.position-end {\n transform: scaleX(-1);\n }\n\n .rail-burger svg {\n position: absolute;\n top: 0;\n left: 0;\n transition: opacity 0.3s ease, transform 0.3s ease;\n }\n\n /* Position start (left) - clockwise rotation */\n .rail-burger .icon-menu {\n opacity: 1;\n transform: rotate(0deg);\n }\n\n .rail-burger .icon-menu-open {\n opacity: 0;\n transform: rotate(-90deg);\n }\n\n .rail-burger.expanded .icon-menu {\n opacity: 0;\n transform: rotate(90deg);\n }\n\n .rail-burger.expanded .icon-menu-open {\n opacity: 1;\n transform: rotate(0deg);\n }\n\n /* Position end (right) - counter-clockwise rotation (mirrored) */\n .rail-burger.position-end .icon-menu {\n transform: rotate(0deg);\n }\n\n .rail-burger.position-end .icon-menu-open {\n transform: rotate(90deg);\n }\n\n .rail-burger.position-end.expanded .icon-menu {\n transform: rotate(-90deg);\n }\n\n .rail-burger.position-end.expanded .icon-menu-open {\n transform: rotate(0deg);\n }\n\n .rail-branding {\n display: flex;\n flex-direction: column;\n justify-content: center;\n min-width: 0;\n overflow: hidden;\n text-align: right;\n padding-top: 5px;\n }\n\n .rail-branding.position-end {\n text-align: left;\n }\n\n .rail-title {\n font-size: 16px;\n font-weight: 500;\n line-height: 1;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--rail-nav-on-surface, var(--mat-sys-on-surface));\n }\n\n .rail-subtitle {\n font-size: 11px;\n line-height: 1;\n color: var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n opacity: 0.7;\n }\n `],\n host: {\n 'class': 'mat-drawer mat-sidenav',\n '[class.expanded]': 'expanded()',\n '[class.position-end]': 'railPosition() === \"end\"'\n }\n})\nexport class RailnavComponent extends MatSidenav {\n /** Position: 'start' (left) or 'end' (right) - aliased to avoid conflict with MatSidenav.position */\n readonly railPosition = input<'start' | 'end'>('start', { alias: 'position' });\n\n /** Hide the default header (burger + title/subtitle) */\n readonly hideDefaultHeader = input(false);\n\n /** Title displayed when expanded (ignored if railNavBranding is projected) */\n readonly title = input<string>();\n\n /** Subtitle displayed when expanded (ignored if railNavBranding is projected) */\n readonly subtitle = input<string>();\n\n /** Whether to auto-collapse when an item is clicked */\n readonly autoCollapse = input(true);\n\n /** Whether the rail is expanded to show labels */\n readonly expanded = signal(false);\n\n /** Detect custom branding content projection */\n protected readonly customBranding = contentChild(RailnavBrandingDirective);\n\n /** Whether there's any branding to show (custom or default) */\n protected readonly hasBranding = computed(() =>\n !!this.customBranding() || !!this.title() || !!this.subtitle()\n );\n\n constructor() {\n super();\n // Default settings for rail behavior\n this.opened = true;\n this.disableClose = true;\n this.mode = 'side';\n\n // Sync mode with expanded state\n effect(() => {\n this.mode = this.expanded() ? 'over' : 'side';\n });\n }\n\n /** Toggle between collapsed (rail) and expanded (drawer) */\n toggleExpanded(): void {\n this.expanded.update(e => !e);\n }\n\n /** Collapse the rail */\n collapse(): void {\n this.expanded.set(false);\n }\n\n /** Expand the rail to drawer */\n expand(): void {\n this.expanded.set(true);\n }\n}\n","import { Component, ContentChild, input } from '@angular/core';\nimport { MatSidenavContainer, MatDrawerContainer } from '@angular/material/sidenav';\nimport { RailnavComponent } from './railnav.component';\n\n@Component({\n selector: 'rail-nav-container',\n template: `\n <ng-content />\n @if (showBackdrop()) {\n <div\n class=\"railnav-backdrop\"\n [class.visible]=\"railnav?.expanded()\"\n [class.position-end]=\"railnav?.railPosition() === 'end'\"\n (click)=\"railnav?.collapse()\">\n </div>\n }\n `,\n styles: [`\n :host {\n display: block;\n position: relative;\n height: 100%;\n overflow: hidden;\n }\n\n .railnav-backdrop {\n position: absolute;\n top: 0;\n bottom: 0;\n left: var(--rail-nav-collapsed-width, 72px);\n right: 0;\n background: var(--rail-nav-backdrop-color, rgba(0, 0, 0, 0.4));\n z-index: 99;\n cursor: pointer;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.3s ease-in-out;\n }\n\n .railnav-backdrop.position-end {\n left: 0;\n right: var(--rail-nav-collapsed-width, 72px);\n }\n\n .railnav-backdrop.visible {\n opacity: 1;\n pointer-events: auto;\n }\n `],\n host: {\n 'class': 'mat-drawer-container mat-sidenav-container'\n },\n providers: [\n { provide: MatDrawerContainer, useExisting: RailnavContainerComponent }\n ]\n})\nexport class RailnavContainerComponent extends MatSidenavContainer {\n /** Whether to show backdrop when expanded */\n readonly showBackdrop = input(true);\n\n @ContentChild(RailnavComponent) railnav?: RailnavComponent;\n}\n","import { Component, input, inject, computed } from '@angular/core';\nimport { MatSidenavContent } from '@angular/material/sidenav';\nimport { RailnavContainerComponent } from './railnav-container.component';\n\n@Component({\n selector: 'rail-nav-content',\n template: `<ng-content />`,\n styles: [`\n :host {\n display: block;\n height: 100%;\n overflow: auto;\n margin-left: var(--rail-nav-collapsed-width, 72px);\n }\n\n :host(.position-end) {\n margin-left: 0;\n margin-right: var(--rail-nav-collapsed-width, 72px);\n }\n `],\n host: {\n 'class': 'mat-drawer-content mat-sidenav-content',\n '[class.position-end]': 'effectivePosition() === \"end\"'\n }\n})\nexport class RailnavContentComponent extends MatSidenavContent {\n /** Optional: Position of the rail. If not set, uses the sibling rail-nav's position */\n readonly position = input<'start' | 'end' | undefined>(undefined);\n\n /** Parent container that gives access to sibling rail-nav */\n private container = inject(RailnavContainerComponent, { optional: true });\n\n /** Effective position - from input or from sibling rail-nav */\n protected readonly effectivePosition = computed(() => {\n const inputValue = this.position();\n if (inputValue !== undefined) return inputValue;\n return this.container?.railnav?.railPosition() ?? 'start';\n });\n}\n","import { Component, input, output, computed, inject, ChangeDetectionStrategy } from '@angular/core';\nimport { RouterLink, RouterLinkActive } from '@angular/router';\nimport { MatRippleModule } from '@angular/material/core';\nimport { RailnavComponent } from './railnav.component';\nimport { NgTemplateOutlet } from '@angular/common';\n\n@Component({\n selector: 'rail-nav-item',\n imports: [RouterLink, RouterLinkActive, MatRippleModule, NgTemplateOutlet],\n template: `\n <ng-template #iconTpl>\n <div class=\"rail-item-pill\">\n <div class=\"rail-item-ripple\" matRipple></div>\n <div class=\"rail-item-icon-wrapper\">\n <div class=\"rail-item-icon\">\n <ng-content />\n </div>\n @if (hasBadge()) {\n <span class=\"rail-badge\" [class.dot]=\"isDotBadge()\">{{ isDotBadge() ? '' : badge() }}</span>\n }\n </div>\n <span class=\"rail-item-label label-inline\">{{ label() }}</span>\n </div>\n <span class=\"rail-item-label label-below\">{{ label() }}</span>\n </ng-template>\n @if (routerLink()) {\n <a\n class=\"rail-item\"\n [class.expanded]=\"expanded()\"\n [class.position-end]=\"position() === 'end'\"\n [class.active]=\"active()\"\n [routerLink]=\"routerLink()\"\n routerLinkActive=\"active\"\n (click)=\"onRouterLinkClick()\">\n <ng-container [ngTemplateOutlet]=\"iconTpl\" />\n </a>\n } @else {\n <button\n type=\"button\"\n class=\"rail-item\"\n [class.expanded]=\"expanded()\"\n [class.position-end]=\"position() === 'end'\"\n [class.active]=\"active()\"\n (click)=\"onItemClick()\">\n <ng-container [ngTemplateOutlet]=\"iconTpl\" />\n </button>\n }\n `,\n styles: [`\n :host {\n display: block;\n }\n\n .rail-item {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: flex-start;\n text-decoration: none;\n color: var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));\n cursor: pointer;\n padding: 0;\n gap: 4px;\n background: none;\n border: none;\n font: inherit;\n min-height: 56px;\n box-sizing: border-box;\n outline: none;\n width: 100%;\n }\n\n .rail-item.position-end {\n align-items: flex-end;\n }\n\n .rail-item:focus-visible .rail-item-pill {\n border-color: var(--rail-nav-primary, var(--mat-sys-primary));\n }\n\n .rail-item.position-end .rail-item-pill {\n justify-content: flex-end;\n padding-left: 0;\n padding-right: 10px;\n }\n\n .rail-item.expanded {\n gap: 0;\n }\n\n .rail-item-pill {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n width: 48px;\n height: 32px;\n border-radius: 9999px;\n border: 2px solid transparent;\n overflow: visible;\n margin-top: 12px;\n padding-left: 10px;\n box-sizing: border-box;\n transition: background 0.2s ease, width 0.2s ease, height 0.2s ease, margin 0.2s ease;\n }\n\n .rail-item-ripple {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n overflow: hidden;\n }\n\n .rail-item:hover .rail-item-pill {\n background: var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high));\n }\n\n .rail-item.active .rail-item-pill {\n background: var(--rail-nav-secondary-container, var(--mat-sys-secondary-container));\n color: var(--rail-nav-on-secondary-container, var(--mat-sys-on-secondary-container));\n }\n\n /* Expanded pill includes both icon and label */\n .rail-item.expanded .rail-item-pill {\n width: auto;\n height: 48px;\n padding: 0 16px 0 10px;\n gap: 12px;\n margin-top: 0;\n }\n\n .rail-item.expanded.position-end .rail-item-pill {\n flex-direction: row-reverse;\n justify-content: flex-start;\n padding: 0 10px 0 16px;\n }\n\n /* First item: reduce space after header and keep icon stable during expand */\n /* Collapsed: 8px + 16px (half of 32px) = 24px from top */\n /* Expanded: 0px + 24px (half of 48px) = 24px from top */\n :host:first-child .rail-item-pill {\n margin-top: 8px;\n }\n\n :host:first-child .rail-item.expanded .rail-item-pill {\n margin-top: 0;\n }\n\n .rail-item-icon-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n overflow: visible;\n }\n\n .rail-item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n }\n\n .rail-item-icon ::ng-deep > * {\n font-size: 24px;\n width: 24px;\n height: 24px;\n }\n\n .rail-badge {\n position: absolute;\n top: -6px;\n right: -6px;\n min-width: 16px;\n height: 16px;\n padding: 0 4px;\n border-radius: 8px;\n background: var(--rail-nav-error, var(--mat-sys-error));\n color: var(--rail-nav-on-error, var(--mat-sys-on-error));\n font-size: 11px;\n font-weight: 500;\n line-height: 16px;\n text-align: center;\n box-sizing: border-box;\n z-index: 10;\n }\n\n /* Small dot badge (no text) */\n .rail-badge.dot {\n top: -2px;\n right: -2px;\n min-width: 6px;\n width: 6px;\n height: 6px;\n padding: 0;\n border-radius: 3px;\n }\n\n /* Label below icon (collapsed mode) */\n .rail-item-label.label-below {\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n text-align: center;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n width: 48px;\n max-height: 16px;\n opacity: 1;\n transition: opacity 0.1s ease, max-height 0.2s ease;\n }\n\n .rail-item.expanded .rail-item-label.label-below {\n opacity: 0;\n max-height: 0;\n pointer-events: none;\n }\n\n /* Label inline with icon (expanded mode) */\n .rail-item-label.label-inline {\n font-size: 14px;\n font-weight: 500;\n line-height: 24px;\n text-align: left;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n width: 0;\n opacity: 0;\n pointer-events: none;\n }\n\n .rail-item.expanded .rail-item-label.label-inline {\n width: auto;\n flex: 1;\n opacity: 1;\n pointer-events: auto;\n transition: opacity 0.15s ease 0.1s;\n }\n\n .rail-item.expanded.position-end .rail-item-label.label-inline {\n text-align: right;\n }\n `],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class.expanded]': 'expanded()',\n '[class.position-end]': 'position() === \"end\"'\n }\n})\nexport class RailnavItemComponent {\n /** Router link for navigation */\n readonly routerLink = input<string | any[]>();\n\n /** Label text displayed */\n readonly label = input<string>();\n\n /** Badge value (number, text, or true for dot badge) */\n readonly badge = input<string | number | boolean>();\n\n /** Whether this item is active (for non-router usage) */\n readonly active = input(false);\n\n /** Whether to show a badge */\n protected readonly hasBadge = computed(() => {\n const b = this.badge();\n return b !== undefined && b !== null && b !== false;\n });\n\n /** Whether to show a small dot badge (no text) */\n protected readonly isDotBadge = computed(() => {\n const b = this.badge();\n return b === true || b === '';\n });\n\n /** Click event (for non-router usage) */\n readonly itemClick = output<void>();\n\n /** Reference to parent rail-nav */\n private railnav = inject(RailnavComponent);\n\n /** Whether the rail is expanded */\n protected expanded = computed(() => this.railnav.expanded());\n\n /** Position of the rail (start or end) */\n protected position = computed(() => this.railnav.railPosition());\n\n /** Handle item click - emit event and optionally collapse rail */\n protected onItemClick(): void {\n this.itemClick.emit();\n if (this.railnav.autoCollapse()) {\n this.railnav.collapse();\n }\n }\n\n /** Handle router link click - optionally collapse rail */\n protected onRouterLinkClick(): void {\n if (this.railnav.autoCollapse()) {\n this.railnav.collapse();\n }\n }\n}\n","/*\n * Public API Surface of @softwarity/rail-nav\n */\n\nexport { RailnavComponent, RailnavBrandingDirective } from './lib/railnav.component';\nexport { RailnavContainerComponent } from './lib/railnav-container.component';\nexport { RailnavContentComponent } from './lib/railnav-content.component';\nexport { RailnavItemComponent } from './lib/railnav-item.component';\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":"softwarity-rail-nav.mjs","sources":["../../src/lib/railnav-drawer.orchestrator.ts","../../src/lib/railnav.component.ts","../../src/lib/railnav-container.component.ts","../../src/lib/railnav-content.component.ts","../../src/lib/railnav-item.component.ts","../../src/lib/railnav-separator.component.ts","../../src/lib/railnav-spacer.component.ts","../../src/public-api.ts","../../src/softwarity-rail-nav.ts"],"sourcesContent":["import { Overlay, OverlayRef } from '@angular/cdk/overlay';\nimport { TemplatePortal } from '@angular/cdk/portal';\nimport { DestroyRef, ElementRef, Injectable, TemplateRef, ViewContainerRef, effect, inject } from '@angular/core';\nimport { Subscription } from 'rxjs';\nimport { RailnavComponent } from './railnav.component';\n\n/**\n * One per `RailnavComponent` (provided on its `providers`). Owns the single\n * CDK overlay used to render the currently-open `<rail-nav-drawer>`, and the\n * close timer that lets the cursor travel between the trigger item and the\n * drawer without the drawer closing on transit.\n *\n * - Anchors the overlay to the rail's host element (right edge), so the drawer\n * sits next to the collapsed rail regardless of which trigger item opened it.\n * - Auto-closes when the rail enters `expanded()` mode (mutual exclusion).\n * - Mouse enter/leave on the overlay panel itself cancels/arms the close timer,\n * so the drawer stays open while the cursor is over it.\n */\n@Injectable()\nexport class RailnavDrawerOrchestrator {\n private readonly overlay = inject(Overlay);\n private readonly vcr = inject(ViewContainerRef);\n private readonly rail = inject(RailnavComponent);\n private readonly railEl = inject(ElementRef<HTMLElement>);\n\n /** Window after which we close if nothing cancels — covers cursor transit. */\n private static readonly LEAVE_DELAY_MS = 300;\n /** Post-open grace period during which `armClose()` is a no-op. Touch devices\n * dispatch a burst of synthetic mouse events (with `pointerType === 'mouse'`)\n * right after `click` — this window swallows them. */\n private static readonly SYNTHETIC_GUARD_MS = 500;\n\n private overlayRef?: OverlayRef;\n private currentTemplate?: TemplateRef<unknown>;\n private closeTimer?: ReturnType<typeof setTimeout>;\n private resizeObserver?: ResizeObserver;\n private outsideClickSub?: Subscription;\n /** Timestamp of the last `open()` — used to gate close-on-leave so that\n * synthetic `mouseleave` events dispatched right after a touch tap\n * (with `pointerType === 'mouse'`) don't shut the drawer instantly. */\n private lastOpenAt = 0;\n\n constructor() {\n // Mutual exclusion with the rail's expanded mode: expanding closes any open\n // drawer, and `open()` short-circuits while expanded. The expanded rail is\n // itself the alternative nav surface, no point stacking a drawer on top.\n effect(() => {\n if (this.rail.expanded()) this.closeNow();\n });\n inject(DestroyRef).onDestroy(() => {\n this.clearCloseTimer();\n this.overlayRef?.dispose();\n });\n }\n\n /** Opens or re-targets the overlay to the given template. No-op while the\n * rail is expanded. Cancels any pending close timer (e.g. user hovered back\n * from a quick traverse). */\n open(template: TemplateRef<unknown>): void {\n if (this.rail.expanded()) return;\n this.clearCloseTimer();\n this.lastOpenAt = performance.now();\n if (this.currentTemplate === template && this.overlayRef?.hasAttached()) {\n return;\n }\n this.detach();\n this.currentTemplate = template;\n\n const positionStrategy = this.overlay\n .position()\n .flexibleConnectedTo(this.railEl)\n .withPositions([\n { originX: 'end', originY: 'top', overlayX: 'start', overlayY: 'top' },\n ]);\n\n this.overlayRef = this.overlay.create({\n positionStrategy,\n hasBackdrop: false,\n panelClass: 'rail-nav-drawer-panel',\n // Reposition on every scroll so the drawer stays glued to the rail\n // when the page scrolls (default `noop` strategy leaves the overlay\n // fixed to the viewport, which visually decouples it from the rail).\n scrollStrategy: this.overlay.scrollStrategies.reposition(),\n });\n\n // Tap / click outside the overlay closes it — except when the click\n // lands in the rail itself (re-clicking a `[for]` item should re-target\n // or no-op, not close). Crucial on touch where hover-leave doesn't fire.\n this.outsideClickSub = this.overlayRef.outsidePointerEvents().subscribe((event) => {\n const target = event.target as Node | null;\n if (target && this.railEl.nativeElement.contains(target)) return;\n this.closeNow();\n });\n\n // Keep the drawer open while the cursor is over it; close shortly after\n // it leaves (matching the trigger's own leave debounce). pointer* events\n // let us ignore the synthetic mouseleave fired around touch taps.\n this.overlayRef.overlayElement.addEventListener('pointerenter', this.onOverlayEnter);\n this.overlayRef.overlayElement.addEventListener('pointerleave', this.onOverlayLeave);\n\n // Inline-style the panel: the projected template content lives outside any\n // component tree, so component-encapsulated styles can't reach it without\n // ::ng-deep / Encapsulation.None. Setting styles directly on overlayElement\n // sidesteps the issue entirely. Each value reads a CSS custom property so\n // consumers can override via `--rail-nav-drawer-*` on the rail.\n //\n // The lib applies both the chrome (background / shadow / radius) AND the\n // default nav-list layout (flex column / padding / gap / min-width) so the\n // consumer's template can be just a flat list of children — they stack\n // vertically with proper spacing out of the box.\n const el = this.overlayRef.overlayElement;\n el.style.background = 'var(--rail-nav-drawer-surface, var(--mat-sys-surface))';\n el.style.color = 'var(--rail-nav-drawer-on-surface, var(--mat-sys-on-surface))';\n el.style.boxShadow = 'var(--rail-nav-drawer-shadow, var(--mat-sys-level2))';\n el.style.borderRadius = 'var(--rail-nav-drawer-radius, 0 12px 12px 0)';\n el.style.overflow = 'auto';\n el.style.boxSizing = 'border-box';\n el.style.display = 'flex';\n el.style.flexDirection = 'column';\n el.style.padding = 'var(--rail-nav-drawer-padding, 12px)';\n el.style.gap = 'var(--rail-nav-drawer-gap, 4px)';\n // Fixed width so the drawer doesn't shrink-fit its content (which would\n // make each trigger's drawer a different size) and absorbs the layout\n // variance between MatButton variants (e.g. text vs tonal padding).\n el.style.width = 'var(--rail-nav-drawer-width, 240px)';\n\n // Subtle fade-in on every (re-)open: applied via Web Animations API so we\n // don't ship CSS keyframes globally. Re-targeting the overlay to another\n // template re-runs this animation, giving a soft cross-fade feel.\n el.animate(\n [\n { opacity: 0, transform: 'translateX(-4px)' },\n { opacity: 1, transform: 'translateX(0)' },\n ],\n { duration: 180, easing: 'ease-out' },\n );\n\n this.overlayRef.attach(new TemplatePortal(template, this.vcr));\n\n // Match the rail's full height so the drawer sits flush against it\n // regardless of viewport / layout. ResizeObserver keeps it in sync.\n this.applyHeight();\n this.resizeObserver?.disconnect();\n this.resizeObserver = new ResizeObserver(() => this.applyHeight());\n this.resizeObserver.observe(this.railEl.nativeElement);\n }\n\n private applyHeight(): void {\n if (!this.overlayRef) return;\n const rect = this.railEl.nativeElement.getBoundingClientRect();\n this.overlayRef.overlayElement.style.height = `${rect.height}px`;\n }\n\n /** Arms the close timer. Cancelled if the cursor returns to the trigger or\n * enters the overlay before it fires. Suppressed during the post-open guard\n * window so synthetic mouseleave events fired right after a touch tap\n * don't shut the drawer instantly. */\n armClose(): void {\n if (performance.now() - this.lastOpenAt < RailnavDrawerOrchestrator.SYNTHETIC_GUARD_MS) {\n return;\n }\n this.clearCloseTimer();\n this.closeTimer = setTimeout(\n () => this.closeNow(),\n RailnavDrawerOrchestrator.LEAVE_DELAY_MS,\n );\n }\n\n /** Force-close immediately, e.g. on a click inside the drawer that navigates. */\n closeNow(): void {\n this.clearCloseTimer();\n this.detach();\n }\n\n private detach(): void {\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n this.outsideClickSub?.unsubscribe();\n this.outsideClickSub = undefined;\n if (!this.overlayRef) return;\n this.overlayRef.overlayElement.removeEventListener('pointerenter', this.onOverlayEnter);\n this.overlayRef.overlayElement.removeEventListener('pointerleave', this.onOverlayLeave);\n // dispose (not just detach) so the next `open()` builds a fresh overlay\n // with its own position strategy + listeners — no stale state to leak.\n this.overlayRef.dispose();\n this.overlayRef = undefined;\n this.currentTemplate = undefined;\n }\n\n private clearCloseTimer(): void {\n if (this.closeTimer !== undefined) {\n clearTimeout(this.closeTimer);\n this.closeTimer = undefined;\n }\n }\n\n // Arrow-functions so the listener references stay stable across add/remove.\n // Touch / pen pointers are ignored — those flows are click-driven.\n private readonly onOverlayEnter = (event: PointerEvent): void => {\n if (event.pointerType !== 'mouse') return;\n this.clearCloseTimer();\n };\n private readonly onOverlayLeave = (event: PointerEvent): void => {\n if (event.pointerType !== 'mouse') return;\n this.armClose();\n };\n}\n","import { Component, signal, effect, input, contentChild, ElementRef, Directive, computed } from '@angular/core';\nimport { MatSidenav } from '@angular/material/sidenav';\nimport { MatRippleModule } from '@angular/material/core';\nimport { NgTemplateOutlet } from '@angular/common';\nimport { RailnavDrawerOrchestrator } from './railnav-drawer.orchestrator';\n\n/** Directive to mark custom branding content */\n@Directive({\n selector: '[railNavBranding]'\n})\nexport class RailnavBrandingDirective {}\n\n@Component({\n selector: 'rail-nav',\n imports: [MatRippleModule, NgTemplateOutlet],\n template: `\n <!-- Default branding template (title/subtitle) -->\n <ng-template #defaultBrandingTpl>\n <div class=\"rail-branding\" [class.position-end]=\"railPosition() === 'end'\">\n @if (title()) {\n <span class=\"rail-title\">{{ title() }}</span>\n }\n @if (subtitle()) {\n <span class=\"rail-subtitle\">{{ subtitle() }}</span>\n }\n </div>\n </ng-template>\n <!-- Custom branding template (projected content) -->\n <ng-template #customBrandingTpl>\n <div class=\"rail-branding rail-branding-custom\" [class.position-end]=\"railPosition() === 'end'\">\n <ng-content select=\"[railNavBranding]\" />\n </div>\n </ng-template>\n @if (!hideDefaultHeader()) {\n <div class=\"rail-header\" [class.position-end]=\"railPosition() === 'end'\" matRipple (click)=\"toggleExpanded()\">\n @if (expanded() && hasBranding() && railPosition() === 'end') {\n <ng-container [ngTemplateOutlet]=\"customBranding() ? customBrandingTpl : defaultBrandingTpl\" />\n }\n <div class=\"rail-burger\" [class.expanded]=\"expanded()\" [class.position-end]=\"railPosition() === 'end'\">\n <svg class=\"icon-menu\" xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" fill=\"currentColor\">\n <path d=\"M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z\"/>\n </svg>\n <svg class=\"icon-menu-open\" xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\" width=\"24px\" fill=\"currentColor\">\n <path d=\"M120-240v-80h520v80H120Zm664-40L584-480l200-200 56 56-144 144 144 144-56 56ZM120-440v-80h400v80H120Zm0-200v-80h520v80H120Z\"/>\n </svg>\n </div>\n @if (expanded() && hasBranding() && railPosition() === 'start') {\n <ng-container [ngTemplateOutlet]=\"customBranding() ? customBrandingTpl : defaultBrandingTpl\" />\n }\n </div>\n }\n <nav class=\"rail-items\">\n <ng-content />\n </nav>\n `,\n styles: [`\n :host {\n /* flex column so .rail-items can stretch (flex: 1) and */\n /* rail-nav-spacer inside it can push siblings to the bottom. */\n display: flex;\n flex-direction: column;\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n z-index: 100;\n width: var(--rail-nav-collapsed-width, 72px);\n border: none !important;\n outline: none !important;\n box-shadow: none;\n background: var(--rail-nav-surface-color, var(--mat-sys-surface));\n transition: width 0.2s ease;\n overflow: visible;\n\n /* Collapsed default: rail items show their label-below (~20px) but */\n /* the next pill keeps its margin-top, so the inter-item gap is */\n /* asymmetric. Shift the separator down to recenter it visually. */\n --rail-nav-separator-shift: 12px;\n }\n\n :host(.expanded) {\n width: var(--rail-nav-expanded-width, fit-content);\n box-shadow: 4px 0 8px rgba(0,0,0,.2);\n /* Expanded mode has no label-below, items are flush. No shift needed. */\n --rail-nav-separator-shift: 0;\n }\n\n :host(.position-end) {\n left: auto;\n right: 0;\n }\n\n :host(.position-end.expanded) {\n box-shadow: -4px 0 8px rgba(0,0,0,.2);\n }\n\n .rail-header {\n display: flex;\n align-items: center;\n gap: 12px;\n height: var(--rail-nav-header-height, 64px);\n padding: 16px 16px 16px 24px;\n box-sizing: border-box;\n cursor: pointer;\n color: var(--rail-nav-on-surface, var(--mat-sys-on-surface));\n }\n\n .rail-header.position-end {\n padding: 16px 24px 16px 16px;\n }\n\n .rail-header.position-end .rail-burger {\n margin-left: auto;\n }\n\n .rail-header:hover {\n background: var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high));\n }\n\n .rail-items {\n display: flex;\n flex-direction: column;\n flex: 1 1 auto;\n min-height: 0;\n /* Symmetric vertical padding so the last item (typically Settings */\n /* anchored via <rail-nav-spacer/>) breathes against the rail edge. */\n padding: 12px;\n gap: 0;\n box-sizing: border-box;\n width: 100%;\n }\n\n .rail-burger {\n position: relative;\n width: 24px;\n height: 24px;\n flex-shrink: 0;\n }\n\n .rail-burger.position-end {\n transform: scaleX(-1);\n }\n\n .rail-burger svg {\n position: absolute;\n top: 0;\n left: 0;\n transition: opacity 0.3s ease, transform 0.3s ease;\n }\n\n /* Position start (left) - clockwise rotation */\n .rail-burger .icon-menu {\n opacity: 1;\n transform: rotate(0deg);\n }\n\n .rail-burger .icon-menu-open {\n opacity: 0;\n transform: rotate(-90deg);\n }\n\n .rail-burger.expanded .icon-menu {\n opacity: 0;\n transform: rotate(90deg);\n }\n\n .rail-burger.expanded .icon-menu-open {\n opacity: 1;\n transform: rotate(0deg);\n }\n\n /* Position end (right) - counter-clockwise rotation (mirrored) */\n .rail-burger.position-end .icon-menu {\n transform: rotate(0deg);\n }\n\n .rail-burger.position-end .icon-menu-open {\n transform: rotate(90deg);\n }\n\n .rail-burger.position-end.expanded .icon-menu {\n transform: rotate(-90deg);\n }\n\n .rail-burger.position-end.expanded .icon-menu-open {\n transform: rotate(0deg);\n }\n\n .rail-branding {\n display: flex;\n flex-direction: column;\n justify-content: center;\n min-width: 0;\n overflow: hidden;\n text-align: right;\n padding-top: 5px;\n }\n\n .rail-branding.position-end {\n text-align: left;\n }\n\n .rail-title {\n font-size: 16px;\n font-weight: 500;\n line-height: 1;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--rail-nav-on-surface, var(--mat-sys-on-surface));\n }\n\n .rail-subtitle {\n font-size: 11px;\n line-height: 1;\n color: var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n opacity: 0.7;\n }\n `],\n host: {\n 'class': 'mat-drawer mat-sidenav',\n '[class.expanded]': 'expanded()',\n '[class.position-end]': 'railPosition() === \"end\"'\n },\n providers: [RailnavDrawerOrchestrator]\n})\nexport class RailnavComponent extends MatSidenav {\n /** Position: 'start' (left) or 'end' (right) - aliased to avoid conflict with MatSidenav.position */\n readonly railPosition = input<'start' | 'end'>('start', { alias: 'position' });\n\n /** Hide the default header (burger + title/subtitle) */\n readonly hideDefaultHeader = input(false);\n\n /** Title displayed when expanded (ignored if railNavBranding is projected) */\n readonly title = input<string>();\n\n /** Subtitle displayed when expanded (ignored if railNavBranding is projected) */\n readonly subtitle = input<string>();\n\n /** Whether to auto-collapse when an item is clicked */\n readonly autoCollapse = input(true);\n\n /** Whether the rail is expanded to show labels */\n readonly expanded = signal(false);\n\n /** Detect custom branding content projection */\n protected readonly customBranding = contentChild(RailnavBrandingDirective);\n\n /** Whether there's any branding to show (custom or default) */\n protected readonly hasBranding = computed(() =>\n !!this.customBranding() || !!this.title() || !!this.subtitle()\n );\n\n constructor() {\n super();\n // Default settings for rail behavior\n this.opened = true;\n this.disableClose = true;\n this.mode = 'side';\n\n // Sync mode with expanded state\n effect(() => {\n this.mode = this.expanded() ? 'over' : 'side';\n });\n }\n\n /** Toggle between collapsed (rail) and expanded (drawer) */\n toggleExpanded(): void {\n this.expanded.update(e => !e);\n }\n\n /** Collapse the rail */\n collapse(): void {\n this.expanded.set(false);\n }\n\n /** Expand the rail to drawer */\n expand(): void {\n this.expanded.set(true);\n }\n}\n","import { Component, ContentChild, input } from '@angular/core';\nimport { MatSidenavContainer, MatDrawerContainer } from '@angular/material/sidenav';\nimport { RailnavComponent } from './railnav.component';\n\n@Component({\n selector: 'rail-nav-container',\n template: `\n <ng-content />\n @if (showBackdrop()) {\n <div\n class=\"railnav-backdrop\"\n [class.visible]=\"railnav?.expanded()\"\n [class.position-end]=\"railnav?.railPosition() === 'end'\"\n (click)=\"railnav?.collapse()\">\n </div>\n }\n `,\n styles: [`\n :host {\n display: block;\n position: relative;\n height: 100%;\n overflow: hidden;\n }\n\n .railnav-backdrop {\n position: absolute;\n top: 0;\n bottom: 0;\n left: var(--rail-nav-collapsed-width, 72px);\n right: 0;\n background: var(--rail-nav-backdrop-color, rgba(0, 0, 0, 0.4));\n z-index: 99;\n cursor: pointer;\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.3s ease-in-out;\n }\n\n .railnav-backdrop.position-end {\n left: 0;\n right: var(--rail-nav-collapsed-width, 72px);\n }\n\n .railnav-backdrop.visible {\n opacity: 1;\n pointer-events: auto;\n }\n `],\n host: {\n 'class': 'mat-drawer-container mat-sidenav-container'\n },\n providers: [\n { provide: MatDrawerContainer, useExisting: RailnavContainerComponent }\n ]\n})\nexport class RailnavContainerComponent extends MatSidenavContainer {\n /** Whether to show backdrop when expanded */\n readonly showBackdrop = input(true);\n\n @ContentChild(RailnavComponent) railnav?: RailnavComponent;\n}\n","import { Component, input, inject, computed } from '@angular/core';\nimport { MatSidenavContent } from '@angular/material/sidenav';\nimport { RailnavContainerComponent } from './railnav-container.component';\n\n@Component({\n selector: 'rail-nav-content',\n template: `<ng-content />`,\n styles: [`\n :host {\n display: block;\n height: 100%;\n overflow: auto;\n margin-left: var(--rail-nav-collapsed-width, 72px);\n }\n\n :host(.position-end) {\n margin-left: 0;\n margin-right: var(--rail-nav-collapsed-width, 72px);\n }\n `],\n host: {\n 'class': 'mat-drawer-content mat-sidenav-content',\n '[class.position-end]': 'effectivePosition() === \"end\"'\n }\n})\nexport class RailnavContentComponent extends MatSidenavContent {\n /** Optional: Position of the rail. If not set, uses the sibling rail-nav's position */\n readonly position = input<'start' | 'end' | undefined>(undefined);\n\n /** Parent container that gives access to sibling rail-nav */\n private container = inject(RailnavContainerComponent, { optional: true });\n\n /** Effective position - from input or from sibling rail-nav */\n protected readonly effectivePosition = computed(() => {\n const inputValue = this.position();\n if (inputValue !== undefined) return inputValue;\n return this.container?.railnav?.railPosition() ?? 'start';\n });\n}\n","import { Component, DestroyRef, TemplateRef, input, output, computed, inject, ChangeDetectionStrategy } from '@angular/core';\nimport { RouterLink, RouterLinkActive } from '@angular/router';\nimport { MatRippleModule } from '@angular/material/core';\nimport { RailnavComponent } from './railnav.component';\nimport { RailnavDrawerOrchestrator } from './railnav-drawer.orchestrator';\nimport { NgTemplateOutlet } from '@angular/common';\n\n@Component({\n selector: 'rail-nav-item',\n imports: [RouterLink, RouterLinkActive, MatRippleModule, NgTemplateOutlet],\n template: `\n <ng-template #iconTpl>\n <div class=\"rail-item-pill\">\n <div class=\"rail-item-ripple\" matRipple></div>\n <div class=\"rail-item-icon-wrapper\">\n <div class=\"rail-item-icon\">\n <ng-content />\n </div>\n @if (hasBadge()) {\n <span class=\"rail-badge\" [class.dot]=\"isDotBadge()\">{{ isDotBadge() ? '' : badge() }}</span>\n }\n </div>\n <span class=\"rail-item-label label-inline\">{{ label() }}</span>\n </div>\n <span class=\"rail-item-label label-below\">{{ label() }}</span>\n </ng-template>\n @if (routerLink()) {\n <a\n class=\"rail-item\"\n [class.expanded]=\"expanded()\"\n [class.position-end]=\"position() === 'end'\"\n [class.active]=\"active()\"\n [routerLink]=\"routerLink()\"\n routerLinkActive=\"active\"\n (click)=\"onRouterLinkClick()\">\n <ng-container [ngTemplateOutlet]=\"iconTpl\" />\n </a>\n } @else {\n <button\n type=\"button\"\n class=\"rail-item\"\n [class.expanded]=\"expanded()\"\n [class.position-end]=\"position() === 'end'\"\n [class.active]=\"active()\"\n (click)=\"onItemClick()\">\n <ng-container [ngTemplateOutlet]=\"iconTpl\" />\n </button>\n }\n `,\n styles: [`\n :host {\n display: block;\n }\n\n .rail-item {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: flex-start;\n text-decoration: none;\n color: var(--rail-nav-on-surface-variant, var(--mat-sys-on-surface-variant));\n cursor: pointer;\n padding: 0;\n gap: 4px;\n background: none;\n border: none;\n font: inherit;\n min-height: 56px;\n box-sizing: border-box;\n outline: none;\n width: 100%;\n /* Transition gap so it animates in sync with the pill (height, margin) */\n /* and the label (max-height) — otherwise gap snaps instantly to 0 and */\n /* siblings jolt during the expand/collapse animation. */\n transition: gap 0.2s ease;\n }\n\n .rail-item.position-end {\n align-items: flex-end;\n }\n\n .rail-item:focus-visible .rail-item-pill {\n border-color: var(--rail-nav-primary, var(--mat-sys-primary));\n }\n\n .rail-item.position-end .rail-item-pill {\n justify-content: flex-end;\n padding-left: 0;\n padding-right: 10px;\n }\n\n .rail-item.expanded {\n gap: 0;\n }\n\n .rail-item-pill {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n width: 48px;\n height: 32px;\n border-radius: 9999px;\n border: 2px solid transparent;\n overflow: visible;\n margin-top: 12px;\n padding-left: 10px;\n box-sizing: border-box;\n transition: background 0.2s ease, width 0.2s ease, height 0.2s ease, margin 0.2s ease;\n }\n\n .rail-item-ripple {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n overflow: hidden;\n }\n\n .rail-item:hover .rail-item-pill {\n background: var(--rail-nav-surface-container-high, var(--mat-sys-surface-container-high));\n }\n\n .rail-item.active .rail-item-pill {\n background: var(--rail-nav-secondary-container, var(--mat-sys-secondary-container));\n color: var(--rail-nav-on-secondary-container, var(--mat-sys-on-secondary-container));\n }\n\n /* Expanded pill includes both icon and label */\n .rail-item.expanded .rail-item-pill {\n width: auto;\n height: 48px;\n padding: 0 16px 0 10px;\n gap: 12px;\n margin-top: 0;\n }\n\n .rail-item.expanded.position-end .rail-item-pill {\n flex-direction: row-reverse;\n justify-content: flex-start;\n padding: 0 10px 0 16px;\n }\n\n /* First item: reduce space after header and keep icon stable during expand */\n /* Collapsed: 8px + 16px (half of 32px) = 24px from top */\n /* Expanded: 0px + 24px (half of 48px) = 24px from top */\n :host:first-child .rail-item-pill {\n margin-top: 8px;\n }\n\n :host:first-child .rail-item.expanded .rail-item-pill {\n margin-top: 0;\n }\n\n /* Last item (typically anchored at the bottom via <rail-nav-spacer/>): */\n /* in expanded mode the pill loses both its margin-top AND the label-below */\n /* below it, so the icon center jumps up 12px relative to the rail bottom. */\n /* Mirror of the first-child fix: add a matching margin-bottom on the pill */\n /* in expanded mode to keep the icon visually anchored. */\n :host:last-child .rail-item.expanded .rail-item-pill {\n margin-bottom: 12px;\n }\n\n .rail-item-icon-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n overflow: visible;\n }\n\n .rail-item-icon {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n }\n\n .rail-item-icon ::ng-deep > * {\n font-size: 24px;\n width: 24px;\n height: 24px;\n }\n\n .rail-badge {\n position: absolute;\n top: -6px;\n right: -6px;\n min-width: 16px;\n height: 16px;\n padding: 0 4px;\n border-radius: 8px;\n background: var(--rail-nav-error, var(--mat-sys-error));\n color: var(--rail-nav-on-error, var(--mat-sys-on-error));\n font-size: 11px;\n font-weight: 500;\n line-height: 16px;\n text-align: center;\n box-sizing: border-box;\n z-index: 10;\n }\n\n /* Small dot badge (no text) */\n .rail-badge.dot {\n top: -2px;\n right: -2px;\n min-width: 6px;\n width: 6px;\n height: 6px;\n padding: 0;\n border-radius: 3px;\n }\n\n /* Label below icon (collapsed mode) */\n .rail-item-label.label-below {\n font-size: 12px;\n font-weight: 500;\n line-height: 16px;\n text-align: center;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n width: 48px;\n max-height: 16px;\n opacity: 1;\n transition: opacity 0.1s ease, max-height 0.2s ease;\n }\n\n .rail-item.expanded .rail-item-label.label-below {\n opacity: 0;\n max-height: 0;\n pointer-events: none;\n }\n\n /* Label inline with icon (expanded mode) */\n .rail-item-label.label-inline {\n font-size: 14px;\n font-weight: 500;\n line-height: 24px;\n text-align: left;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n width: 0;\n opacity: 0;\n pointer-events: none;\n }\n\n .rail-item.expanded .rail-item-label.label-inline {\n width: auto;\n flex: 1;\n opacity: 1;\n pointer-events: auto;\n transition: opacity 0.15s ease 0.1s;\n }\n\n .rail-item.expanded.position-end .rail-item-label.label-inline {\n text-align: right;\n }\n `],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n '[class.expanded]': 'expanded()',\n '[class.position-end]': 'position() === \"end\"',\n // pointer* events let us filter by pointerType: touch devices emit\n // synthetic mouseenter/leave around taps, which would arm the close timer\n // right after a click-to-open and shut the drawer instantly. Hover-intent\n // only applies for real mouse pointers.\n '(pointerenter)': 'onHostPointerEnter($event)',\n '(pointerleave)': 'onHostPointerLeave($event)'\n }\n})\nexport class RailnavItemComponent {\n /** Router link for navigation */\n readonly routerLink = input<string | any[]>();\n\n /** Label text displayed */\n readonly label = input<string>();\n\n /** Badge value (number, text, or true for dot badge) */\n readonly badge = input<string | number | boolean>();\n\n /** Whether this item is active (for non-router usage) */\n readonly active = input(false);\n\n /** Template projected as a contextual side panel when the item is hovered\n * or clicked. Aliased `for` to mirror Material's `mat-datepicker-toggle`\n * (`<rail-nav-item [for]=\"myTpl\">`). Declare the content as a plain\n * `<ng-template #myTpl>...</ng-template>` anywhere in the host template.\n *\n * When set, the item becomes a trigger; the rail's orchestrator handles\n * overlay rendering, animation, hover-intent and close timer. The panel\n * carries the `rail-nav-drawer-panel` class — style it via the theme mixin\n * `rail-nav.panel()` (or your own global rule). Suppressed while the rail\n * is `expanded()`. */\n readonly for = input<TemplateRef<unknown> | null>(null, { alias: 'for' });\n\n /** Hover-intent delay (ms) before opening the drawer. Cancelled if the\n * cursor leaves the item first. Click bypasses the delay. */\n private static readonly ENTER_DELAY_MS = 200;\n\n /** Whether to show a badge */\n protected readonly hasBadge = computed(() => {\n const b = this.badge();\n return b !== undefined && b !== null && b !== false;\n });\n\n /** Whether to show a small dot badge (no text) */\n protected readonly isDotBadge = computed(() => {\n const b = this.badge();\n return b === true || b === '';\n });\n\n /** Click event (for non-router usage) */\n readonly itemClick = output<void>();\n\n /** Reference to parent rail-nav */\n private railnav = inject(RailnavComponent);\n\n /** Owned by the parent rail (one orchestrator per rail). null in tests where\n * the item is rendered outside a real rail. */\n private orchestrator = inject(RailnavDrawerOrchestrator, { optional: true });\n\n /** Pending hover-intent timer, cleared on leave or destroy. */\n private enterTimeout?: ReturnType<typeof setTimeout>;\n\n constructor() {\n inject(DestroyRef).onDestroy(() => this.clearEnterTimeout());\n }\n\n /** Whether the rail is expanded */\n protected expanded = computed(() => this.railnav.expanded());\n\n /** Position of the rail (start or end) */\n protected position = computed(() => this.railnav.railPosition());\n\n /** Handle item click - emit event, optionally collapse rail, then open drawer.\n * Collapse must come BEFORE openDrawer: while the rail is expanded, the\n * orchestrator suppresses opens (mutual exclusion). Collapsing first ensures\n * the click on an expanded rail still opens the drawer. */\n protected onItemClick(): void {\n this.itemClick.emit();\n if (this.railnav.autoCollapse()) {\n this.railnav.collapse();\n }\n this.openDrawerIfAny();\n }\n\n /** Handle router link click - optionally collapse rail */\n protected onRouterLinkClick(): void {\n if (this.railnav.autoCollapse()) {\n this.railnav.collapse();\n }\n }\n\n /** Hover-intent: when the item carries a `for` drawer, open it after a\n * short delay. The delay is cancelled if the cursor leaves before it fires.\n * Ignored on non-mouse pointers (touch/pen) — the click path handles them. */\n protected onHostPointerEnter(event: PointerEvent): void {\n if (event.pointerType !== 'mouse') return;\n if (!this.for() || !this.orchestrator) return;\n if (this.railnav.expanded()) return;\n this.clearEnterTimeout();\n this.enterTimeout = setTimeout(\n () => this.openDrawerIfAny(),\n RailnavItemComponent.ENTER_DELAY_MS,\n );\n }\n\n /** Arm the orchestrator's close timer when leaving the trigger item. If\n * the cursor reaches the overlay before it fires, the overlay's own\n * `pointerenter` cancels it. Ignored on touch/pen for the same reason. */\n protected onHostPointerLeave(event: PointerEvent): void {\n if (event.pointerType !== 'mouse') return;\n this.clearEnterTimeout();\n if (this.for()) this.orchestrator?.armClose();\n }\n\n private openDrawerIfAny(): void {\n const drawer = this.for();\n if (!drawer || !this.orchestrator) return;\n this.clearEnterTimeout();\n this.orchestrator.open(drawer);\n }\n\n private clearEnterTimeout(): void {\n if (this.enterTimeout !== undefined) {\n clearTimeout(this.enterTimeout);\n this.enterTimeout = undefined;\n }\n }\n}\n","import { ChangeDetectionStrategy, Component } from '@angular/core';\n\n/**\n * Visual separator between groups of `<rail-nav-item>`. Renders a thin\n * horizontal rule with sensible margins so groups of items breathe.\n *\n * <rail-nav>\n * <rail-nav-item label=\"Home\">...</rail-nav-item>\n * <rail-nav-item label=\"Search\">...</rail-nav-item>\n * <rail-nav-separator />\n * <rail-nav-item label=\"Trash\">...</rail-nav-item>\n * </rail-nav>\n *\n * Color overridable via `--rail-nav-separator-color`.\n */\n@Component({\n selector: 'rail-nav-separator',\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: '',\n styles: [`\n /* Fixed-height host with the 1px line centered via flex. */\n /* The parent rail sets --rail-nav-separator-shift to recenter the */\n /* line visually between items (label-below in collapsed mode makes */\n /* the inter-item gap asymmetric without this). */\n :host {\n display: flex;\n align-items: center;\n height: 20px;\n padding: 0 12px;\n margin-top: var(--rail-nav-separator-shift, 0);\n box-sizing: border-box;\n transition: margin-top 0.2s ease;\n }\n :host::before {\n content: '';\n flex: 1;\n height: 1px;\n background: var(--rail-nav-separator-color, var(--mat-sys-outline-variant));\n }\n `],\n})\nexport class RailnavSeparatorComponent {}\n","import { ChangeDetectionStrategy, Component } from '@angular/core';\n\n/**\n * Flexible spacer between rail items. Placed between two groups of items, it\n * pushes everything after it to the bottom of the rail — the classic pattern\n * for keeping primary nav at the top and secondary entries (Settings,\n * Profile…) anchored at the bottom.\n *\n * <rail-nav>\n * <rail-nav-item label=\"Home\">...</rail-nav-item>\n * <rail-nav-item label=\"Inbox\">...</rail-nav-item>\n * <rail-nav-spacer />\n * <rail-nav-item label=\"Settings\">...</rail-nav-item>\n * </rail-nav>\n *\n * Requires the rail to own its full height (already the case by default).\n */\n@Component({\n selector: 'rail-nav-spacer',\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: '',\n styles: [`\n :host {\n display: block;\n flex: 1 1 auto;\n }\n `],\n})\nexport class RailnavSpacerComponent {}\n","/*\n * Public API Surface of @softwarity/rail-nav\n */\n\nexport { RailnavComponent, RailnavBrandingDirective } from './lib/railnav.component';\nexport { RailnavContainerComponent } from './lib/railnav-container.component';\nexport { RailnavContentComponent } from './lib/railnav-content.component';\nexport { RailnavItemComponent } from './lib/railnav-item.component';\nexport { RailnavSeparatorComponent } from './lib/railnav-separator.component';\nexport { RailnavSpacerComponent } from './lib/railnav-spacer.component';\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": "@softwarity/rail-nav",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "author": "Softwarity",
5
5
  "license": "MIT",
6
6
  "description": "Angular Material Navigation Rail component - Material Design 3 compliant",
@@ -1,4 +1,5 @@
1
1
  import * as _angular_core from '@angular/core';
2
+ import { TemplateRef } from '@angular/core';
2
3
  import { MatSidenav, MatSidenavContainer, MatSidenavContent } from '@angular/material/sidenav';
3
4
 
4
5
  /** Directive to mark custom branding content */
@@ -62,6 +63,20 @@ declare class RailnavItemComponent {
62
63
  readonly badge: _angular_core.InputSignal<string | number | boolean | undefined>;
63
64
  /** Whether this item is active (for non-router usage) */
64
65
  readonly active: _angular_core.InputSignal<boolean>;
66
+ /** Template projected as a contextual side panel when the item is hovered
67
+ * or clicked. Aliased `for` to mirror Material's `mat-datepicker-toggle`
68
+ * (`<rail-nav-item [for]="myTpl">`). Declare the content as a plain
69
+ * `<ng-template #myTpl>...</ng-template>` anywhere in the host template.
70
+ *
71
+ * When set, the item becomes a trigger; the rail's orchestrator handles
72
+ * overlay rendering, animation, hover-intent and close timer. The panel
73
+ * carries the `rail-nav-drawer-panel` class — style it via the theme mixin
74
+ * `rail-nav.panel()` (or your own global rule). Suppressed while the rail
75
+ * is `expanded()`. */
76
+ readonly for: _angular_core.InputSignal<TemplateRef<unknown> | null>;
77
+ /** Hover-intent delay (ms) before opening the drawer. Cancelled if the
78
+ * cursor leaves the item first. Click bypasses the delay. */
79
+ private static readonly ENTER_DELAY_MS;
65
80
  /** Whether to show a badge */
66
81
  protected readonly hasBadge: _angular_core.Signal<boolean>;
67
82
  /** Whether to show a small dot badge (no text) */
@@ -70,16 +85,73 @@ declare class RailnavItemComponent {
70
85
  readonly itemClick: _angular_core.OutputEmitterRef<void>;
71
86
  /** Reference to parent rail-nav */
72
87
  private railnav;
88
+ /** Owned by the parent rail (one orchestrator per rail). null in tests where
89
+ * the item is rendered outside a real rail. */
90
+ private orchestrator;
91
+ /** Pending hover-intent timer, cleared on leave or destroy. */
92
+ private enterTimeout?;
93
+ constructor();
73
94
  /** Whether the rail is expanded */
74
95
  protected expanded: _angular_core.Signal<boolean>;
75
96
  /** Position of the rail (start or end) */
76
97
  protected position: _angular_core.Signal<"start" | "end">;
77
- /** Handle item click - emit event and optionally collapse rail */
98
+ /** Handle item click - emit event, optionally collapse rail, then open drawer.
99
+ * Collapse must come BEFORE openDrawer: while the rail is expanded, the
100
+ * orchestrator suppresses opens (mutual exclusion). Collapsing first ensures
101
+ * the click on an expanded rail still opens the drawer. */
78
102
  protected onItemClick(): void;
79
103
  /** Handle router link click - optionally collapse rail */
80
104
  protected onRouterLinkClick(): void;
105
+ /** Hover-intent: when the item carries a `for` drawer, open it after a
106
+ * short delay. The delay is cancelled if the cursor leaves before it fires.
107
+ * Ignored on non-mouse pointers (touch/pen) — the click path handles them. */
108
+ protected onHostPointerEnter(event: PointerEvent): void;
109
+ /** Arm the orchestrator's close timer when leaving the trigger item. If
110
+ * the cursor reaches the overlay before it fires, the overlay's own
111
+ * `pointerenter` cancels it. Ignored on touch/pen for the same reason. */
112
+ protected onHostPointerLeave(event: PointerEvent): void;
113
+ private openDrawerIfAny;
114
+ private clearEnterTimeout;
81
115
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<RailnavItemComponent, never>;
82
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<RailnavItemComponent, "rail-nav-item", never, { "routerLink": { "alias": "routerLink"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "badge": { "alias": "badge"; "required": false; "isSignal": true; }; "active": { "alias": "active"; "required": false; "isSignal": true; }; }, { "itemClick": "itemClick"; }, never, ["*"], true, never>;
116
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<RailnavItemComponent, "rail-nav-item", never, { "routerLink": { "alias": "routerLink"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "badge": { "alias": "badge"; "required": false; "isSignal": true; }; "active": { "alias": "active"; "required": false; "isSignal": true; }; "for": { "alias": "for"; "required": false; "isSignal": true; }; }, { "itemClick": "itemClick"; }, never, ["*"], true, never>;
117
+ }
118
+
119
+ /**
120
+ * Visual separator between groups of `<rail-nav-item>`. Renders a thin
121
+ * horizontal rule with sensible margins so groups of items breathe.
122
+ *
123
+ * <rail-nav>
124
+ * <rail-nav-item label="Home">...</rail-nav-item>
125
+ * <rail-nav-item label="Search">...</rail-nav-item>
126
+ * <rail-nav-separator />
127
+ * <rail-nav-item label="Trash">...</rail-nav-item>
128
+ * </rail-nav>
129
+ *
130
+ * Color overridable via `--rail-nav-separator-color`.
131
+ */
132
+ declare class RailnavSeparatorComponent {
133
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<RailnavSeparatorComponent, never>;
134
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<RailnavSeparatorComponent, "rail-nav-separator", never, {}, {}, never, never, true, never>;
135
+ }
136
+
137
+ /**
138
+ * Flexible spacer between rail items. Placed between two groups of items, it
139
+ * pushes everything after it to the bottom of the rail — the classic pattern
140
+ * for keeping primary nav at the top and secondary entries (Settings,
141
+ * Profile…) anchored at the bottom.
142
+ *
143
+ * <rail-nav>
144
+ * <rail-nav-item label="Home">...</rail-nav-item>
145
+ * <rail-nav-item label="Inbox">...</rail-nav-item>
146
+ * <rail-nav-spacer />
147
+ * <rail-nav-item label="Settings">...</rail-nav-item>
148
+ * </rail-nav>
149
+ *
150
+ * Requires the rail to own its full height (already the case by default).
151
+ */
152
+ declare class RailnavSpacerComponent {
153
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<RailnavSpacerComponent, never>;
154
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<RailnavSpacerComponent, "rail-nav-spacer", never, {}, {}, never, never, true, never>;
83
155
  }
84
156
 
85
- export { RailnavBrandingDirective, RailnavComponent, RailnavContainerComponent, RailnavContentComponent, RailnavItemComponent };
157
+ export { RailnavBrandingDirective, RailnavComponent, RailnavContainerComponent, RailnavContentComponent, RailnavItemComponent, RailnavSeparatorComponent, RailnavSpacerComponent };