@cocoar/ui 0.1.0-beta.99 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +21 -11
  2. package/fesm2022/cocoar-ui-components.mjs +9549 -0
  3. package/fesm2022/cocoar-ui-components.mjs.map +1 -0
  4. package/fesm2022/cocoar-ui-menu.mjs +1082 -0
  5. package/fesm2022/cocoar-ui-menu.mjs.map +1 -0
  6. package/fesm2022/cocoar-ui-overlay.mjs +1284 -0
  7. package/fesm2022/cocoar-ui-overlay.mjs.map +1 -0
  8. package/fesm2022/cocoar-ui.mjs +8 -0
  9. package/fesm2022/cocoar-ui.mjs.map +1 -0
  10. package/llms-full.txt +2303 -0
  11. package/llms.txt +82 -0
  12. package/package.json +38 -19
  13. package/styles/all.css +9 -0
  14. package/styles/components.css +127 -0
  15. package/styles/tokens/all.css +38 -0
  16. package/styles/tokens/code-block.css +72 -0
  17. package/styles/tokens/colors-primitives-dark.css +84 -0
  18. package/styles/tokens/colors-primitives-light.css +75 -0
  19. package/styles/tokens/colors-usage.css +272 -0
  20. package/styles/tokens/components-shared.css +42 -0
  21. package/styles/tokens/elevation.css +30 -0
  22. package/styles/tokens/focus.css +30 -0
  23. package/styles/tokens/layers.css +17 -0
  24. package/styles/tokens/menu.css +53 -0
  25. package/styles/tokens/motion.css +93 -0
  26. package/styles/tokens/new-components.css +104 -0
  27. package/styles/tokens/radius.css +15 -0
  28. package/styles/tokens/select-overlay.css +40 -0
  29. package/styles/tokens/shadows.css +38 -0
  30. package/styles/tokens/sidebar.css +67 -0
  31. package/styles/tokens/spacing.css +16 -0
  32. package/styles/tokens/stroke-width.css +12 -0
  33. package/styles/tokens/type-primitives.css +23 -0
  34. package/styles/tokens/typography-responsive.css +44 -0
  35. package/styles/tokens/typography.css +41 -0
  36. package/types/cocoar-ui-components.d.ts +3719 -0
  37. package/types/cocoar-ui-menu.d.ts +326 -0
  38. package/types/cocoar-ui-overlay.d.ts +301 -0
  39. package/types/cocoar-ui.d.ts +3 -0
  40. package/src/index.d.ts +0 -4
  41. package/src/index.d.ts.map +0 -1
  42. package/src/index.js +0 -5
  43. package/src/index.js.map +0 -1
@@ -0,0 +1,1082 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, input, Directive, booleanAttribute, ChangeDetectionStrategy, Component, output, HostListener, TemplateRef, DestroyRef, ChangeDetectorRef, Injector, ViewChild, ContentChild, signal, computed, effect } from '@angular/core';
3
+ import { COAR_OVERLAY_REF, COAR_MENU_PARENT, createOverlayBuilder, 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 overlay content (via `createOverlayBuilder`) 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
+ * Removes border, background, border-radius, and shadow for seamless embedding in containers.
326
+ *
327
+ * Use when embedding menu in sidebars, panels, or custom containers that provide their own styling.
328
+ * Use as boolean attribute: `<coar-menu borderless>` or `[borderless]="true"`
329
+ */
330
+ borderless = input(false, { ...(ngDevMode ? { debugName: "borderless" } : {}), transform: booleanAttribute });
331
+ /**
332
+ * Check if this menu is rendered inside an overlay (flyout).
333
+ * Used to enable border visibility for menu items.
334
+ */
335
+ isInOverlay() {
336
+ return this.overlayRef !== null;
337
+ }
338
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
339
+ 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 }, borderless: { classPropertyName: "borderless", publicName: "borderless", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "menu" }, properties: { "class.coar-menu--in-overlay": "isInOverlay()", "class.coar-menu--borderless": "borderless()", "style.--coar-menu-icon-slot-display": "showIconColumn() ? null : \"none\"", "style.--coar-menu-item-icon-slot-size": "showIconColumn() ? null : \"0px\"" }, classAttribute: "coar-menu" }, providers: [
340
+ {
341
+ provide: COAR_MENU_CASCADE,
342
+ useFactory: () => {
343
+ // Check if we're inside an overlay
344
+ const inOverlay = inject(COAR_OVERLAY_REF, { optional: true });
345
+ const parentCascade = inject(COAR_MENU_CASCADE, { optional: true, skipSelf: true });
346
+ if (inOverlay && parentCascade) {
347
+ // We're a menu inside a submenu overlay
348
+ // Create a new cascade as a child of the parent, so sibling submenu-items
349
+ // at this level can track each other
350
+ return new CoarMenuCascade(parentCascade);
351
+ }
352
+ // For root menus (inline or context menu), create a new root cascade
353
+ return new CoarMenuCascade(parentCascade);
354
+ },
355
+ },
356
+ ], 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))}:host(.coar-menu--borderless){background:transparent;border:none;border-radius:0;box-shadow:none;min-width:unset;max-width:unset}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
357
+ }
358
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuComponent, decorators: [{
359
+ type: Component,
360
+ args: [{ selector: 'coar-menu', standalone: true, imports: [], hostDirectives: [
361
+ {
362
+ directive: CoarMenuAimConfigDirective,
363
+ inputs: ['aimEnabled', 'aimDebugEnabled', 'aimSwitchDelayMs', 'aimSampleMaxAgeMs'],
364
+ },
365
+ ], changeDetection: ChangeDetectionStrategy.OnPush, host: {
366
+ role: 'menu',
367
+ class: 'coar-menu',
368
+ '[class.coar-menu--in-overlay]': 'isInOverlay()',
369
+ '[class.coar-menu--borderless]': 'borderless()',
370
+ '[style.--coar-menu-icon-slot-display]': 'showIconColumn() ? null : "none"',
371
+ '[style.--coar-menu-item-icon-slot-size]': 'showIconColumn() ? null : "0px"',
372
+ }, providers: [
373
+ {
374
+ provide: COAR_MENU_CASCADE,
375
+ useFactory: () => {
376
+ // Check if we're inside an overlay
377
+ const inOverlay = inject(COAR_OVERLAY_REF, { optional: true });
378
+ const parentCascade = inject(COAR_MENU_CASCADE, { optional: true, skipSelf: true });
379
+ if (inOverlay && parentCascade) {
380
+ // We're a menu inside a submenu overlay
381
+ // Create a new cascade as a child of the parent, so sibling submenu-items
382
+ // at this level can track each other
383
+ return new CoarMenuCascade(parentCascade);
384
+ }
385
+ // For root menus (inline or context menu), create a new root cascade
386
+ return new CoarMenuCascade(parentCascade);
387
+ },
388
+ },
389
+ ], 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))}:host(.coar-menu--borderless){background:transparent;border:none;border-radius:0;box-shadow:none;min-width:unset;max-width:unset}\n"] }]
390
+ }], propDecorators: { showIconColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "showIconColumn", required: false }] }], borderless: [{ type: i0.Input, args: [{ isSignal: true, alias: "borderless", required: false }] }] } });
391
+
392
+ /**
393
+ * CoarMenuItem: Individual menu item with optional icon and submenu support.
394
+ *
395
+ * Responsibilities:
396
+ * - Render item text and optional icon
397
+ * - Handle click events
398
+ * - Support disabled state
399
+ * - Visual states: hover, active, focus
400
+ * - Trigger submenu (accordion or flyout via parent menu logic)
401
+ *
402
+ * Does NOT manage overlay directly - parent menu handles flyout logic.
403
+ *
404
+ * @example
405
+ * ```html
406
+ * <coar-menu-item (clicked)="onSave()">Save</coar-menu-item>
407
+ * <coar-menu-item icon="copy">Copy</coar-menu-item>
408
+ * <coar-menu-item [disabled]="true">Unavailable</coar-menu-item>
409
+ *
410
+ * <!-- Prevent menu from closing on click: -->
411
+ * <coar-menu-item (clicked)="toggle($event)">Toggle Setting</coar-menu-item>
412
+ *
413
+ * toggle(event: CoarMenuItemClickEvent) {
414
+ * event.keepMenuOpen(); // Keep menu open
415
+ * this.setting = !this.setting;
416
+ * }
417
+ * ```
418
+ */
419
+ class CoarMenuItemComponent {
420
+ parentOverlay = inject(COAR_MENU_PARENT, { optional: true });
421
+ /** Item text content */
422
+ label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : []));
423
+ /** Optional icon identifier (rendered via CoarIconComponent) */
424
+ icon = input(undefined, ...(ngDevMode ? [{ debugName: "icon" }] : []));
425
+ /** Disabled state prevents interaction */
426
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
427
+ /** Emitted when user clicks/selects the item. Menu closes by default unless preventDefault() is called. */
428
+ clicked = output();
429
+ /** Emitted when user hovers over item (for flyout trigger) */
430
+ hovered = output();
431
+ onClick(event) {
432
+ if (this.disabled()) {
433
+ event.preventDefault();
434
+ event.stopPropagation();
435
+ return;
436
+ }
437
+ // Always stop propagation to prevent overlay's outside-click handler
438
+ event.stopPropagation();
439
+ let shouldClose = true;
440
+ const clickEvent = {
441
+ event,
442
+ keepMenuOpen: () => {
443
+ shouldClose = false;
444
+ },
445
+ };
446
+ this.clicked.emit(clickEvent);
447
+ if (shouldClose) {
448
+ this.closeMenuTree();
449
+ }
450
+ }
451
+ onMouseEnter(event) {
452
+ if (!this.disabled()) {
453
+ this.hovered.emit(event);
454
+ }
455
+ }
456
+ onKeyboardActivate() {
457
+ if (this.disabled()) {
458
+ return;
459
+ }
460
+ let shouldClose = true;
461
+ const clickEvent = {
462
+ event: new MouseEvent('click'), // Synthetic event for keyboard
463
+ keepMenuOpen: () => {
464
+ shouldClose = false;
465
+ },
466
+ };
467
+ this.clicked.emit(clickEvent);
468
+ if (shouldClose) {
469
+ this.closeMenuTree();
470
+ }
471
+ }
472
+ closeMenuTree() {
473
+ const parentOverlay = this.parentOverlay;
474
+ if (parentOverlay) {
475
+ queueMicrotask(() => {
476
+ parentOverlay.getRoot().close();
477
+ });
478
+ }
479
+ }
480
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
481
+ 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: { clicked: "clicked", hovered: "hovered" }, 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=\"s\" 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-m-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:var(--coar-menu-item-border-radius, 0);margin:var(--coar-menu-item-margin, 0);position:relative;cursor:pointer;-webkit-user-select:none;user-select:none;transition:background var(--coar-duration-fast, .1s) var(--coar-ease-out, ease);outline:none}:host(:hover:not(.coar-menu-item--disabled)){background:var(--coar-menu-item-background-hover, #f5f5f5);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-hover, #f5f5f5)}:host(.active){background:var(--coar-menu-item-background-hover, #f5f5f5)}:host-context(.coar-sub-expand__panel-inner){opacity:1}:host(:focus-visible){background:var(--coar-menu-item-background-focus, #f5f5f5);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", "source", "size", "rotate", "rotateTransition", "spin", "color", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
482
+ }
483
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuItemComponent, decorators: [{
484
+ type: Component,
485
+ args: [{ selector: 'coar-menu-item', standalone: true, imports: [CoarIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, host: {
486
+ role: 'menuitem',
487
+ class: 'coar-menu-item',
488
+ '[class.coar-menu-item--disabled]': 'disabled()',
489
+ '[attr.aria-disabled]': 'disabled()',
490
+ '[attr.tabindex]': 'disabled() ? -1 : 0',
491
+ }, template: "<span class=\"coar-menu-item__icon\" aria-hidden=\"true\">\n <coar-icon [name]=\"icon() || 'square-rounded-dashed'\" size=\"s\" 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-m-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:var(--coar-menu-item-border-radius, 0);margin:var(--coar-menu-item-margin, 0);position:relative;cursor:pointer;-webkit-user-select:none;user-select:none;transition:background var(--coar-duration-fast, .1s) var(--coar-ease-out, ease);outline:none}:host(:hover:not(.coar-menu-item--disabled)){background:var(--coar-menu-item-background-hover, #f5f5f5);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-hover, #f5f5f5)}:host(.active){background:var(--coar-menu-item-background-hover, #f5f5f5)}:host-context(.coar-sub-expand__panel-inner){opacity:1}:host(:focus-visible){background:var(--coar-menu-item-background-focus, #f5f5f5);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"] }]
492
+ }], 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 }] }], clicked: [{ type: i0.Output, args: ["clicked"] }], hovered: [{ type: i0.Output, args: ["hovered"] }], onClick: [{
493
+ type: HostListener,
494
+ args: ['click', ['$event']]
495
+ }], onMouseEnter: [{
496
+ type: HostListener,
497
+ args: ['mouseenter', ['$event']]
498
+ }], onKeyboardActivate: [{
499
+ type: HostListener,
500
+ args: ['keydown.enter']
501
+ }, {
502
+ type: HostListener,
503
+ args: ['keydown.space']
504
+ }] } });
505
+
506
+ /**
507
+ * CoarMenuDivider: Visual separator between menu items.
508
+ *
509
+ * Simple horizontal line for grouping menu items.
510
+ *
511
+ * @example
512
+ * ```html
513
+ * <coar-menu>
514
+ * <coar-menu-item>Cut</coar-menu-item>
515
+ * <coar-menu-item>Copy</coar-menu-item>
516
+ * <coar-menu-divider></coar-menu-divider>
517
+ * <coar-menu-item>Paste</coar-menu-item>
518
+ * </coar-menu>
519
+ * ```
520
+ */
521
+ class CoarMenuDividerComponent {
522
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuDividerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
523
+ 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 });
524
+ }
525
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuDividerComponent, decorators: [{
526
+ type: Component,
527
+ args: [{ selector: 'coar-menu-divider', standalone: true, imports: [], template: '', changeDetection: ChangeDetectionStrategy.OnPush, host: {
528
+ role: 'separator',
529
+ class: 'coar-menu-divider',
530
+ }, 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"] }]
531
+ }] });
532
+
533
+ /**
534
+ * CoarMenuHeading: Non-interactive section label for menu groups.
535
+ *
536
+ * Responsibilities:
537
+ * - Display section/group heading text
538
+ * - Provide visual separation between menu sections
539
+ * - Non-interactive (no hover, no click, not focusable)
540
+ *
541
+ * @example
542
+ * ```html
543
+ * <coar-menu>
544
+ * <coar-menu-heading>Foundations</coar-menu-heading>
545
+ * <coar-menu-item routerLink="/typography">Typography</coar-menu-item>
546
+ * <coar-menu-item routerLink="/colors">Colors</coar-menu-item>
547
+ *
548
+ * <coar-menu-heading>Form Controls</coar-menu-heading>
549
+ * <coar-menu-item routerLink="/text-input">Text Input</coar-menu-item>
550
+ * </coar-menu>
551
+ * ```
552
+ */
553
+ class CoarMenuHeadingComponent {
554
+ /** Optional explicit label text (alternative to content projection) */
555
+ label = input(...(ngDevMode ? [undefined, { debugName: "label" }] : []));
556
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuHeadingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
557
+ 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: `
558
+ <div class="coar-menu-heading">
559
+ @if (label()) {
560
+ {{ label() }}
561
+ } @else {
562
+ <ng-content />
563
+ }
564
+ </div>
565
+ `, isInline: true, styles: [":host{display:block;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(:not(:first-child)){margin-top:var(--coar-menu-heading-spacing-top, .2rem)}.coar-menu-heading{display:flex;align-items:center}:host-context(.coar-menu--sidebar){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 });
566
+ }
567
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarMenuHeadingComponent, decorators: [{
568
+ type: Component,
569
+ args: [{ selector: 'coar-menu-heading', standalone: true, imports: [], template: `
570
+ <div class="coar-menu-heading">
571
+ @if (label()) {
572
+ {{ label() }}
573
+ } @else {
574
+ <ng-content />
575
+ }
576
+ </div>
577
+ `, changeDetection: ChangeDetectionStrategy.OnPush, host: {
578
+ class: 'coar-menu-heading-host',
579
+ }, styles: [":host{display:block;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(:not(:first-child)){margin-top:var(--coar-menu-heading-spacing-top, .2rem)}.coar-menu-heading{display:flex;align-items:center}:host-context(.coar-menu--sidebar){font-size:16px;letter-spacing:.08em;padding:.5rem .75rem .375rem}:host-context(.coar-menu--sidebar):not(:first-child){margin-top:1.25rem}\n"] }]
580
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
581
+
582
+ /**
583
+ * Marks an inline submenu template for a `coar-submenu-item`.
584
+ *
585
+ * This is optional: if a submenu item contains exactly one direct child `<ng-template>`,
586
+ * that template will be used even without this directive.
587
+ */
588
+ class CoarSubmenuTemplateDirective {
589
+ templateRef = inject(TemplateRef);
590
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubmenuTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
591
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: CoarSubmenuTemplateDirective, isStandalone: true, selector: "ng-template[coarSubmenu]", ngImport: i0 });
592
+ }
593
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubmenuTemplateDirective, decorators: [{
594
+ type: Directive,
595
+ args: [{
596
+ selector: 'ng-template[coarSubmenu]',
597
+ standalone: true,
598
+ }]
599
+ }] });
600
+
601
+ /**
602
+ * CoarSubmenuItem: Menu item that opens a submenu on hover.
603
+ *
604
+ * Responsibilities:
605
+ * - Render parent item with icon and label
606
+ * - Manage submenu overlay lifecycle
607
+ * - Support nested submenus (can contain other submenu-items)
608
+ *
609
+ * @example
610
+ * ```html
611
+ * <coar-submenu-item label="Share" icon="🔗">
612
+ * <ng-template>
613
+ * <coar-menu-item icon="✉️" (clicked)="sendEmail()">Email</coar-menu-item>
614
+ * <coar-menu-item icon="🔗" (clicked)="copyLink()">Copy Link</coar-menu-item>
615
+ * </ng-template>
616
+ * </coar-submenu-item>
617
+ *
618
+ * <!-- Optional: explicitly mark the template -->
619
+ * <coar-submenu-item label="Share" icon="🔗">
620
+ * <ng-template coarSubmenu>
621
+ * ...
622
+ * </ng-template>
623
+ * </coar-submenu-item>
624
+ *
625
+ * <!-- Also supported (legacy / external template): -->
626
+ * <coar-submenu-item label="Share" icon="🔗" [submenuTemplate]="shareMenu" />
627
+ * ```
628
+ */
629
+ class CoarSubmenuItemComponent {
630
+ destroyRef = inject(DestroyRef);
631
+ cdr = inject(ChangeDetectorRef);
632
+ cascade = inject(COAR_MENU_CASCADE);
633
+ parentOverlay = inject(COAR_MENU_PARENT, { optional: true });
634
+ injector = inject(Injector);
635
+ overlayBuilder = createOverlayBuilder();
636
+ // Ensures the submenu TemplateRef is instantiated with a parent injector that contains
637
+ // the correct cascade instance for this submenu item. Without this, TemplateRefs declared
638
+ // outside of this component can end up resolving menu context from the declaration site.
639
+ submenuTemplateInjector = Injector.create({
640
+ providers: [{ provide: COAR_MENU_CASCADE, useValue: this.cascade }],
641
+ parent: this.injector,
642
+ });
643
+ constructor() {
644
+ // Cleanup cascade when component is destroyed
645
+ this.destroyRef.onDestroy(() => {
646
+ this.cascade.destroy();
647
+ // Ensure state is cleared on destroy
648
+ if (this.isOpen) {
649
+ this.isOpen = false;
650
+ this.submenuRef = null;
651
+ }
652
+ });
653
+ }
654
+ /** Label text for the menu item */
655
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
656
+ /** Optional icon identifier */
657
+ icon = input(undefined, ...(ngDevMode ? [{ debugName: "icon" }] : []));
658
+ /** Disabled state prevents interaction */
659
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
660
+ /**
661
+ * Optional external submenu template.
662
+ *
663
+ * Prefer an inline `<ng-template>` child when possible.
664
+ */
665
+ submenuTemplate = input(null, ...(ngDevMode ? [{ debugName: "submenuTemplate" }] : []));
666
+ /**
667
+ * Optional data to pass to the submenu template.
668
+ *
669
+ * If provided, this data will be passed as the context to the submenu template,
670
+ * instead of inheriting the parent overlay's context.
671
+ *
672
+ * Use this to create clean boundaries between parent and submenu:
673
+ * - Simple case: `[submenuData]="context"` (pass full context)
674
+ * - Complex case: `[submenuData]="transformData(context)"` (pass only what submenu needs)
675
+ *
676
+ * @example
677
+ * ```html
678
+ * <!-- Pass specific data contract to submenu -->
679
+ * <coar-submenu-item
680
+ * label="Status"
681
+ * [submenuTemplate]="statusMenu"
682
+ * [submenuData]="{ selectedIds: context.selected.map(s => s.Id) }">
683
+ * </coar-submenu-item>
684
+ * ```
685
+ */
686
+ submenuData = input(undefined, ...(ngDevMode ? [{ debugName: "submenuData" }] : []));
687
+ markedInlineTemplate;
688
+ inlineTemplate;
689
+ overlaySubmenuTemplate;
690
+ submenuRef = null;
691
+ isOpen = false;
692
+ submenuTemplateToRender() {
693
+ const template = this.markedInlineTemplate?.templateRef ?? this.inlineTemplate ?? this.submenuTemplate();
694
+ if (!template) {
695
+ throw new Error('CoarSubmenuItemComponent: missing submenu content. Provide either an inline <ng-template> child or set [submenuTemplate].');
696
+ }
697
+ return template;
698
+ }
699
+ onMouseEnter(event) {
700
+ if (this.disabled()) {
701
+ return;
702
+ }
703
+ // Defensive: sync state with actual overlay status
704
+ if (this.submenuRef) {
705
+ if (this.submenuRef.isClosed) {
706
+ // Overlay is closed, clear everything
707
+ this.submenuRef = null;
708
+ this.isOpen = false;
709
+ this.cdr.markForCheck();
710
+ }
711
+ else if (!this.isOpen) {
712
+ // Overlay is open but state is wrong - fix it
713
+ this.isOpen = true;
714
+ this.cdr.markForCheck();
715
+ }
716
+ }
717
+ // Open submenu if not already open
718
+ if (!this.submenuRef) {
719
+ const anchor = event.currentTarget;
720
+ // Menu-aim: when a different sibling submenu is already open, delay switching
721
+ // if the pointer trajectory suggests the user is heading into the open submenu panel.
722
+ const parent = this.cascade.parent;
723
+ if (parent) {
724
+ parent.requestOpenFromChild(this.cascade, () => this.openSubmenu(anchor), {
725
+ x: event.clientX,
726
+ y: event.clientY,
727
+ });
728
+ }
729
+ else {
730
+ this.openSubmenu(anchor);
731
+ }
732
+ }
733
+ }
734
+ onClick(event) {
735
+ if (this.disabled()) {
736
+ event.preventDefault();
737
+ event.stopPropagation();
738
+ return;
739
+ }
740
+ if (this.submenuRef?.isClosed) {
741
+ this.submenuRef = null;
742
+ this.isOpen = false;
743
+ this.cdr.markForCheck();
744
+ }
745
+ // For accessibility: toggle on click/Enter/Space
746
+ if (this.submenuRef) {
747
+ this.closeSubmenu();
748
+ }
749
+ else {
750
+ this.openSubmenu(event.currentTarget);
751
+ }
752
+ }
753
+ onKeyboardActivate(event) {
754
+ if (this.disabled()) {
755
+ event.preventDefault();
756
+ event.stopPropagation();
757
+ return;
758
+ }
759
+ if (this.submenuRef?.isClosed) {
760
+ this.submenuRef = null;
761
+ this.isOpen = false;
762
+ this.cdr.markForCheck();
763
+ }
764
+ const target = event.currentTarget;
765
+ if (this.submenuRef) {
766
+ this.closeSubmenu();
767
+ }
768
+ else {
769
+ this.openSubmenu(target);
770
+ }
771
+ }
772
+ openSubmenu(anchorElement) {
773
+ // Validate early so the error points at the submenu item usage.
774
+ this.submenuTemplateToRender();
775
+ // Determine what data to pass to the submenu template
776
+ const submenuContextData = this.submenuData();
777
+ const opener = this.overlayBuilder
778
+ .withPreset(coarHoverMenuPreset)
779
+ .anchor({ kind: 'element', element: anchorElement })
780
+ .position({ placement: ['right-start', 'left-start'], offset: -4, flip: true, shift: true })
781
+ .fromTemplate(this.overlaySubmenuTemplate);
782
+ // Prefer the cascade parent's overlayRef when available.
783
+ // With Angular content projection, submenu content can be instantiated in the *root* overlay
784
+ // injector even when it's rendered inside a child overlay. The cascade chain preserves the
785
+ // correct containing overlay via overlayRef.
786
+ const containingOverlay = this.cascade.parent?.overlayRef ?? this.parentOverlay;
787
+ if (containingOverlay) {
788
+ // Inside an overlay: close siblings (direct children of parent) then open as child
789
+ // First, create the child overlay
790
+ // Pass submenuData if provided, otherwise undefined (inherits parent context via $implicit)
791
+ this.submenuRef = opener.openAsChild(containingOverlay, submenuContextData);
792
+ // Then close siblings, excluding the newly opened one
793
+ containingOverlay.closeChildren(this.submenuRef);
794
+ }
795
+ else {
796
+ // Inline menu: use cascade-level sibling closure
797
+ this.cascade.closeSiblings();
798
+ this.submenuRef = opener.open(submenuContextData);
799
+ }
800
+ // Expose the overlay ref to descendants so they can parent their own flyouts correctly.
801
+ this.cascade.overlayRef = this.submenuRef;
802
+ this.cascade.parent?.notifyChildOpened(this.cascade);
803
+ this.isOpen = true;
804
+ this.cdr.markForCheck();
805
+ const openedRef = this.submenuRef;
806
+ // Clear the active styling as soon as a close is initiated (e.g. hoverTree timer,
807
+ // outside click, or sibling submenu switch). Otherwise the item can look "stuck"
808
+ // while the overlay finishes its close transition.
809
+ const originalClose = openedRef.close.bind(openedRef);
810
+ openedRef.close = (result) => {
811
+ if (this.submenuRef === openedRef && this.isOpen) {
812
+ this.isOpen = false;
813
+ this.cdr.markForCheck();
814
+ }
815
+ originalClose(result);
816
+ };
817
+ openedRef.afterClosed$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
818
+ // Only clear state if this is still the current overlay
819
+ // (prevents race condition when rapidly opening new overlays)
820
+ if (this.submenuRef === openedRef) {
821
+ this.submenuRef = null;
822
+ this.isOpen = false;
823
+ this.cdr.markForCheck();
824
+ }
825
+ if (this.cascade.overlayRef === openedRef) {
826
+ this.cascade.overlayRef = null;
827
+ }
828
+ this.cascade.parent?.notifyChildClosed(this.cascade);
829
+ });
830
+ }
831
+ // Parent overlay is resolved via COAR_OVERLAY_REF when rendered inside an overlay.
832
+ closeSubmenu() {
833
+ this.submenuRef?.close();
834
+ this.submenuRef = null;
835
+ this.isOpen = false;
836
+ this.cdr.markForCheck();
837
+ }
838
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubmenuItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
839
+ 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 }, submenuData: { classPropertyName: "submenuData", publicName: "submenuData", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
840
+ {
841
+ provide: COAR_MENU_CASCADE,
842
+ useFactory: () => new CoarMenuCascade(inject(COAR_MENU_CASCADE, { optional: true, skipSelf: true })),
843
+ },
844
+ ], 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: `
845
+ <div
846
+ class="coar-submenu-item"
847
+ [class.coar-submenu-item--disabled]="disabled()"
848
+ [class.coar-submenu-item--open]="isOpen"
849
+ [attr.role]="'menuitem'"
850
+ [attr.aria-haspopup]="'menu'"
851
+ [attr.aria-expanded]="isOpen"
852
+ [attr.aria-disabled]="disabled()"
853
+ [attr.tabindex]="disabled() ? -1 : 0"
854
+ (mouseenter)="onMouseEnter($event)"
855
+ (click)="onClick($event)"
856
+ (keydown.enter)="onKeyboardActivate($event)"
857
+ (keydown.space)="onKeyboardActivate($event)"
858
+ >
859
+ <span class="coar-submenu-item__icon" aria-hidden="true">
860
+ <coar-icon [name]="icon() || 'square-rounded-dashed'" size="s" aria-hidden="true" />
861
+ </span>
862
+ <span class="coar-submenu-item__label">{{ label() }}</span>
863
+ <coar-icon
864
+ name="chevron-right"
865
+ size="xs"
866
+ class="coar-submenu-item__arrow"
867
+ aria-hidden="true"
868
+ />
869
+ </div>
870
+
871
+ <ng-template #overlaySubmenuTemplate>
872
+ <ng-container
873
+ *ngTemplateOutlet="submenuTemplateToRender(); injector: submenuTemplateInjector"
874
+ />
875
+ </ng-template>
876
+ `, 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-m-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:hover:not(.coar-submenu-item--disabled){background:var(--coar-menu-item-background-hover, #f5f5f5);font-weight:500}.coar-submenu-item--open{background:var(--coar-menu-item-background-hover, #f5f5f5);font-weight:500}.coar-submenu-item:focus-visible{background:var(--coar-menu-item-background-focus, #f5f5f5);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", "source", "size", "rotate", "rotateTransition", "spin", "color", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
877
+ }
878
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubmenuItemComponent, decorators: [{
879
+ type: Component,
880
+ args: [{ selector: 'coar-submenu-item, coar-sub-flyout', standalone: true, imports: [CommonModule, CoarIconComponent], template: `
881
+ <div
882
+ class="coar-submenu-item"
883
+ [class.coar-submenu-item--disabled]="disabled()"
884
+ [class.coar-submenu-item--open]="isOpen"
885
+ [attr.role]="'menuitem'"
886
+ [attr.aria-haspopup]="'menu'"
887
+ [attr.aria-expanded]="isOpen"
888
+ [attr.aria-disabled]="disabled()"
889
+ [attr.tabindex]="disabled() ? -1 : 0"
890
+ (mouseenter)="onMouseEnter($event)"
891
+ (click)="onClick($event)"
892
+ (keydown.enter)="onKeyboardActivate($event)"
893
+ (keydown.space)="onKeyboardActivate($event)"
894
+ >
895
+ <span class="coar-submenu-item__icon" aria-hidden="true">
896
+ <coar-icon [name]="icon() || 'square-rounded-dashed'" size="s" aria-hidden="true" />
897
+ </span>
898
+ <span class="coar-submenu-item__label">{{ label() }}</span>
899
+ <coar-icon
900
+ name="chevron-right"
901
+ size="xs"
902
+ class="coar-submenu-item__arrow"
903
+ aria-hidden="true"
904
+ />
905
+ </div>
906
+
907
+ <ng-template #overlaySubmenuTemplate>
908
+ <ng-container
909
+ *ngTemplateOutlet="submenuTemplateToRender(); injector: submenuTemplateInjector"
910
+ />
911
+ </ng-template>
912
+ `, changeDetection: ChangeDetectionStrategy.OnPush, providers: [
913
+ {
914
+ provide: COAR_MENU_CASCADE,
915
+ useFactory: () => new CoarMenuCascade(inject(COAR_MENU_CASCADE, { optional: true, skipSelf: true })),
916
+ },
917
+ ], 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-m-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:hover:not(.coar-submenu-item--disabled){background:var(--coar-menu-item-background-hover, #f5f5f5);font-weight:500}.coar-submenu-item--open{background:var(--coar-menu-item-background-hover, #f5f5f5);font-weight:500}.coar-submenu-item:focus-visible{background:var(--coar-menu-item-background-focus, #f5f5f5);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"] }]
918
+ }], 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 }] }], submenuData: [{ type: i0.Input, args: [{ isSignal: true, alias: "submenuData", required: false }] }], markedInlineTemplate: [{
919
+ type: ContentChild,
920
+ args: [CoarSubmenuTemplateDirective, { descendants: false }]
921
+ }], inlineTemplate: [{
922
+ type: ContentChild,
923
+ args: [TemplateRef, { descendants: false }]
924
+ }], overlaySubmenuTemplate: [{
925
+ type: ViewChild,
926
+ args: ['overlaySubmenuTemplate']
927
+ }] } });
928
+
929
+ /**
930
+ * CoarSubExpand: Menu item that expands/collapses a submenu inline.
931
+ *
932
+ * Use this variant when you want nested options to stay visible (e.g. sidebar panels)
933
+ * while still reusing the same menu item types inside the submenu.
934
+ *
935
+ * @example
936
+ * ```html
937
+ * <coar-menu>
938
+ * <coar-sub-expand label="Filters" icon="settings" [(open)]="filtersOpen">
939
+ * <ng-template>
940
+ * <coar-menu>
941
+ * <coar-menu-item icon="plus">Add Filter</coar-menu-item>
942
+ * <coar-menu-item icon="trash">Clear</coar-menu-item>
943
+ * </coar-menu>
944
+ * </ng-template>
945
+ </coar-sub-expand>
946
+ * </coar-menu>
947
+ * ```
948
+ */
949
+ class CoarSubExpandComponent {
950
+ /** Label text for the menu item */
951
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
952
+ /** Optional icon identifier */
953
+ icon = input(undefined, ...(ngDevMode ? [{ debugName: "icon" }] : []));
954
+ /** Disabled state prevents interaction */
955
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
956
+ /** Expanded state (two-way bindable with [(open)]) */
957
+ open = input(undefined, { ...(ngDevMode ? { debugName: "open" } : {}), transform: booleanAttribute });
958
+ /** Emits when expanded state changes (for [(open)]) */
959
+ openChange = output();
960
+ openInternal = signal(false, ...(ngDevMode ? [{ debugName: "openInternal" }] : []));
961
+ isOpen = computed(() => this.open() ?? this.openInternal(), ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
962
+ /** Optional external submenu template. Prefer an inline `<ng-template>` child when possible. */
963
+ submenuTemplate = input(null, ...(ngDevMode ? [{ debugName: "submenuTemplate" }] : []));
964
+ markedInlineTemplate;
965
+ inlineTemplate;
966
+ submenuTemplateToRender() {
967
+ const template = this.markedInlineTemplate?.templateRef ?? this.inlineTemplate ?? this.submenuTemplate();
968
+ if (!template) {
969
+ throw new Error('CoarSubExpandComponent: missing submenu content. Provide either an inline <ng-template> child or set [submenuTemplate].');
970
+ }
971
+ return template;
972
+ }
973
+ constructor() {
974
+ effect(() => {
975
+ const value = this.open();
976
+ if (value === undefined)
977
+ return;
978
+ this.openInternal.set(value);
979
+ });
980
+ }
981
+ toggle(event) {
982
+ if (this.disabled()) {
983
+ event.preventDefault();
984
+ event.stopPropagation();
985
+ return;
986
+ }
987
+ const next = !this.isOpen();
988
+ this.openInternal.set(next);
989
+ this.openChange.emit(next);
990
+ }
991
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubExpandComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
992
+ 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: `
993
+ <div
994
+ class="coar-sub-expand"
995
+ [class.coar-sub-expand--disabled]="disabled()"
996
+ [class.coar-sub-expand--open]="isOpen()"
997
+ [attr.role]="'menuitem'"
998
+ [attr.aria-haspopup]="'menu'"
999
+ [attr.aria-expanded]="isOpen()"
1000
+ [attr.aria-disabled]="disabled()"
1001
+ [attr.tabindex]="disabled() ? -1 : 0"
1002
+ (click)="toggle($event)"
1003
+ (keydown.enter)="toggle($event)"
1004
+ (keydown.space)="toggle($event)"
1005
+ >
1006
+ <span class="coar-sub-expand__icon" aria-hidden="true">
1007
+ <coar-icon [name]="icon() || 'square-rounded-dashed'" size="s" aria-hidden="true" />
1008
+ </span>
1009
+ <span class="coar-sub-expand__label">{{ label() }}</span>
1010
+ <coar-icon
1011
+ [name]="isOpen() ? 'minus' : 'plus'"
1012
+ size="xs"
1013
+ class="coar-sub-expand__arrow"
1014
+ aria-hidden="true"
1015
+ />
1016
+ </div>
1017
+
1018
+ <div
1019
+ class="coar-sub-expand__panel"
1020
+ [class.coar-sub-expand__panel--open]="isOpen()"
1021
+ [attr.aria-hidden]="isOpen() ? null : 'true'"
1022
+ role="group"
1023
+ >
1024
+ <div class="coar-sub-expand__panel-inner">
1025
+ <ng-container *ngTemplateOutlet="submenuTemplateToRender()" />
1026
+ </div>
1027
+ </div>
1028
+ `, 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-m-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:hover:not(.coar-sub-expand--disabled){background:var(--coar-menu-item-background-hover, #f5f5f5);color:var(--coar-menu-item-color-hover, var(--coar-text-neutral-primary, #545454))}.coar-sub-expand:focus-visible{background:var(--coar-menu-item-background-focus, #f5f5f5);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:before{content:\"\";position:absolute;top:0;bottom:0;left:0;width:1px;background:var(--coar-sub-expand-line-color, var(--coar-border-neutral-tertiary, #e5e5e5));pointer-events:none;opacity:0;transition:opacity var(--coar-duration-fast, .1s) var(--coar-ease-out, ease);z-index:1}.coar-sub-expand__panel--open{grid-template-rows:1fr}.coar-sub-expand__panel--open:before{opacity:1}.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)}:host-context(coar-sidebar){margin-top:var(--coar-sub-expand-sidebar-spacing-top, var(--coar-spacing-m, 12px))}:host-context(coar-sidebar):first-child{margin-top:0}:host-context(coar-sidebar) .coar-sub-expand{font-size:var(--coar-menu-heading-font-size, var(--coar-font-size-xs, 12px));font-weight:var(--coar-font-weight-semi-bold, 600);text-transform:uppercase;letter-spacing:.05em;color:var(--coar-menu-heading-color, var(--coar-text-neutral-secondary));padding:.5rem .75rem .25rem;background:transparent}:host-context(coar-sidebar) .coar-sub-expand:hover:not(.coar-sub-expand--disabled){background:transparent;color:var(--coar-text-neutral-primary)}:host-context(coar-sidebar) .coar-sub-expand__icon{display:none}:host-context(coar-sidebar) .coar-sub-expand__arrow{opacity:.4}:host-context(coar-sidebar) .coar-sub-expand:hover:not(.coar-sub-expand--disabled) .coar-sub-expand__arrow{opacity:.8}:host-context(coar-sidebar) .coar-sub-expand__panel{margin-left:0}:host-context(coar-sidebar) .coar-sub-expand__panel:before{display:none}\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", "source", "size", "rotate", "rotateTransition", "spin", "color", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1029
+ }
1030
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarSubExpandComponent, decorators: [{
1031
+ type: Component,
1032
+ args: [{ selector: 'coar-sub-expand', standalone: true, imports: [CommonModule, CoarIconComponent], template: `
1033
+ <div
1034
+ class="coar-sub-expand"
1035
+ [class.coar-sub-expand--disabled]="disabled()"
1036
+ [class.coar-sub-expand--open]="isOpen()"
1037
+ [attr.role]="'menuitem'"
1038
+ [attr.aria-haspopup]="'menu'"
1039
+ [attr.aria-expanded]="isOpen()"
1040
+ [attr.aria-disabled]="disabled()"
1041
+ [attr.tabindex]="disabled() ? -1 : 0"
1042
+ (click)="toggle($event)"
1043
+ (keydown.enter)="toggle($event)"
1044
+ (keydown.space)="toggle($event)"
1045
+ >
1046
+ <span class="coar-sub-expand__icon" aria-hidden="true">
1047
+ <coar-icon [name]="icon() || 'square-rounded-dashed'" size="s" aria-hidden="true" />
1048
+ </span>
1049
+ <span class="coar-sub-expand__label">{{ label() }}</span>
1050
+ <coar-icon
1051
+ [name]="isOpen() ? 'minus' : 'plus'"
1052
+ size="xs"
1053
+ class="coar-sub-expand__arrow"
1054
+ aria-hidden="true"
1055
+ />
1056
+ </div>
1057
+
1058
+ <div
1059
+ class="coar-sub-expand__panel"
1060
+ [class.coar-sub-expand__panel--open]="isOpen()"
1061
+ [attr.aria-hidden]="isOpen() ? null : 'true'"
1062
+ role="group"
1063
+ >
1064
+ <div class="coar-sub-expand__panel-inner">
1065
+ <ng-container *ngTemplateOutlet="submenuTemplateToRender()" />
1066
+ </div>
1067
+ </div>
1068
+ `, 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-m-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:hover:not(.coar-sub-expand--disabled){background:var(--coar-menu-item-background-hover, #f5f5f5);color:var(--coar-menu-item-color-hover, var(--coar-text-neutral-primary, #545454))}.coar-sub-expand:focus-visible{background:var(--coar-menu-item-background-focus, #f5f5f5);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:before{content:\"\";position:absolute;top:0;bottom:0;left:0;width:1px;background:var(--coar-sub-expand-line-color, var(--coar-border-neutral-tertiary, #e5e5e5));pointer-events:none;opacity:0;transition:opacity var(--coar-duration-fast, .1s) var(--coar-ease-out, ease);z-index:1}.coar-sub-expand__panel--open{grid-template-rows:1fr}.coar-sub-expand__panel--open:before{opacity:1}.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)}:host-context(coar-sidebar){margin-top:var(--coar-sub-expand-sidebar-spacing-top, var(--coar-spacing-m, 12px))}:host-context(coar-sidebar):first-child{margin-top:0}:host-context(coar-sidebar) .coar-sub-expand{font-size:var(--coar-menu-heading-font-size, var(--coar-font-size-xs, 12px));font-weight:var(--coar-font-weight-semi-bold, 600);text-transform:uppercase;letter-spacing:.05em;color:var(--coar-menu-heading-color, var(--coar-text-neutral-secondary));padding:.5rem .75rem .25rem;background:transparent}:host-context(coar-sidebar) .coar-sub-expand:hover:not(.coar-sub-expand--disabled){background:transparent;color:var(--coar-text-neutral-primary)}:host-context(coar-sidebar) .coar-sub-expand__icon{display:none}:host-context(coar-sidebar) .coar-sub-expand__arrow{opacity:.4}:host-context(coar-sidebar) .coar-sub-expand:hover:not(.coar-sub-expand--disabled) .coar-sub-expand__arrow{opacity:.8}:host-context(coar-sidebar) .coar-sub-expand__panel{margin-left:0}:host-context(coar-sidebar) .coar-sub-expand__panel:before{display:none}\n"] }]
1069
+ }], 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: [{
1070
+ type: ContentChild,
1071
+ args: [CoarSubmenuTemplateDirective, { descendants: false }]
1072
+ }], inlineTemplate: [{
1073
+ type: ContentChild,
1074
+ args: [TemplateRef, { descendants: false }]
1075
+ }] } });
1076
+
1077
+ /**
1078
+ * Generated bundle index. Do not edit.
1079
+ */
1080
+
1081
+ export { COAR_MENU_AIM_CONFIG, CoarMenuAimConfigDirective, CoarMenuComponent, CoarMenuDividerComponent, CoarMenuHeadingComponent, CoarMenuItemComponent, CoarSubExpandComponent, CoarSubmenuItemComponent, CoarSubmenuTemplateDirective, DEFAULT_COAR_MENU_AIM_CONFIG };
1082
+ //# sourceMappingURL=cocoar-ui-menu.mjs.map