@cocoar/ui-menu 0.1.0-beta.70

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.
@@ -0,0 +1,1054 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, input, Directive, ChangeDetectionStrategy, Component, output, HostListener, TemplateRef, DestroyRef, ChangeDetectorRef, Injector, ViewChild, ContentChild, booleanAttribute, signal, computed, effect } from '@angular/core';
3
+ import { COAR_OVERLAY_REF, COAR_MENU_PARENT, CoarOverlayService, Overlay, coarHoverMenuPreset } from '@cocoar/ui-overlay';
4
+ import { CoarIconComponent } from '@cocoar/ui-components';
5
+ import * as i1 from '@angular/common';
6
+ import { CommonModule } from '@angular/common';
7
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
+
9
+ function shouldDelaySubmenuSwitch(previous, current, submenuRect, direction, sampleMaxAgeMs = 200) {
10
+ if (!previous)
11
+ return false;
12
+ // If submenu rect is not measurable, don't delay.
13
+ if (!Number.isFinite(submenuRect.left) || submenuRect.width <= 0 || submenuRect.height <= 0) {
14
+ return false;
15
+ }
16
+ // If the last movement was long ago, don't apply aim heuristics.
17
+ if (current.t - previous.t > sampleMaxAgeMs)
18
+ return false;
19
+ const dx = current.x - previous.x;
20
+ // Require clear horizontal intent toward the submenu.
21
+ if (direction === 'right' && dx <= 2)
22
+ return false;
23
+ if (direction === 'left' && dx >= -2)
24
+ return false;
25
+ // Use the NEAR edge of the submenu panel (closest to the parent menu).
26
+ // This matches the classic "menu aim" wedge used to detect intent to enter the open submenu.
27
+ // Right-opening submenu: use left edge; left-opening submenu: use right edge.
28
+ const edgeX = direction === 'right' ? submenuRect.left : submenuRect.right;
29
+ // Expand wedge slightly to be forgiving.
30
+ const padY = 8;
31
+ const cornerA = { x: edgeX, y: submenuRect.top - padY };
32
+ const cornerB = { x: edgeX, y: submenuRect.bottom + padY };
33
+ return pointInTriangle(current, previous, cornerA, cornerB);
34
+ }
35
+ function pointInTriangle(p, a, b, c) {
36
+ const d1 = sign(p, a, b);
37
+ const d2 = sign(p, b, c);
38
+ const d3 = sign(p, c, a);
39
+ const hasNeg = d1 < 0 || d2 < 0 || d3 < 0;
40
+ const hasPos = d1 > 0 || d2 > 0 || d3 > 0;
41
+ return !(hasNeg && hasPos);
42
+ }
43
+ function sign(p1, p2, p3) {
44
+ return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
45
+ }
46
+
47
+ const DEFAULT_COAR_MENU_AIM_CONFIG = {
48
+ enabled: true,
49
+ debugEnabled: false,
50
+ switchDelayMs: 500,
51
+ sampleMaxAgeMs: 200,
52
+ };
53
+ const COAR_MENU_AIM_CONFIG = new InjectionToken('COAR_MENU_AIM_CONFIG');
54
+
55
+ /**
56
+ * CoarMenuCascade: Tracks parent-child and sibling relationships for menu hierarchies.
57
+ *
58
+ * Responsibilities:
59
+ * - Track overlay refs for nested submenu parenting
60
+ * - Track children to enable sibling closure (for inline menus without a common parent overlay)
61
+ */
62
+ class CoarMenuCascade {
63
+ parent;
64
+ overlayRef = null;
65
+ children = new Set();
66
+ aimConfig = inject(COAR_MENU_AIM_CONFIG, {
67
+ optional: true,
68
+ });
69
+ activeChild = null;
70
+ pointerHistory = [];
71
+ pointerAbort = null;
72
+ pendingSwitchTimer = null;
73
+ pendingChild = null;
74
+ pendingActivate = null;
75
+ activePanelCleanup = null;
76
+ constructor(parent) {
77
+ this.parent = parent;
78
+ // Register with parent to enable sibling tracking
79
+ if (parent) {
80
+ parent.children.add(this);
81
+ }
82
+ }
83
+ requestOpenFromChild(child, activate, pointer) {
84
+ const aim = this.aimConfig?.getMenuAimConfig() ?? DEFAULT_COAR_MENU_AIM_CONFIG;
85
+ if (!aim.enabled) {
86
+ this.activateNow(child, activate);
87
+ return;
88
+ }
89
+ if (typeof document === 'undefined') {
90
+ this.activateNow(child, activate);
91
+ return;
92
+ }
93
+ const now = Date.now();
94
+ const point = { x: pointer.x, y: pointer.y, t: now };
95
+ this.pushPointerPoint(point);
96
+ if (!this.activeChild || this.activeChild === child) {
97
+ this.activateNow(child, activate);
98
+ return;
99
+ }
100
+ const submenuRect = this.getActiveChildSubmenuRect();
101
+ if (!submenuRect) {
102
+ this.activateNow(child, activate);
103
+ return;
104
+ }
105
+ const previous = this.pointerHistory.length >= 2 ? this.pointerHistory[this.pointerHistory.length - 2] : null;
106
+ const direction = this.inferSubmenuDirection(submenuRect, point);
107
+ const shouldDelay = shouldDelaySubmenuSwitch(previous, point, submenuRect, direction, aim.sampleMaxAgeMs);
108
+ this.emitAimDebugIfEnabled(aim.debugEnabled, shouldDelay, previous, point, submenuRect, direction);
109
+ if (!shouldDelay) {
110
+ this.activateNow(child, activate);
111
+ return;
112
+ }
113
+ this.scheduleSwitch(child, activate, aim.switchDelayMs);
114
+ }
115
+ emitAimDebugIfEnabled(debugEnabled, shouldDelay, previous, current, submenuRect, direction) {
116
+ if (typeof window === 'undefined')
117
+ return;
118
+ // Backward-compatible global override for ad-hoc debugging.
119
+ const w = window;
120
+ if (!debugEnabled && !w.__COAR_MENU_AIM_DEBUG__)
121
+ return;
122
+ try {
123
+ window.dispatchEvent(new CustomEvent('coar-menu-aim', {
124
+ detail: {
125
+ shouldDelay,
126
+ previous,
127
+ current,
128
+ submenuRect: {
129
+ left: submenuRect.left,
130
+ top: submenuRect.top,
131
+ right: submenuRect.right,
132
+ bottom: submenuRect.bottom,
133
+ },
134
+ direction,
135
+ },
136
+ }));
137
+ }
138
+ catch {
139
+ // ignore
140
+ }
141
+ }
142
+ notifyChildOpened(child) {
143
+ if (this.activeChild === child) {
144
+ this.attachActivePanelListener();
145
+ }
146
+ }
147
+ notifyChildClosed(child) {
148
+ if (this.activeChild === child) {
149
+ this.activeChild = null;
150
+ this.detachActivePanelListener();
151
+ this.cancelPendingSwitch();
152
+ this.stopPointerTracking();
153
+ }
154
+ }
155
+ activateNow(child, activate) {
156
+ this.cancelPendingSwitch();
157
+ this.activeChild = child;
158
+ this.ensurePointerTracking();
159
+ activate();
160
+ }
161
+ scheduleSwitch(child, activate, delayMs) {
162
+ this.cancelPendingSwitch();
163
+ this.pendingChild = child;
164
+ this.pendingActivate = activate;
165
+ this.pendingSwitchTimer = setTimeout(() => {
166
+ const pendingChild = this.pendingChild;
167
+ const pendingActivate = this.pendingActivate;
168
+ this.pendingChild = null;
169
+ this.pendingActivate = null;
170
+ this.pendingSwitchTimer = null;
171
+ if (!pendingChild || !pendingActivate)
172
+ return;
173
+ this.activeChild = pendingChild;
174
+ this.ensurePointerTracking();
175
+ pendingActivate();
176
+ }, delayMs);
177
+ }
178
+ cancelPendingSwitch() {
179
+ if (this.pendingSwitchTimer) {
180
+ clearTimeout(this.pendingSwitchTimer);
181
+ this.pendingSwitchTimer = null;
182
+ }
183
+ this.pendingChild = null;
184
+ this.pendingActivate = null;
185
+ }
186
+ ensurePointerTracking() {
187
+ if (this.pointerAbort)
188
+ return;
189
+ if (typeof document === 'undefined')
190
+ return;
191
+ const abort = new AbortController();
192
+ this.pointerAbort = abort;
193
+ const onMove = (e) => {
194
+ this.pushPointerPoint({ x: e.clientX, y: e.clientY, t: Date.now() });
195
+ };
196
+ document.addEventListener('pointermove', onMove, { signal: abort.signal, passive: true });
197
+ }
198
+ stopPointerTracking() {
199
+ if (!this.pointerAbort)
200
+ return;
201
+ this.pointerAbort.abort();
202
+ this.pointerAbort = null;
203
+ this.pointerHistory = [];
204
+ }
205
+ pushPointerPoint(point) {
206
+ this.pointerHistory.push(point);
207
+ if (this.pointerHistory.length > 5) {
208
+ this.pointerHistory = this.pointerHistory.slice(-5);
209
+ }
210
+ }
211
+ getActiveChildSubmenuRect() {
212
+ const panelEl = this.activeChild?.overlayRef?.getPanelElement?.();
213
+ if (!panelEl)
214
+ return null;
215
+ return panelEl.getBoundingClientRect();
216
+ }
217
+ inferSubmenuDirection(rect, point) {
218
+ // If the submenu's left edge is to the right of the pointer, it's a right-opening flyout.
219
+ // Otherwise, assume left.
220
+ return rect.left >= point.x ? 'right' : 'left';
221
+ }
222
+ attachActivePanelListener() {
223
+ this.detachActivePanelListener();
224
+ const panelEl = this.activeChild?.overlayRef?.getPanelElement?.();
225
+ if (!panelEl)
226
+ return;
227
+ const onEnter = () => {
228
+ // Once the user reaches the currently open submenu panel, don't allow a delayed switch
229
+ // to steal focus/open another sibling submenu.
230
+ this.cancelPendingSwitch();
231
+ };
232
+ panelEl.addEventListener('pointerenter', onEnter);
233
+ this.activePanelCleanup = () => panelEl.removeEventListener('pointerenter', onEnter);
234
+ }
235
+ detachActivePanelListener() {
236
+ this.activePanelCleanup?.();
237
+ this.activePanelCleanup = null;
238
+ }
239
+ /**
240
+ * Close all sibling submenus at the cascade level.
241
+ * Used for inline menus where siblings don't have a common parent overlay.
242
+ */
243
+ closeSiblings() {
244
+ if (this.parent) {
245
+ for (const sibling of this.parent.children) {
246
+ if (sibling !== this && sibling.overlayRef) {
247
+ sibling.overlayRef.close();
248
+ }
249
+ }
250
+ }
251
+ }
252
+ destroy() {
253
+ this.detachActivePanelListener();
254
+ this.cancelPendingSwitch();
255
+ this.stopPointerTracking();
256
+ if (this.parent) {
257
+ this.parent.children.delete(this);
258
+ }
259
+ }
260
+ }
261
+ const COAR_MENU_CASCADE = new InjectionToken('COAR_MENU_CASCADE');
262
+
263
+ class CoarMenuAimConfigDirective {
264
+ parent = inject(COAR_MENU_AIM_CONFIG, { optional: true, skipSelf: true });
265
+ /** Enable/disable menu-aim for this menu tree. */
266
+ aimEnabled = input(undefined, ...(ngDevMode ? [{ debugName: "aimEnabled" }] : []));
267
+ /** Emit debug events for menu-aim visualization (showcase only). */
268
+ aimDebugEnabled = input(undefined, ...(ngDevMode ? [{ debugName: "aimDebugEnabled" }] : []));
269
+ /** Delay before switching to a newly hovered sibling submenu when aim is detected. */
270
+ aimSwitchDelayMs = input(undefined, ...(ngDevMode ? [{ debugName: "aimSwitchDelayMs" }] : []));
271
+ /** Maximum age of the last pointer sample used for intent detection. */
272
+ aimSampleMaxAgeMs = input(undefined, ...(ngDevMode ? [{ debugName: "aimSampleMaxAgeMs" }] : []));
273
+ getMenuAimConfig() {
274
+ const base = this.parent?.getMenuAimConfig() ?? DEFAULT_COAR_MENU_AIM_CONFIG;
275
+ return {
276
+ enabled: this.aimEnabled() ?? base.enabled,
277
+ debugEnabled: this.aimDebugEnabled() ?? base.debugEnabled,
278
+ switchDelayMs: this.aimSwitchDelayMs() ?? base.switchDelayMs,
279
+ sampleMaxAgeMs: this.aimSampleMaxAgeMs() ?? base.sampleMaxAgeMs,
280
+ };
281
+ }
282
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuAimConfigDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
283
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.6", type: CoarMenuAimConfigDirective, isStandalone: true, selector: "[coarMenuAimConfig]", inputs: { aimEnabled: { classPropertyName: "aimEnabled", publicName: "aimEnabled", isSignal: true, isRequired: false, transformFunction: null }, aimDebugEnabled: { classPropertyName: "aimDebugEnabled", publicName: "aimDebugEnabled", isSignal: true, isRequired: false, transformFunction: null }, aimSwitchDelayMs: { classPropertyName: "aimSwitchDelayMs", publicName: "aimSwitchDelayMs", isSignal: true, isRequired: false, transformFunction: null }, aimSampleMaxAgeMs: { classPropertyName: "aimSampleMaxAgeMs", publicName: "aimSampleMaxAgeMs", isSignal: true, isRequired: false, transformFunction: null } }, providers: [{ provide: COAR_MENU_AIM_CONFIG, useExisting: CoarMenuAimConfigDirective }], ngImport: i0 });
284
+ }
285
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuAimConfigDirective, decorators: [{
286
+ type: Directive,
287
+ args: [{
288
+ selector: '[coarMenuAimConfig]',
289
+ standalone: true,
290
+ providers: [{ provide: COAR_MENU_AIM_CONFIG, useExisting: CoarMenuAimConfigDirective }],
291
+ }]
292
+ }], propDecorators: { aimEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "aimEnabled", required: false }] }], aimDebugEnabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "aimDebugEnabled", required: false }] }], aimSwitchDelayMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "aimSwitchDelayMs", required: false }] }], aimSampleMaxAgeMs: [{ type: i0.Input, args: [{ isSignal: true, alias: "aimSampleMaxAgeMs", required: false }] }] } });
293
+
294
+ /**
295
+ * CoarMenu: Shell component providing menu styling container.
296
+ *
297
+ * Responsibilities:
298
+ * - Apply consistent menu styling via CSS variables
299
+ * - Provide semantic menu container (<menu> or <div role="menu">)
300
+ * - Provide root cascade for sibling submenu tracking
301
+ * - No logic - just a styled wrapper
302
+ *
303
+ * Use standalone for inline menus, or as content in CoarOverlayService for context menus/flyouts.
304
+ *
305
+ * @example
306
+ * ```html
307
+ * <coar-menu>
308
+ * <coar-menu-item>Action 1</coar-menu-item>
309
+ * <coar-menu-item>Action 2</coar-menu-item>
310
+ * <coar-menu-divider></coar-menu-divider>
311
+ * <coar-menu-item>Action 3</coar-menu-item>
312
+ * </coar-menu>
313
+ * ```
314
+ */
315
+ class CoarMenuComponent {
316
+ overlayRef = inject(COAR_OVERLAY_REF, { optional: true });
317
+ /**
318
+ * Controls whether the menu reserves and renders an icon column.
319
+ *
320
+ * Default is enabled to avoid layout shift for stateful icons (e.g. checkmarks).
321
+ * Set to false for text-only menus (icons will not render).
322
+ */
323
+ showIconColumn = input(true, ...(ngDevMode ? [{ debugName: "showIconColumn" }] : []));
324
+ /**
325
+ * Check if this menu is rendered inside an overlay (flyout).
326
+ * Used to enable border visibility for menu items.
327
+ */
328
+ isInOverlay() {
329
+ return this.overlayRef !== null;
330
+ }
331
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
332
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.6", type: CoarMenuComponent, isStandalone: true, selector: "coar-menu", inputs: { showIconColumn: { classPropertyName: "showIconColumn", publicName: "showIconColumn", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menu" }, properties: { "class.coar-menu--in-overlay": "isInOverlay()", "style.--coar-menu-icon-slot-display": "showIconColumn() ? null : \"none\"", "style.--coar-menu-item-icon-slot-size": "showIconColumn() ? null : \"0px\"" }, classAttribute: "coar-menu" }, providers: [
333
+ {
334
+ provide: COAR_MENU_CASCADE,
335
+ useFactory: () => {
336
+ // Check if we're inside an overlay
337
+ const inOverlay = inject(COAR_OVERLAY_REF, { optional: true });
338
+ const parentCascade = inject(COAR_MENU_CASCADE, { optional: true, skipSelf: true });
339
+ if (inOverlay && parentCascade) {
340
+ // We're a menu inside a submenu overlay
341
+ // Create a new cascade as a child of the parent, so sibling submenu-items
342
+ // at this level can track each other
343
+ return new CoarMenuCascade(parentCascade);
344
+ }
345
+ // For root menus (inline or context menu), create a new root cascade
346
+ return new CoarMenuCascade(parentCascade);
347
+ },
348
+ },
349
+ ], hostDirectives: [{ directive: CoarMenuAimConfigDirective, inputs: ["aimEnabled", "aimEnabled", "aimDebugEnabled", "aimDebugEnabled", "aimSwitchDelayMs", "aimSwitchDelayMs", "aimSampleMaxAgeMs", "aimSampleMaxAgeMs"] }], ngImport: i0, template: "<ng-content />\n", styles: [":host{display:flex;flex-direction:column;min-width:var(--coar-menu-min-width, 12rem);max-width:var(--coar-menu-max-width, 20rem);gap:0;background:var(--coar-menu-background, #f8f9fa);border:var(--coar-menu-border, 1px solid var(--coar-border-neutral-tertiary, #d0d0d0));border-radius:var(--coar-menu-border-radius, var(--coar-radius-s, 4px));overflow:hidden;box-shadow:var(--coar-menu-shadow, var(--coar-shadow-s, none))}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
350
+ }
351
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuComponent, decorators: [{
352
+ type: Component,
353
+ args: [{ selector: 'coar-menu', standalone: true, imports: [], hostDirectives: [
354
+ {
355
+ directive: CoarMenuAimConfigDirective,
356
+ inputs: ['aimEnabled', 'aimDebugEnabled', 'aimSwitchDelayMs', 'aimSampleMaxAgeMs'],
357
+ },
358
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
359
+ role: 'menu',
360
+ class: 'coar-menu',
361
+ '[class.coar-menu--in-overlay]': 'isInOverlay()',
362
+ '[style.--coar-menu-icon-slot-display]': 'showIconColumn() ? null : "none"',
363
+ '[style.--coar-menu-item-icon-slot-size]': 'showIconColumn() ? null : "0px"',
364
+ }, providers: [
365
+ {
366
+ provide: COAR_MENU_CASCADE,
367
+ useFactory: () => {
368
+ // Check if we're inside an overlay
369
+ const inOverlay = inject(COAR_OVERLAY_REF, { optional: true });
370
+ const parentCascade = inject(COAR_MENU_CASCADE, { optional: true, skipSelf: true });
371
+ if (inOverlay && parentCascade) {
372
+ // We're a menu inside a submenu overlay
373
+ // Create a new cascade as a child of the parent, so sibling submenu-items
374
+ // at this level can track each other
375
+ return new CoarMenuCascade(parentCascade);
376
+ }
377
+ // For root menus (inline or context menu), create a new root cascade
378
+ return new CoarMenuCascade(parentCascade);
379
+ },
380
+ },
381
+ ], template: "<ng-content />\n", styles: [":host{display:flex;flex-direction:column;min-width:var(--coar-menu-min-width, 12rem);max-width:var(--coar-menu-max-width, 20rem);gap:0;background:var(--coar-menu-background, #f8f9fa);border:var(--coar-menu-border, 1px solid var(--coar-border-neutral-tertiary, #d0d0d0));border-radius:var(--coar-menu-border-radius, var(--coar-radius-s, 4px));overflow:hidden;box-shadow:var(--coar-menu-shadow, var(--coar-shadow-s, none))}\n"] }]
382
+ }], propDecorators: { showIconColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "showIconColumn", required: false }] }] } });
383
+
384
+ /**
385
+ * CoarMenuItem: Individual menu item with optional icon and submenu support.
386
+ *
387
+ * Responsibilities:
388
+ * - Render item text and optional icon
389
+ * - Handle click events
390
+ * - Support disabled state
391
+ * - Visual states: hover, active, focus
392
+ * - Trigger submenu (accordion or flyout via parent menu logic)
393
+ *
394
+ * Does NOT manage overlay directly - parent menu handles flyout logic.
395
+ *
396
+ * @example
397
+ * ```html
398
+ * <coar-menu-item (itemClick)="onSave()">Save</coar-menu-item>
399
+ * <coar-menu-item icon="copy">Copy</coar-menu-item>
400
+ * <coar-menu-item [disabled]="true">Unavailable</coar-menu-item>
401
+ *
402
+ * <!-- Prevent menu from closing on click: -->
403
+ * <coar-menu-item (itemClick)="toggle($event)">Toggle Setting</coar-menu-item>
404
+ *
405
+ * toggle(event: CoarMenuItemClickEvent) {
406
+ * event.keepMenuOpen(); // Keep menu open
407
+ * this.setting = !this.setting;
408
+ * }
409
+ * ```
410
+ */
411
+ class CoarMenuItemComponent {
412
+ parentOverlay = inject(COAR_MENU_PARENT, { optional: true });
413
+ /** Item text content */
414
+ label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : []));
415
+ /** Optional icon identifier (rendered via CoarIconComponent) */
416
+ icon = input(undefined, ...(ngDevMode ? [{ debugName: "icon" }] : []));
417
+ /** Disabled state prevents interaction */
418
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
419
+ /** Emitted when user clicks/selects the item. Menu closes by default unless preventDefault() is called. */
420
+ itemClick = output();
421
+ /** Emitted when user hovers over item (for flyout trigger) */
422
+ itemHover = output();
423
+ onClick(event) {
424
+ if (this.disabled()) {
425
+ event.preventDefault();
426
+ event.stopPropagation();
427
+ return;
428
+ }
429
+ // Always stop propagation to prevent overlay's outside-click handler
430
+ event.stopPropagation();
431
+ let shouldClose = true;
432
+ const clickEvent = {
433
+ event,
434
+ keepMenuOpen: () => {
435
+ shouldClose = false;
436
+ },
437
+ };
438
+ this.itemClick.emit(clickEvent);
439
+ // Close root overlay after Angular's change detection and OUTSIDE the hover area
440
+ const parentOverlay = this.parentOverlay;
441
+ if (shouldClose && parentOverlay) {
442
+ setTimeout(() => {
443
+ // Close the ROOT overlay to ensure the entire menu tree closes
444
+ const root = parentOverlay.getRoot();
445
+ root.close();
446
+ }, 10);
447
+ }
448
+ }
449
+ onMouseEnter(event) {
450
+ if (!this.disabled()) {
451
+ this.itemHover.emit(event);
452
+ }
453
+ }
454
+ onKeyboardActivate() {
455
+ if (this.disabled()) {
456
+ return;
457
+ }
458
+ let shouldClose = true;
459
+ const clickEvent = {
460
+ event: new MouseEvent('click'), // Synthetic event for keyboard
461
+ keepMenuOpen: () => {
462
+ shouldClose = false;
463
+ },
464
+ };
465
+ this.itemClick.emit(clickEvent);
466
+ const parentOverlay = this.parentOverlay;
467
+ if (shouldClose && parentOverlay) {
468
+ setTimeout(() => {
469
+ // Close the ROOT overlay to ensure the entire menu tree closes
470
+ const root = parentOverlay.getRoot();
471
+ root.close();
472
+ }, 10);
473
+ }
474
+ }
475
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
476
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: CoarMenuItemComponent, isStandalone: true, selector: "coar-menu-item", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", itemHover: "itemHover" }, host: { attributes: { "role": "menuitem" }, listeners: { "click": "onClick($event)", "mouseenter": "onMouseEnter($event)", "keydown.enter": "onKeyboardActivate()", "keydown.space": "onKeyboardActivate()" }, properties: { "class.coar-menu-item--disabled": "disabled()", "attr.aria-disabled": "disabled()", "attr.tabindex": "disabled() ? -1 : 0" }, classAttribute: "coar-menu-item" }, ngImport: i0, template: "<span class=\"coar-menu-item__icon\" aria-hidden=\"true\">\n <coar-icon [name]=\"icon() || 'square-rounded-dashed'\" size=\"sm\" aria-hidden=\"true\" />\n</span>\n<span class=\"coar-menu-item__label\">\n @if (label()) {\n {{ label() }}\n } @else {\n <ng-content />\n }\n</span>\n", styles: [":host{display:flex;align-items:center;gap:var(--coar-menu-item-gap, .75rem);width:100%;box-sizing:border-box;padding:var(--coar-menu-item-padding, .5rem .75rem);font-family:var(--coar-menu-item-font-family, var(--coar-font-family-body, Poppins));font-size:var( --coar-menu-item-font-size, var(--coar-component-md-font-size, var(--coar-font-size-xs, 14px)) );font-weight:var(--coar-menu-item-font-weight, var(--coar-font-weight-regular, 400));line-height:var(--coar-menu-item-line-height, 1.5);color:var(--coar-menu-item-color, var(--coar-text-neutral-primary, #545454));background:var(--coar-menu-item-background, transparent);border-radius:0;position:relative;cursor:pointer;-webkit-user-select:none;user-select:none;transition:var(--coar-transition-fast, all .1s ease);outline:none}:host:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:1px;background:var(--coar-menu-item-border-left-color, #d0d0d0);opacity:0;pointer-events:none;transition:width var(--coar-duration-fast) var(--coar-ease-out),background var(--coar-duration-fast) var(--coar-ease-out)}:host(:hover:not(.coar-menu-item--disabled)){background:var(--coar-menu-item-background-hover, #f0f1f2);color:var(--coar-menu-item-color-hover, var(--coar-text-neutral-primary, #545454))}:host(:active:not(.coar-menu-item--disabled)){background:var( --coar-menu-item-background-active, var(--coar-menu-item-background-open, #f0f1f2) )}:host(:active:not(.coar-menu-item--disabled)):before{width:2px;background:var( --coar-menu-item-border-left-color-active, var(--coar-border-accent-primary, #156db7) );opacity:1}:host-context(.coar-sub-expand__panel-inner):before{opacity:1}:host-context(.coar-menu--in-overlay):before{opacity:1}:host(:focus-visible){background:var(--coar-menu-item-background-focus, #f0f1f2);outline:2px solid var(--coar-menu-item-outline-focus, var(--coar-border-accent-primary, #156db7));outline-offset:-2px}:host(.coar-menu-item--disabled){color:var(--coar-menu-item-color-disabled, var(--coar-text-neutral-disabled, #999999));cursor:not-allowed;opacity:var(--coar-menu-item-opacity-disabled, .5)}.coar-menu-item__icon{display:var(--coar-menu-icon-slot-display, inline-flex);align-items:center;justify-content:center;flex-shrink:0;width:var(--coar-menu-item-icon-slot-size, 16px);height:var(--coar-menu-item-icon-slot-size, 16px);opacity:1}.coar-menu-item__icon:has(coar-icon[icon-name=square-rounded-dashed]){opacity:.3}.coar-menu-item__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"], dependencies: [{ kind: "component", type: CoarIconComponent, selector: "coar-icon", inputs: ["name", "size", "rotate", "rotateTransition", "spin", "color", "label", "fallback"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
477
+ }
478
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuItemComponent, decorators: [{
479
+ type: Component,
480
+ args: [{ selector: 'coar-menu-item', standalone: true, imports: [CoarIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
481
+ role: 'menuitem',
482
+ class: 'coar-menu-item',
483
+ '[class.coar-menu-item--disabled]': 'disabled()',
484
+ '[attr.aria-disabled]': 'disabled()',
485
+ '[attr.tabindex]': 'disabled() ? -1 : 0',
486
+ }, template: "<span class=\"coar-menu-item__icon\" aria-hidden=\"true\">\n <coar-icon [name]=\"icon() || 'square-rounded-dashed'\" size=\"sm\" aria-hidden=\"true\" />\n</span>\n<span class=\"coar-menu-item__label\">\n @if (label()) {\n {{ label() }}\n } @else {\n <ng-content />\n }\n</span>\n", styles: [":host{display:flex;align-items:center;gap:var(--coar-menu-item-gap, .75rem);width:100%;box-sizing:border-box;padding:var(--coar-menu-item-padding, .5rem .75rem);font-family:var(--coar-menu-item-font-family, var(--coar-font-family-body, Poppins));font-size:var( --coar-menu-item-font-size, var(--coar-component-md-font-size, var(--coar-font-size-xs, 14px)) );font-weight:var(--coar-menu-item-font-weight, var(--coar-font-weight-regular, 400));line-height:var(--coar-menu-item-line-height, 1.5);color:var(--coar-menu-item-color, var(--coar-text-neutral-primary, #545454));background:var(--coar-menu-item-background, transparent);border-radius:0;position:relative;cursor:pointer;-webkit-user-select:none;user-select:none;transition:var(--coar-transition-fast, all .1s ease);outline:none}:host:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:1px;background:var(--coar-menu-item-border-left-color, #d0d0d0);opacity:0;pointer-events:none;transition:width var(--coar-duration-fast) var(--coar-ease-out),background var(--coar-duration-fast) var(--coar-ease-out)}:host(:hover:not(.coar-menu-item--disabled)){background:var(--coar-menu-item-background-hover, #f0f1f2);color:var(--coar-menu-item-color-hover, var(--coar-text-neutral-primary, #545454))}:host(:active:not(.coar-menu-item--disabled)){background:var( --coar-menu-item-background-active, var(--coar-menu-item-background-open, #f0f1f2) )}:host(:active:not(.coar-menu-item--disabled)):before{width:2px;background:var( --coar-menu-item-border-left-color-active, var(--coar-border-accent-primary, #156db7) );opacity:1}:host-context(.coar-sub-expand__panel-inner):before{opacity:1}:host-context(.coar-menu--in-overlay):before{opacity:1}:host(:focus-visible){background:var(--coar-menu-item-background-focus, #f0f1f2);outline:2px solid var(--coar-menu-item-outline-focus, var(--coar-border-accent-primary, #156db7));outline-offset:-2px}:host(.coar-menu-item--disabled){color:var(--coar-menu-item-color-disabled, var(--coar-text-neutral-disabled, #999999));cursor:not-allowed;opacity:var(--coar-menu-item-opacity-disabled, .5)}.coar-menu-item__icon{display:var(--coar-menu-icon-slot-display, inline-flex);align-items:center;justify-content:center;flex-shrink:0;width:var(--coar-menu-item-icon-slot-size, 16px);height:var(--coar-menu-item-icon-slot-size, 16px);opacity:1}.coar-menu-item__icon:has(coar-icon[icon-name=square-rounded-dashed]){opacity:.3}.coar-menu-item__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n"] }]
487
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], itemHover: [{ type: i0.Output, args: ["itemHover"] }], onClick: [{
488
+ type: HostListener,
489
+ args: ['click', ['$event']]
490
+ }], onMouseEnter: [{
491
+ type: HostListener,
492
+ args: ['mouseenter', ['$event']]
493
+ }], onKeyboardActivate: [{
494
+ type: HostListener,
495
+ args: ['keydown.enter']
496
+ }, {
497
+ type: HostListener,
498
+ args: ['keydown.space']
499
+ }] } });
500
+
501
+ /**
502
+ * CoarMenuDivider: Visual separator between menu items.
503
+ *
504
+ * Simple horizontal line for grouping menu items.
505
+ *
506
+ * @example
507
+ * ```html
508
+ * <coar-menu>
509
+ * <coar-menu-item>Cut</coar-menu-item>
510
+ * <coar-menu-item>Copy</coar-menu-item>
511
+ * <coar-menu-divider></coar-menu-divider>
512
+ * <coar-menu-item>Paste</coar-menu-item>
513
+ * </coar-menu>
514
+ * ```
515
+ */
516
+ class CoarMenuDividerComponent {
517
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuDividerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
518
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: CoarMenuDividerComponent, isStandalone: true, selector: "coar-menu-divider", host: { attributes: { "role": "separator" }, classAttribute: "coar-menu-divider" }, ngImport: i0, template: '', isInline: true, styles: [":host{display:block;height:1px;margin:var(--coar-menu-divider-margin, .25rem .75rem);background:var(--coar-menu-divider-color, var(--coar-border-neutral-tertiary, #e0e0e0))}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
519
+ }
520
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuDividerComponent, decorators: [{
521
+ type: Component,
522
+ args: [{ selector: 'coar-menu-divider', standalone: true, imports: [], template: '', changeDetection: ChangeDetectionStrategy.OnPush, host: {
523
+ role: 'separator',
524
+ class: 'coar-menu-divider',
525
+ }, styles: [":host{display:block;height:1px;margin:var(--coar-menu-divider-margin, .25rem .75rem);background:var(--coar-menu-divider-color, var(--coar-border-neutral-tertiary, #e0e0e0))}\n"] }]
526
+ }] });
527
+
528
+ /**
529
+ * CoarMenuHeading: Non-interactive section label for menu groups.
530
+ *
531
+ * Responsibilities:
532
+ * - Display section/group heading text
533
+ * - Provide visual separation between menu sections
534
+ * - Non-interactive (no hover, no click, not focusable)
535
+ *
536
+ * @example
537
+ * ```html
538
+ * <coar-menu>
539
+ * <coar-menu-heading>Foundations</coar-menu-heading>
540
+ * <coar-menu-item routerLink="/typography">Typography</coar-menu-item>
541
+ * <coar-menu-item routerLink="/colors">Colors</coar-menu-item>
542
+ *
543
+ * <coar-menu-heading>Form Controls</coar-menu-heading>
544
+ * <coar-menu-item routerLink="/text-input">Text Input</coar-menu-item>
545
+ * </coar-menu>
546
+ * ```
547
+ */
548
+ class CoarMenuHeadingComponent {
549
+ /** Optional explicit label text (alternative to content projection) */
550
+ label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : []));
551
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuHeadingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
552
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: CoarMenuHeadingComponent, isStandalone: true, selector: "coar-menu-heading", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "coar-menu-heading-host" }, ngImport: i0, template: `
553
+ <div class="coar-menu-heading">
554
+ @if (label()) {
555
+ {{ label() }}
556
+ } @else {
557
+ <ng-content />
558
+ }
559
+ </div>
560
+ `, isInline: true, styles: [":host{display:block}:host(:not(:first-child)){margin-top:var(--coar-menu-heading-spacing-top, .2rem)}.coar-menu-heading{display:flex;align-items:center;width:100%;box-sizing:border-box;padding:var(--coar-menu-heading-padding, .75rem .75rem .25rem .75rem);font-family:var(--coar-menu-heading-font-family, var(--coar-font-family-body, Poppins));font-size:var(--coar-menu-heading-font-size, var(--coar-component-xs-font-size, 11px));font-weight:var(--coar-menu-heading-font-weight, var(--coar-font-weight-semi-bold, 600));line-height:var(--coar-menu-heading-line-height, 1.4);text-transform:var(--coar-menu-heading-text-transform, uppercase);letter-spacing:var(--coar-menu-heading-letter-spacing, .05em);color:var(--coar-menu-heading-color, var(--coar-text-neutral-secondary, #6b7280));cursor:default;-webkit-user-select:none;user-select:none}:host-context(.coar-menu--sidebar) .coar-menu-heading{font-size:16px;letter-spacing:.08em;padding:.5rem .75rem .375rem}:host-context(.coar-menu--sidebar):not(:first-child){margin-top:1.25rem}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
561
+ }
562
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuHeadingComponent, decorators: [{
563
+ type: Component,
564
+ args: [{ selector: 'coar-menu-heading', standalone: true, imports: [], template: `
565
+ <div class="coar-menu-heading">
566
+ @if (label()) {
567
+ {{ label() }}
568
+ } @else {
569
+ <ng-content />
570
+ }
571
+ </div>
572
+ `, changeDetection: ChangeDetectionStrategy.OnPush, host: {
573
+ class: 'coar-menu-heading-host',
574
+ }, styles: [":host{display:block}:host(:not(:first-child)){margin-top:var(--coar-menu-heading-spacing-top, .2rem)}.coar-menu-heading{display:flex;align-items:center;width:100%;box-sizing:border-box;padding:var(--coar-menu-heading-padding, .75rem .75rem .25rem .75rem);font-family:var(--coar-menu-heading-font-family, var(--coar-font-family-body, Poppins));font-size:var(--coar-menu-heading-font-size, var(--coar-component-xs-font-size, 11px));font-weight:var(--coar-menu-heading-font-weight, var(--coar-font-weight-semi-bold, 600));line-height:var(--coar-menu-heading-line-height, 1.4);text-transform:var(--coar-menu-heading-text-transform, uppercase);letter-spacing:var(--coar-menu-heading-letter-spacing, .05em);color:var(--coar-menu-heading-color, var(--coar-text-neutral-secondary, #6b7280));cursor:default;-webkit-user-select:none;user-select:none}:host-context(.coar-menu--sidebar) .coar-menu-heading{font-size:16px;letter-spacing:.08em;padding:.5rem .75rem .375rem}:host-context(.coar-menu--sidebar):not(:first-child){margin-top:1.25rem}\n"] }]
575
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
576
+
577
+ /**
578
+ * Marks an inline submenu template for a `coar-submenu-item`.
579
+ *
580
+ * This is optional: if a submenu item contains exactly one direct child `<ng-template>`,
581
+ * that template will be used even without this directive.
582
+ */
583
+ class CoarSubmenuTemplateDirective {
584
+ templateRef = inject(TemplateRef);
585
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubmenuTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
586
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: CoarSubmenuTemplateDirective, isStandalone: true, selector: "ng-template[coarSubmenu]", ngImport: i0 });
587
+ }
588
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubmenuTemplateDirective, decorators: [{
589
+ type: Directive,
590
+ args: [{
591
+ selector: 'ng-template[coarSubmenu]',
592
+ standalone: true,
593
+ }]
594
+ }] });
595
+
596
+ /**
597
+ * CoarSubmenuItem: Menu item that opens a submenu on hover.
598
+ *
599
+ * Responsibilities:
600
+ * - Render parent item with icon and label
601
+ * - Manage submenu overlay lifecycle
602
+ * - Support nested submenus (can contain other submenu-items)
603
+ *
604
+ * @example
605
+ * ```html
606
+ * <coar-submenu-item label="Share" icon="🔗">
607
+ * <ng-template>
608
+ * <coar-menu-item icon="✉️" (itemClick)="sendEmail()">Email</coar-menu-item>
609
+ * <coar-menu-item icon="🔗" (itemClick)="copyLink()">Copy Link</coar-menu-item>
610
+ * </ng-template>
611
+ * </coar-submenu-item>
612
+ *
613
+ * <!-- Optional: explicitly mark the template -->
614
+ * <coar-submenu-item label="Share" icon="🔗">
615
+ * <ng-template coarSubmenu>
616
+ * ...
617
+ * </ng-template>
618
+ * </coar-submenu-item>
619
+ *
620
+ * <!-- Also supported (legacy / external template): -->
621
+ * <coar-submenu-item label="Share" icon="🔗" [submenuTemplate]="shareMenu" />
622
+ * ```
623
+ */
624
+ class CoarSubmenuItemComponent {
625
+ overlayService = inject(CoarOverlayService);
626
+ destroyRef = inject(DestroyRef);
627
+ cdr = inject(ChangeDetectorRef);
628
+ cascade = inject(COAR_MENU_CASCADE);
629
+ parentOverlay = inject(COAR_MENU_PARENT, { optional: true });
630
+ injector = inject(Injector);
631
+ // Ensures the submenu TemplateRef is instantiated with a parent injector that contains
632
+ // the correct cascade instance for this submenu item. Without this, TemplateRefs declared
633
+ // outside of this component can end up resolving menu context from the declaration site.
634
+ submenuTemplateInjector = Injector.create({
635
+ providers: [{ provide: COAR_MENU_CASCADE, useValue: this.cascade }],
636
+ parent: this.injector,
637
+ });
638
+ constructor() {
639
+ // Cleanup cascade when component is destroyed
640
+ this.destroyRef.onDestroy(() => {
641
+ this.cascade.destroy();
642
+ // Ensure state is cleared on destroy
643
+ if (this.isOpen) {
644
+ this.isOpen = false;
645
+ this.submenuRef = null;
646
+ }
647
+ });
648
+ }
649
+ /** Label text for the menu item */
650
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
651
+ /** Optional icon identifier */
652
+ icon = input(undefined, ...(ngDevMode ? [{ debugName: "icon" }] : []));
653
+ /** Disabled state prevents interaction */
654
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
655
+ /**
656
+ * Optional external submenu template.
657
+ *
658
+ * Prefer an inline `<ng-template>` child when possible.
659
+ */
660
+ submenuTemplate = input(null, ...(ngDevMode ? [{ debugName: "submenuTemplate" }] : []));
661
+ markedInlineTemplate;
662
+ inlineTemplate;
663
+ overlaySubmenuTemplate;
664
+ submenuRef = null;
665
+ isOpen = false;
666
+ submenuTemplateToRender() {
667
+ const template = this.markedInlineTemplate?.templateRef ?? this.inlineTemplate ?? this.submenuTemplate();
668
+ if (!template) {
669
+ throw new Error('CoarSubmenuItemComponent: missing submenu content. Provide either an inline <ng-template> child or set [submenuTemplate].');
670
+ }
671
+ return template;
672
+ }
673
+ onMouseEnter(event) {
674
+ if (this.disabled()) {
675
+ return;
676
+ }
677
+ // Defensive: sync state with actual overlay status
678
+ if (this.submenuRef) {
679
+ if (this.submenuRef.isClosed) {
680
+ // Overlay is closed, clear everything
681
+ this.submenuRef = null;
682
+ this.isOpen = false;
683
+ this.cdr.markForCheck();
684
+ }
685
+ else if (!this.isOpen) {
686
+ // Overlay is open but state is wrong - fix it
687
+ this.isOpen = true;
688
+ this.cdr.markForCheck();
689
+ }
690
+ }
691
+ // Open submenu if not already open
692
+ if (!this.submenuRef) {
693
+ const anchor = event.currentTarget;
694
+ // Menu-aim: when a different sibling submenu is already open, delay switching
695
+ // if the pointer trajectory suggests the user is heading into the open submenu panel.
696
+ const parent = this.cascade.parent;
697
+ if (parent) {
698
+ parent.requestOpenFromChild(this.cascade, () => this.openSubmenu(anchor), {
699
+ x: event.clientX,
700
+ y: event.clientY,
701
+ });
702
+ }
703
+ else {
704
+ this.openSubmenu(anchor);
705
+ }
706
+ }
707
+ }
708
+ onClick(event) {
709
+ if (this.disabled()) {
710
+ event.preventDefault();
711
+ event.stopPropagation();
712
+ return;
713
+ }
714
+ if (this.submenuRef?.isClosed) {
715
+ this.submenuRef = null;
716
+ this.isOpen = false;
717
+ this.cdr.markForCheck();
718
+ }
719
+ // For accessibility: toggle on click/Enter/Space
720
+ if (this.submenuRef) {
721
+ this.closeSubmenu();
722
+ }
723
+ else {
724
+ this.openSubmenu(event.currentTarget);
725
+ }
726
+ }
727
+ onKeyboardActivate(event) {
728
+ if (this.disabled()) {
729
+ event.preventDefault();
730
+ event.stopPropagation();
731
+ return;
732
+ }
733
+ if (this.submenuRef?.isClosed) {
734
+ this.submenuRef = null;
735
+ this.isOpen = false;
736
+ this.cdr.markForCheck();
737
+ }
738
+ const target = event.currentTarget;
739
+ if (this.submenuRef) {
740
+ this.closeSubmenu();
741
+ }
742
+ else {
743
+ this.openSubmenu(target);
744
+ }
745
+ }
746
+ openSubmenu(anchorElement) {
747
+ // Validate early so the error points at the submenu item usage.
748
+ this.submenuTemplateToRender();
749
+ // All overlays use hoverTree preset for proper tree tracking
750
+ const spec = Overlay.define((b) => {
751
+ b.content((c) => c.fromTemplate(this.overlaySubmenuTemplate));
752
+ b.anchor({ kind: 'element', element: anchorElement });
753
+ b.position({ placement: ['right-start', 'left-start'], offset: -4, flip: true, shift: true });
754
+ }, coarHoverMenuPreset);
755
+ // Prefer the cascade parent's overlayRef when available.
756
+ // With Angular content projection, submenu content can be instantiated in the *root* overlay
757
+ // injector even when it's rendered inside a child overlay. The cascade chain preserves the
758
+ // correct containing overlay via overlayRef.
759
+ const containingOverlay = this.cascade.parent?.overlayRef ?? this.parentOverlay;
760
+ if (containingOverlay) {
761
+ // Inside an overlay: close siblings (direct children of parent) then open as child
762
+ // First, create the child overlay
763
+ this.submenuRef = this.overlayService.openChild(containingOverlay, spec, undefined);
764
+ // Then close siblings, excluding the newly opened one
765
+ containingOverlay.closeChildren(this.submenuRef);
766
+ }
767
+ else {
768
+ // Inline menu: use cascade-level sibling closure
769
+ this.cascade.closeSiblings();
770
+ this.submenuRef = this.overlayService.open(spec, undefined);
771
+ }
772
+ // Expose the overlay ref to descendants so they can parent their own flyouts correctly.
773
+ this.cascade.overlayRef = this.submenuRef;
774
+ this.cascade.parent?.notifyChildOpened(this.cascade);
775
+ this.isOpen = true;
776
+ this.cdr.markForCheck();
777
+ const openedRef = this.submenuRef;
778
+ // Clear the active styling as soon as a close is initiated (e.g. hoverTree timer,
779
+ // outside click, or sibling submenu switch). Otherwise the item can look "stuck"
780
+ // while the overlay finishes its close transition.
781
+ const originalClose = openedRef.close.bind(openedRef);
782
+ openedRef.close = (result) => {
783
+ if (this.submenuRef === openedRef && this.isOpen) {
784
+ this.isOpen = false;
785
+ this.cdr.markForCheck();
786
+ }
787
+ originalClose(result);
788
+ };
789
+ openedRef.afterClosed$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
790
+ // Only clear state if this is still the current overlay
791
+ // (prevents race condition when rapidly opening new overlays)
792
+ if (this.submenuRef === openedRef) {
793
+ this.submenuRef = null;
794
+ this.isOpen = false;
795
+ this.cdr.markForCheck();
796
+ }
797
+ if (this.cascade.overlayRef === openedRef) {
798
+ this.cascade.overlayRef = null;
799
+ }
800
+ this.cascade.parent?.notifyChildClosed(this.cascade);
801
+ });
802
+ }
803
+ // Parent overlay is resolved via COAR_OVERLAY_REF when rendered inside an overlay.
804
+ closeSubmenu() {
805
+ this.submenuRef?.close();
806
+ this.submenuRef = null;
807
+ this.isOpen = false;
808
+ this.cdr.markForCheck();
809
+ }
810
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubmenuItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
811
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.6", type: CoarSubmenuItemComponent, isStandalone: true, selector: "coar-submenu-item, coar-sub-flyout", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, submenuTemplate: { classPropertyName: "submenuTemplate", publicName: "submenuTemplate", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
812
+ {
813
+ provide: COAR_MENU_CASCADE,
814
+ useFactory: () => new CoarMenuCascade(inject(COAR_MENU_CASCADE, { optional: true, skipSelf: true })),
815
+ },
816
+ ], queries: [{ propertyName: "markedInlineTemplate", first: true, predicate: CoarSubmenuTemplateDirective }, { propertyName: "inlineTemplate", first: true, predicate: TemplateRef }], viewQueries: [{ propertyName: "overlaySubmenuTemplate", first: true, predicate: ["overlaySubmenuTemplate"], descendants: true }], ngImport: i0, template: `
817
+ <div
818
+ class="coar-submenu-item"
819
+ [class.coar-submenu-item--disabled]="disabled()"
820
+ [class.coar-submenu-item--open]="isOpen"
821
+ [attr.role]="'menuitem'"
822
+ [attr.aria-haspopup]="'menu'"
823
+ [attr.aria-expanded]="isOpen"
824
+ [attr.aria-disabled]="disabled()"
825
+ [attr.tabindex]="disabled() ? -1 : 0"
826
+ (mouseenter)="onMouseEnter($event)"
827
+ (click)="onClick($event)"
828
+ (keydown.enter)="onKeyboardActivate($event)"
829
+ (keydown.space)="onKeyboardActivate($event)"
830
+ >
831
+ <span class="coar-submenu-item__icon" aria-hidden="true">
832
+ <coar-icon [name]="icon() || 'square-rounded-dashed'" size="sm" aria-hidden="true" />
833
+ </span>
834
+ <span class="coar-submenu-item__label">{{ label() }}</span>
835
+ <coar-icon
836
+ name="chevron-right"
837
+ size="xs"
838
+ class="coar-submenu-item__arrow"
839
+ aria-hidden="true"
840
+ />
841
+ </div>
842
+
843
+ <ng-template #overlaySubmenuTemplate>
844
+ <ng-container
845
+ *ngTemplateOutlet="submenuTemplateToRender(); injector: submenuTemplateInjector"
846
+ />
847
+ </ng-template>
848
+ `, isInline: true, styles: [":host{display:block}.coar-submenu-item{display:flex;align-items:center;gap:var(--coar-menu-item-gap, .75rem);width:100%;box-sizing:border-box;padding:var(--coar-menu-item-padding, .5rem .75rem);font-family:var(--coar-menu-item-font-family, var(--coar-font-family-body, Poppins));font-size:var( --coar-menu-item-font-size, var(--coar-component-md-font-size, var(--coar-font-size-xs, 14px)) );font-weight:var(--coar-menu-item-font-weight, var(--coar-font-weight-regular, 400));line-height:var(--coar-menu-item-line-height, 1.5);color:var(--coar-menu-item-color, var(--coar-text-neutral-primary, #545454));background:var(--coar-menu-item-background, transparent);border-radius:0;position:relative;cursor:pointer;-webkit-user-select:none;user-select:none;transition:var(--coar-transition-fast, all .1s ease);outline:none}.coar-submenu-item:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:1px;background:var(--coar-menu-item-border-left-color, #d0d0d0);opacity:0;pointer-events:none;transition:width var(--coar-duration-fast) var(--coar-ease-out),background var(--coar-duration-fast) var(--coar-ease-out)}.coar-submenu-item:hover:not(.coar-submenu-item--disabled){background:var(--coar-menu-item-background-hover, #f0f1f2);color:var(--coar-menu-item-color-hover, var(--coar-text-neutral-primary, #545454))}.coar-submenu-item--open{background:var( --coar-menu-item-background-open, var(--coar-menu-item-background-active, #f0f1f2) )}.coar-submenu-item--open:before{width:2px;background:var( --coar-menu-item-border-left-color-active, var(--coar-border-accent-primary, #156db7) );opacity:1}:host-context(.coar-sub-expand__panel-inner) .coar-submenu-item:before{opacity:1}:host-context(.coar-menu--in-overlay) .coar-submenu-item:before{opacity:1}.coar-submenu-item:focus-visible{background:var(--coar-menu-item-background-focus, #f0f1f2);outline:2px solid var(--coar-menu-item-outline-focus, var(--coar-border-accent-primary, #156db7));outline-offset:-2px}.coar-submenu-item--disabled{color:var(--coar-menu-item-color-disabled, var(--coar-text-neutral-disabled, #999999));cursor:not-allowed;opacity:.6}.coar-submenu-item__icon{flex-shrink:0;display:var(--coar-menu-icon-slot-display, inline-flex);align-items:center;justify-content:center;width:var(--coar-menu-item-icon-slot-size, 16px);height:var(--coar-menu-item-icon-slot-size, 16px);opacity:1}.coar-submenu-item__icon:has(coar-icon[icon-name=square-rounded-dashed]){opacity:.15}.coar-submenu-item__label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.coar-submenu-item__arrow{flex-shrink:0;margin-left:auto;opacity:.6;transition:var(--coar-transition-fast, all .1s ease)}.coar-submenu-item:hover:not(.coar-submenu-item--disabled) .coar-submenu-item__arrow{opacity:1}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: CoarIconComponent, selector: "coar-icon", inputs: ["name", "size", "rotate", "rotateTransition", "spin", "color", "label", "fallback"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
849
+ }
850
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubmenuItemComponent, decorators: [{
851
+ type: Component,
852
+ args: [{ selector: 'coar-submenu-item, coar-sub-flyout', standalone: true, imports: [CommonModule, CoarIconComponent], template: `
853
+ <div
854
+ class="coar-submenu-item"
855
+ [class.coar-submenu-item--disabled]="disabled()"
856
+ [class.coar-submenu-item--open]="isOpen"
857
+ [attr.role]="'menuitem'"
858
+ [attr.aria-haspopup]="'menu'"
859
+ [attr.aria-expanded]="isOpen"
860
+ [attr.aria-disabled]="disabled()"
861
+ [attr.tabindex]="disabled() ? -1 : 0"
862
+ (mouseenter)="onMouseEnter($event)"
863
+ (click)="onClick($event)"
864
+ (keydown.enter)="onKeyboardActivate($event)"
865
+ (keydown.space)="onKeyboardActivate($event)"
866
+ >
867
+ <span class="coar-submenu-item__icon" aria-hidden="true">
868
+ <coar-icon [name]="icon() || 'square-rounded-dashed'" size="sm" aria-hidden="true" />
869
+ </span>
870
+ <span class="coar-submenu-item__label">{{ label() }}</span>
871
+ <coar-icon
872
+ name="chevron-right"
873
+ size="xs"
874
+ class="coar-submenu-item__arrow"
875
+ aria-hidden="true"
876
+ />
877
+ </div>
878
+
879
+ <ng-template #overlaySubmenuTemplate>
880
+ <ng-container
881
+ *ngTemplateOutlet="submenuTemplateToRender(); injector: submenuTemplateInjector"
882
+ />
883
+ </ng-template>
884
+ `, changeDetection: ChangeDetectionStrategy.OnPush, providers: [
885
+ {
886
+ provide: COAR_MENU_CASCADE,
887
+ useFactory: () => new CoarMenuCascade(inject(COAR_MENU_CASCADE, { optional: true, skipSelf: true })),
888
+ },
889
+ ], styles: [":host{display:block}.coar-submenu-item{display:flex;align-items:center;gap:var(--coar-menu-item-gap, .75rem);width:100%;box-sizing:border-box;padding:var(--coar-menu-item-padding, .5rem .75rem);font-family:var(--coar-menu-item-font-family, var(--coar-font-family-body, Poppins));font-size:var( --coar-menu-item-font-size, var(--coar-component-md-font-size, var(--coar-font-size-xs, 14px)) );font-weight:var(--coar-menu-item-font-weight, var(--coar-font-weight-regular, 400));line-height:var(--coar-menu-item-line-height, 1.5);color:var(--coar-menu-item-color, var(--coar-text-neutral-primary, #545454));background:var(--coar-menu-item-background, transparent);border-radius:0;position:relative;cursor:pointer;-webkit-user-select:none;user-select:none;transition:var(--coar-transition-fast, all .1s ease);outline:none}.coar-submenu-item:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:1px;background:var(--coar-menu-item-border-left-color, #d0d0d0);opacity:0;pointer-events:none;transition:width var(--coar-duration-fast) var(--coar-ease-out),background var(--coar-duration-fast) var(--coar-ease-out)}.coar-submenu-item:hover:not(.coar-submenu-item--disabled){background:var(--coar-menu-item-background-hover, #f0f1f2);color:var(--coar-menu-item-color-hover, var(--coar-text-neutral-primary, #545454))}.coar-submenu-item--open{background:var( --coar-menu-item-background-open, var(--coar-menu-item-background-active, #f0f1f2) )}.coar-submenu-item--open:before{width:2px;background:var( --coar-menu-item-border-left-color-active, var(--coar-border-accent-primary, #156db7) );opacity:1}:host-context(.coar-sub-expand__panel-inner) .coar-submenu-item:before{opacity:1}:host-context(.coar-menu--in-overlay) .coar-submenu-item:before{opacity:1}.coar-submenu-item:focus-visible{background:var(--coar-menu-item-background-focus, #f0f1f2);outline:2px solid var(--coar-menu-item-outline-focus, var(--coar-border-accent-primary, #156db7));outline-offset:-2px}.coar-submenu-item--disabled{color:var(--coar-menu-item-color-disabled, var(--coar-text-neutral-disabled, #999999));cursor:not-allowed;opacity:.6}.coar-submenu-item__icon{flex-shrink:0;display:var(--coar-menu-icon-slot-display, inline-flex);align-items:center;justify-content:center;width:var(--coar-menu-item-icon-slot-size, 16px);height:var(--coar-menu-item-icon-slot-size, 16px);opacity:1}.coar-submenu-item__icon:has(coar-icon[icon-name=square-rounded-dashed]){opacity:.15}.coar-submenu-item__label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.coar-submenu-item__arrow{flex-shrink:0;margin-left:auto;opacity:.6;transition:var(--coar-transition-fast, all .1s ease)}.coar-submenu-item:hover:not(.coar-submenu-item--disabled) .coar-submenu-item__arrow{opacity:1}\n"] }]
890
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], submenuTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "submenuTemplate", required: false }] }], markedInlineTemplate: [{
891
+ type: ContentChild,
892
+ args: [CoarSubmenuTemplateDirective, { descendants: false }]
893
+ }], inlineTemplate: [{
894
+ type: ContentChild,
895
+ args: [TemplateRef, { descendants: false }]
896
+ }], overlaySubmenuTemplate: [{
897
+ type: ViewChild,
898
+ args: ['overlaySubmenuTemplate']
899
+ }] } });
900
+
901
+ /**
902
+ * CoarSubExpand: Menu item that expands/collapses a submenu inline.
903
+ *
904
+ * Use this variant when you want nested options to stay visible (e.g. sidebar panels)
905
+ * while still reusing the same menu item types inside the submenu.
906
+ *
907
+ * @example
908
+ * ```html
909
+ * <coar-menu>
910
+ * <coar-sub-expand label="Filters" icon="settings" [(open)]="filtersOpen">
911
+ * <ng-template>
912
+ * <coar-menu>
913
+ * <coar-menu-item icon="plus">Add Filter</coar-menu-item>
914
+ * <coar-menu-item icon="trash">Clear</coar-menu-item>
915
+ * </coar-menu>
916
+ * </ng-template>
917
+ </coar-sub-expand>
918
+ * </coar-menu>
919
+ * ```
920
+ */
921
+ class CoarSubExpandComponent {
922
+ /** Label text for the menu item */
923
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
924
+ /** Optional icon identifier */
925
+ icon = input(undefined, ...(ngDevMode ? [{ debugName: "icon" }] : []));
926
+ /** Disabled state prevents interaction */
927
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
928
+ /** Expanded state (two-way bindable with [(open)]) */
929
+ open = input(undefined, { ...(ngDevMode ? { debugName: "open" } : {}), transform: booleanAttribute });
930
+ /** Emits when expanded state changes (for [(open)]) */
931
+ openChange = output();
932
+ openInternal = signal(false, ...(ngDevMode ? [{ debugName: "openInternal" }] : []));
933
+ isOpen = computed(() => this.open() ?? this.openInternal(), ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
934
+ /** Optional external submenu template. Prefer an inline `<ng-template>` child when possible. */
935
+ submenuTemplate = input(null, ...(ngDevMode ? [{ debugName: "submenuTemplate" }] : []));
936
+ markedInlineTemplate;
937
+ inlineTemplate;
938
+ submenuTemplateToRender() {
939
+ const template = this.markedInlineTemplate?.templateRef ?? this.inlineTemplate ?? this.submenuTemplate();
940
+ if (!template) {
941
+ throw new Error('CoarSubExpandComponent: missing submenu content. Provide either an inline <ng-template> child or set [submenuTemplate].');
942
+ }
943
+ return template;
944
+ }
945
+ constructor() {
946
+ effect(() => {
947
+ const value = this.open();
948
+ if (value === undefined)
949
+ return;
950
+ this.openInternal.set(value);
951
+ });
952
+ }
953
+ toggle(event) {
954
+ if (this.disabled()) {
955
+ event.preventDefault();
956
+ event.stopPropagation();
957
+ return;
958
+ }
959
+ const next = !this.isOpen();
960
+ this.openInternal.set(next);
961
+ this.openChange.emit(next);
962
+ }
963
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubExpandComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
964
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.6", type: CoarSubExpandComponent, isStandalone: true, selector: "coar-sub-expand", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, submenuTemplate: { classPropertyName: "submenuTemplate", publicName: "submenuTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { openChange: "openChange" }, queries: [{ propertyName: "markedInlineTemplate", first: true, predicate: CoarSubmenuTemplateDirective }, { propertyName: "inlineTemplate", first: true, predicate: TemplateRef }], ngImport: i0, template: `
965
+ <div
966
+ class="coar-sub-expand"
967
+ [class.coar-sub-expand--disabled]="disabled()"
968
+ [class.coar-sub-expand--open]="isOpen()"
969
+ [attr.role]="'menuitem'"
970
+ [attr.aria-haspopup]="'menu'"
971
+ [attr.aria-expanded]="isOpen()"
972
+ [attr.aria-disabled]="disabled()"
973
+ [attr.tabindex]="disabled() ? -1 : 0"
974
+ (click)="toggle($event)"
975
+ (keydown.enter)="toggle($event)"
976
+ (keydown.space)="toggle($event)"
977
+ >
978
+ <span class="coar-sub-expand__icon" aria-hidden="true">
979
+ <coar-icon [name]="icon() || 'square-rounded-dashed'" size="sm" aria-hidden="true" />
980
+ </span>
981
+ <span class="coar-sub-expand__label">{{ label() }}</span>
982
+ <coar-icon
983
+ [name]="isOpen() ? 'minus' : 'plus'"
984
+ size="xs"
985
+ class="coar-sub-expand__arrow"
986
+ aria-hidden="true"
987
+ />
988
+ </div>
989
+
990
+ <div
991
+ class="coar-sub-expand__panel"
992
+ [class.coar-sub-expand__panel--open]="isOpen()"
993
+ [attr.aria-hidden]="isOpen() ? null : 'true'"
994
+ role="group"
995
+ >
996
+ <div class="coar-sub-expand__panel-inner">
997
+ <ng-container *ngTemplateOutlet="submenuTemplateToRender()" />
998
+ </div>
999
+ </div>
1000
+ `, isInline: true, styles: [":host{display:block}.coar-sub-expand{display:flex;align-items:center;gap:var(--coar-menu-item-gap, .75rem);width:100%;box-sizing:border-box;padding:var(--coar-menu-item-padding, .5rem .75rem);font-family:var(--coar-menu-item-font-family, var(--coar-font-family-body, Poppins));font-size:var( --coar-menu-item-font-size, var(--coar-component-md-font-size, var(--coar-font-size-xs, 14px)) );font-weight:var(--coar-menu-item-font-weight, var(--coar-font-weight-regular, 400));line-height:var(--coar-menu-item-line-height, 1.5);color:var(--coar-menu-item-color, var(--coar-text-neutral-primary, #545454));background:var(--coar-menu-item-background, transparent);border-radius:0;position:relative;cursor:pointer;-webkit-user-select:none;user-select:none;transition:var(--coar-transition-fast, all .1s ease);outline:none}.coar-sub-expand:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:1px;background:var(--coar-menu-item-border-left-color, #d0d0d0);opacity:0;pointer-events:none;transition:width var(--coar-duration-fast) var(--coar-ease-out),background var(--coar-duration-fast) var(--coar-ease-out)}.coar-sub-expand:hover:not(.coar-sub-expand--disabled){background:var(--coar-menu-item-background-hover, #f0f1f2);color:var(--coar-menu-item-color-hover, var(--coar-text-neutral-primary, #545454))}.coar-sub-expand--open{background:var( --coar-menu-item-background-open, var(--coar-menu-item-background-active, #f0f1f2) )}.coar-sub-expand--open:before{width:2px;background:var( --coar-menu-item-border-left-color-active, var(--coar-border-accent-primary, #156db7) );opacity:1}:host-context(.coar-sub-expand__panel-inner) .coar-sub-expand:before{opacity:1}:host-context(.coar-menu--in-overlay) .coar-sub-expand:before{opacity:1}.coar-sub-expand:focus-visible{background:var(--coar-menu-item-background-focus, #f0f1f2);outline:2px solid var(--coar-menu-item-outline-focus, var(--coar-border-accent-primary, #156db7));outline-offset:-2px}.coar-sub-expand--disabled{color:var(--coar-menu-item-color-disabled, var(--coar-text-neutral-disabled, #999999));cursor:not-allowed;opacity:.6}.coar-sub-expand__icon{flex-shrink:0;display:var(--coar-menu-icon-slot-display, inline-flex);align-items:center;justify-content:center;width:var(--coar-menu-item-icon-slot-size, 16px);height:var(--coar-menu-item-icon-slot-size, 16px);opacity:1}.coar-sub-expand__icon:has(coar-icon[icon-name=square-rounded-dashed]){opacity:.15}.coar-sub-expand__label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.coar-sub-expand__arrow{flex-shrink:0;margin-left:auto;opacity:.6;transition:var(--coar-transition-fast, all .1s ease)}.coar-sub-expand:hover:not(.coar-sub-expand--disabled) .coar-sub-expand__arrow{opacity:1}.coar-sub-expand__panel{box-sizing:border-box;margin-left:var(--coar-sub-expand-indent-offset, 16px);position:relative;display:grid;grid-template-rows:0fr;overflow:hidden;transition:grid-template-rows var(--coar-duration-normal, .2s) var(--coar-ease-out, ease)}.coar-sub-expand__panel--open{grid-template-rows:1fr}.coar-sub-expand__panel-inner{overflow:hidden;min-height:0;padding-top:0;padding-bottom:0;opacity:0;transform:translateY(-2px);transition:opacity var(--coar-duration-fast, .1s) var(--coar-ease-out, ease),transform var(--coar-duration-fast, .1s) var(--coar-ease-out, ease)}.coar-sub-expand__panel--open>.coar-sub-expand__panel-inner{padding-top:var(--coar-sub-expand-panel-padding-y, var(--coar-spacing-1, .25rem));padding-bottom:var(--coar-sub-expand-panel-padding-y, var(--coar-spacing-1, .25rem));opacity:1;transform:translateY(0)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: CoarIconComponent, selector: "coar-icon", inputs: ["name", "size", "rotate", "rotateTransition", "spin", "color", "label", "fallback"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1001
+ }
1002
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubExpandComponent, decorators: [{
1003
+ type: Component,
1004
+ args: [{ selector: 'coar-sub-expand', standalone: true, imports: [CommonModule, CoarIconComponent], template: `
1005
+ <div
1006
+ class="coar-sub-expand"
1007
+ [class.coar-sub-expand--disabled]="disabled()"
1008
+ [class.coar-sub-expand--open]="isOpen()"
1009
+ [attr.role]="'menuitem'"
1010
+ [attr.aria-haspopup]="'menu'"
1011
+ [attr.aria-expanded]="isOpen()"
1012
+ [attr.aria-disabled]="disabled()"
1013
+ [attr.tabindex]="disabled() ? -1 : 0"
1014
+ (click)="toggle($event)"
1015
+ (keydown.enter)="toggle($event)"
1016
+ (keydown.space)="toggle($event)"
1017
+ >
1018
+ <span class="coar-sub-expand__icon" aria-hidden="true">
1019
+ <coar-icon [name]="icon() || 'square-rounded-dashed'" size="sm" aria-hidden="true" />
1020
+ </span>
1021
+ <span class="coar-sub-expand__label">{{ label() }}</span>
1022
+ <coar-icon
1023
+ [name]="isOpen() ? 'minus' : 'plus'"
1024
+ size="xs"
1025
+ class="coar-sub-expand__arrow"
1026
+ aria-hidden="true"
1027
+ />
1028
+ </div>
1029
+
1030
+ <div
1031
+ class="coar-sub-expand__panel"
1032
+ [class.coar-sub-expand__panel--open]="isOpen()"
1033
+ [attr.aria-hidden]="isOpen() ? null : 'true'"
1034
+ role="group"
1035
+ >
1036
+ <div class="coar-sub-expand__panel-inner">
1037
+ <ng-container *ngTemplateOutlet="submenuTemplateToRender()" />
1038
+ </div>
1039
+ </div>
1040
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{display:block}.coar-sub-expand{display:flex;align-items:center;gap:var(--coar-menu-item-gap, .75rem);width:100%;box-sizing:border-box;padding:var(--coar-menu-item-padding, .5rem .75rem);font-family:var(--coar-menu-item-font-family, var(--coar-font-family-body, Poppins));font-size:var( --coar-menu-item-font-size, var(--coar-component-md-font-size, var(--coar-font-size-xs, 14px)) );font-weight:var(--coar-menu-item-font-weight, var(--coar-font-weight-regular, 400));line-height:var(--coar-menu-item-line-height, 1.5);color:var(--coar-menu-item-color, var(--coar-text-neutral-primary, #545454));background:var(--coar-menu-item-background, transparent);border-radius:0;position:relative;cursor:pointer;-webkit-user-select:none;user-select:none;transition:var(--coar-transition-fast, all .1s ease);outline:none}.coar-sub-expand:before{content:\"\";position:absolute;left:0;top:0;bottom:0;width:1px;background:var(--coar-menu-item-border-left-color, #d0d0d0);opacity:0;pointer-events:none;transition:width var(--coar-duration-fast) var(--coar-ease-out),background var(--coar-duration-fast) var(--coar-ease-out)}.coar-sub-expand:hover:not(.coar-sub-expand--disabled){background:var(--coar-menu-item-background-hover, #f0f1f2);color:var(--coar-menu-item-color-hover, var(--coar-text-neutral-primary, #545454))}.coar-sub-expand--open{background:var( --coar-menu-item-background-open, var(--coar-menu-item-background-active, #f0f1f2) )}.coar-sub-expand--open:before{width:2px;background:var( --coar-menu-item-border-left-color-active, var(--coar-border-accent-primary, #156db7) );opacity:1}:host-context(.coar-sub-expand__panel-inner) .coar-sub-expand:before{opacity:1}:host-context(.coar-menu--in-overlay) .coar-sub-expand:before{opacity:1}.coar-sub-expand:focus-visible{background:var(--coar-menu-item-background-focus, #f0f1f2);outline:2px solid var(--coar-menu-item-outline-focus, var(--coar-border-accent-primary, #156db7));outline-offset:-2px}.coar-sub-expand--disabled{color:var(--coar-menu-item-color-disabled, var(--coar-text-neutral-disabled, #999999));cursor:not-allowed;opacity:.6}.coar-sub-expand__icon{flex-shrink:0;display:var(--coar-menu-icon-slot-display, inline-flex);align-items:center;justify-content:center;width:var(--coar-menu-item-icon-slot-size, 16px);height:var(--coar-menu-item-icon-slot-size, 16px);opacity:1}.coar-sub-expand__icon:has(coar-icon[icon-name=square-rounded-dashed]){opacity:.15}.coar-sub-expand__label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.coar-sub-expand__arrow{flex-shrink:0;margin-left:auto;opacity:.6;transition:var(--coar-transition-fast, all .1s ease)}.coar-sub-expand:hover:not(.coar-sub-expand--disabled) .coar-sub-expand__arrow{opacity:1}.coar-sub-expand__panel{box-sizing:border-box;margin-left:var(--coar-sub-expand-indent-offset, 16px);position:relative;display:grid;grid-template-rows:0fr;overflow:hidden;transition:grid-template-rows var(--coar-duration-normal, .2s) var(--coar-ease-out, ease)}.coar-sub-expand__panel--open{grid-template-rows:1fr}.coar-sub-expand__panel-inner{overflow:hidden;min-height:0;padding-top:0;padding-bottom:0;opacity:0;transform:translateY(-2px);transition:opacity var(--coar-duration-fast, .1s) var(--coar-ease-out, ease),transform var(--coar-duration-fast, .1s) var(--coar-ease-out, ease)}.coar-sub-expand__panel--open>.coar-sub-expand__panel-inner{padding-top:var(--coar-sub-expand-panel-padding-y, var(--coar-spacing-1, .25rem));padding-bottom:var(--coar-sub-expand-panel-padding-y, var(--coar-spacing-1, .25rem));opacity:1;transform:translateY(0)}\n"] }]
1041
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }], openChange: [{ type: i0.Output, args: ["openChange"] }], submenuTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "submenuTemplate", required: false }] }], markedInlineTemplate: [{
1042
+ type: ContentChild,
1043
+ args: [CoarSubmenuTemplateDirective, { descendants: false }]
1044
+ }], inlineTemplate: [{
1045
+ type: ContentChild,
1046
+ args: [TemplateRef, { descendants: false }]
1047
+ }] } });
1048
+
1049
+ /**
1050
+ * Generated bundle index. Do not edit.
1051
+ */
1052
+
1053
+ export { COAR_MENU_AIM_CONFIG, CoarMenuAimConfigDirective, CoarMenuComponent, CoarMenuDividerComponent, CoarMenuHeadingComponent, CoarMenuItemComponent, CoarSubExpandComponent, CoarSubmenuItemComponent, CoarSubmenuTemplateDirective, DEFAULT_COAR_MENU_AIM_CONFIG };
1054
+ //# sourceMappingURL=cocoar-ui-menu.mjs.map