@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.
- package/README.md +7 -0
- package/fesm2022/cocoar-ui-menu.mjs +1054 -0
- package/fesm2022/cocoar-ui-menu.mjs.map +1 -0
- package/package.json +25 -0
- package/types/cocoar-ui-menu.d.ts +298 -0
|
@@ -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
|