@rivinia/menu 0.1.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from "@angular/core";
2
- import { Component, Directive, ElementRef, ViewEncapsulation, afterRenderEffect, computed, effect, inject, input, output, signal } from "@angular/core";
2
+ import { Component, Directive, ElementRef, ViewEncapsulation, afterRenderEffect, computed, contentChildren, effect, inject, input, output, signal } from "@angular/core";
3
3
  import { RiviniaMenu } from "@rivinia/menu";
4
4
  const MENU_GAP = 6;
5
5
  const EDGE = 8;
@@ -31,6 +31,7 @@ function computeMenuPosition(trigger, panel, placement, viewport, rtl, gap = 6)
31
31
  flipped
32
32
  };
33
33
  }
34
+ let nextPanelId = 0;
34
35
  var RiviniaMenuItem = class RiviniaMenuItem {
35
36
  id = input.required(...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
36
37
  disabled = input(false, {
@@ -38,6 +39,8 @@ var RiviniaMenuItem = class RiviniaMenuItem {
38
39
  transform: (v) => v === "" || v === true
39
40
  });
40
41
  checked = input(null, ...ngDevMode ? [{ debugName: "checked" }] : /* istanbul ignore next */ []);
42
+ label = input(null, ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
43
+ host = inject(ElementRef, { optional: true });
41
44
  menu = inject(RiviniaMenuComponent, {
42
45
  optional: true,
43
46
  skipSelf: true
@@ -53,20 +56,41 @@ var RiviniaMenuItem = class RiviniaMenuItem {
53
56
  const checked = this.checked();
54
57
  return checked === null ? null : String(checked);
55
58
  }
59
+ isDisabled() {
60
+ if (this.disabled()) return true;
61
+ return this.host?.nativeElement?.disabled === true;
62
+ }
56
63
  pick() {
57
64
  this.menu?.pickItem(this.id());
58
65
  }
59
66
  hover() {
60
67
  this.menu?.hoverItem(this.id());
61
68
  }
69
+ registeredId = null;
70
+ constructor() {
71
+ effect(() => {
72
+ const id = this.id();
73
+ const menu = this.menu;
74
+ if (this.registeredId === null) return;
75
+ if (this.registeredId !== id) menu?.unregisterItem(this.registeredId);
76
+ menu?.registerItem({
77
+ id,
78
+ disabled: this.isDisabled()
79
+ });
80
+ this.registeredId = id;
81
+ });
82
+ }
62
83
  ngOnInit() {
84
+ this.registeredId = this.id();
63
85
  this.menu?.registerItem({
64
86
  id: this.id(),
65
- disabled: this.disabled()
87
+ disabled: this.isDisabled()
66
88
  });
67
89
  }
68
90
  ngOnDestroy() {
69
- this.menu?.unregisterItem(this.id());
91
+ if (this.registeredId === null) return;
92
+ this.menu?.unregisterItem(this.registeredId);
93
+ this.registeredId = null;
70
94
  }
71
95
  static ɵfac = i0.ɵɵngDeclareFactory({
72
96
  minVersion: "12.0.0",
@@ -103,6 +127,13 @@ var RiviniaMenuItem = class RiviniaMenuItem {
103
127
  isSignal: true,
104
128
  isRequired: false,
105
129
  transformFunction: null
130
+ },
131
+ label: {
132
+ classPropertyName: "label",
133
+ publicName: "label",
134
+ isSignal: true,
135
+ isRequired: false,
136
+ transformFunction: null
106
137
  }
107
138
  },
108
139
  host: {
@@ -113,8 +144,9 @@ var RiviniaMenuItem = class RiviniaMenuItem {
113
144
  properties: {
114
145
  "attr.role": "computedRole()",
115
146
  "attr.data-rv-id": "id()",
147
+ "attr.data-rv-label": "label()",
116
148
  "attr.tabindex": "tab()",
117
- "attr.aria-disabled": "disabled()",
149
+ "attr.aria-disabled": "isDisabled()",
118
150
  "attr.data-active": "active()",
119
151
  "attr.aria-checked": "checkedAttr()"
120
152
  }
@@ -135,8 +167,9 @@ i0.ɵɵngDeclareClassMetadata({
135
167
  host: {
136
168
  "[attr.role]": "computedRole()",
137
169
  "[attr.data-rv-id]": "id()",
170
+ "[attr.data-rv-label]": "label()",
138
171
  "[attr.tabindex]": "tab()",
139
- "[attr.aria-disabled]": "disabled()",
172
+ "[attr.aria-disabled]": "isDisabled()",
140
173
  "[attr.data-active]": "active()",
141
174
  "[attr.aria-checked]": "checkedAttr()",
142
175
  "(click)": "pick()",
@@ -144,6 +177,7 @@ i0.ɵɵngDeclareClassMetadata({
144
177
  }
145
178
  }]
146
179
  }],
180
+ ctorParameters: () => [],
147
181
  propDecorators: {
148
182
  id: [{
149
183
  type: i0.Input,
@@ -168,18 +202,36 @@ i0.ɵɵngDeclareClassMetadata({
168
202
  alias: "checked",
169
203
  required: false
170
204
  }]
205
+ }],
206
+ label: [{
207
+ type: i0.Input,
208
+ args: [{
209
+ isSignal: true,
210
+ alias: "label",
211
+ required: false
212
+ }]
171
213
  }]
172
214
  }
173
215
  });
174
216
  var RiviniaMenuComponent = class RiviniaMenuComponent {
175
217
  menu = input(null, ...ngDevMode ? [{ debugName: "menu" }] : /* istanbul ignore next */ []);
176
218
  placement = input("bottom-start", ...ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []);
219
+ menuLabel = input(null, ...ngDevMode ? [{ debugName: "menuLabel" }] : /* istanbul ignore next */ []);
220
+ items = input(null, ...ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []);
221
+ panelClass = input(null, ...ngDevMode ? [{ debugName: "panelClass" }] : /* istanbul ignore next */ []);
222
+ itemClass = input(null, ...ngDevMode ? [{ debugName: "itemClass" }] : /* istanbul ignore next */ []);
177
223
  picked = output();
178
224
  opened = output();
179
225
  closed = output();
180
226
  owned = new RiviniaMenu();
181
227
  self = inject(ElementRef);
228
+ projectedItems = contentChildren(RiviniaMenuItem, {
229
+ ...ngDevMode ? { debugName: "projectedItems" } : /* istanbul ignore next */ {},
230
+ descendants: true
231
+ });
232
+ panelId = `rv-menu-panel-${++nextPanelId}`;
182
233
  focusPending = false;
234
+ pendingActivation = false;
183
235
  topLayerSupported = typeof HTMLElement !== "undefined" && "showPopover" in HTMLElement.prototype;
184
236
  positioned = signal(false, ...ngDevMode ? [{ debugName: "positioned" }] : /* istanbul ignore next */ []);
185
237
  flipped = signal(false, ...ngDevMode ? [{ debugName: "flipped" }] : /* istanbul ignore next */ []);
@@ -188,19 +240,38 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
188
240
  controller() {
189
241
  return this.menu() ?? this.owned;
190
242
  }
243
+ dataItems() {
244
+ return this.ambiguous() ? [] : this.items() ?? [];
245
+ }
246
+ checkedFor(id) {
247
+ const c = this.controller();
248
+ return typeof c.isSelected === "function" ? c.isSelected(id) : null;
249
+ }
250
+ ambiguous() {
251
+ return this.items() !== null && this.projectedItems().length > 0;
252
+ }
191
253
  registerItem(item) {
192
- this.controller().register(item);
254
+ const c = this.controller();
255
+ c.register(item);
256
+ if (this.pendingActivation && c.open() && c.activeId() === null && !item.disabled) {
257
+ c.activateFirst();
258
+ this.pendingActivation = false;
259
+ }
193
260
  }
194
261
  unregisterItem(id) {
195
262
  this.controller().unregister(id);
196
263
  }
197
264
  constructor() {
265
+ effect(() => {
266
+ if (!this.ambiguous()) return;
267
+ throw new Error("rv-menu: `[items]` cannot be combined with projected `rvMenuItem` children — pick one way to populate the panel.");
268
+ });
198
269
  effect((onCleanup) => {
199
270
  if (typeof window === "undefined") return;
200
271
  if (!this.controller().open()) return;
201
272
  const onPointer = (event) => {
202
273
  const host = this.self.nativeElement;
203
- if (host && !host.contains(event.target)) this.doClose(true);
274
+ if (host && !host.contains(event.target)) this.doClose(false);
204
275
  };
205
276
  document.addEventListener("pointerdown", onPointer);
206
277
  const onScroll = (event) => {
@@ -222,6 +293,7 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
222
293
  const trigger = this.triggerEl();
223
294
  trigger?.setAttribute("aria-haspopup", "menu");
224
295
  trigger?.setAttribute("aria-expanded", String(open));
296
+ trigger?.setAttribute("aria-controls", this.panelId);
225
297
  });
226
298
  afterRenderEffect(() => {
227
299
  if (this.controller().open() && !this.positioned()) this.showAndPosition();
@@ -279,10 +351,9 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
279
351
  }
280
352
  markOpening() {
281
353
  this.focusPending = true;
282
- if (typeof window !== "undefined" && this.topLayerSupported) {
283
- this.flipped.set(false);
284
- this.positioned.set(false);
285
- }
354
+ this.pendingActivation = true;
355
+ this.flipped.set(false);
356
+ this.positioned.set(false);
286
357
  }
287
358
  onTriggerClick() {
288
359
  const c = this.controller();
@@ -347,12 +418,23 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
347
418
  event.preventDefault();
348
419
  this.pickFocused();
349
420
  break;
350
- case "Tab":
421
+ case "Tab": {
422
+ event.preventDefault();
423
+ const backwards = event.shiftKey;
351
424
  this.doClose(false);
425
+ this.focusAdjacentToTrigger(backwards);
352
426
  break;
427
+ }
353
428
  default: this.onTypeahead(event);
354
429
  }
355
430
  }
431
+ resetTypeahead() {
432
+ this.typeBuffer = "";
433
+ if (this.typeTimer !== null) {
434
+ clearTimeout(this.typeTimer);
435
+ this.typeTimer = null;
436
+ }
437
+ }
356
438
  onTypeahead(event) {
357
439
  if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return;
358
440
  const ch = event.key.toLocaleLowerCase();
@@ -371,7 +453,7 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
371
453
  const enabled = new Set(this.controller().items().filter((i) => !i.disabled).map((i) => i.id));
372
454
  const rows = [...root.querySelectorAll("[data-rv-id]")].map((el) => ({
373
455
  id: el.getAttribute("data-rv-id") ?? "",
374
- text: (el.textContent ?? "").trim().toLocaleLowerCase()
456
+ text: (el.getAttribute("data-rv-label") ?? el.textContent ?? "").trim().toLocaleLowerCase()
375
457
  })).filter((r) => enabled.has(r.id) && r.text.startsWith(buffer));
376
458
  if (rows.length === 0) return null;
377
459
  if (new Set(buffer).size === 1) {
@@ -381,8 +463,11 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
381
463
  return rows[0].id;
382
464
  }
383
465
  pickFocused() {
384
- const id = this.controller().pickActive();
466
+ const c = this.controller();
467
+ const id = c.pickActive();
385
468
  if (id === null) return;
469
+ const item = c.items().find((i) => i.id === id);
470
+ if (!item || item.disabled) return;
386
471
  this.doClose(false);
387
472
  this.picked.emit({ id });
388
473
  }
@@ -396,16 +481,22 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
396
481
  }
397
482
  doClose(restoreFocus) {
398
483
  const was = this.controller().open();
484
+ this.resetTypeahead();
399
485
  this.controller().closeMenu();
400
486
  if (was) this.closed.emit();
401
487
  if (restoreFocus) this.focusTrigger();
402
488
  }
403
489
  consumePendingFocus() {
404
- if (!this.controller().open() || !this.focusPending) return;
405
- this.focusPending = false;
490
+ if (!this.positioned() || !this.controller().open() || !this.focusPending) return;
406
491
  const root = this.self.nativeElement.querySelector(".rv-menu-panel");
492
+ if (!root) return;
407
493
  const active = this.activeDocumentElement();
408
- if (root && (!active || !root.contains(active))) this.focusActive();
494
+ if (active && root.contains(active)) {
495
+ this.focusPending = false;
496
+ return;
497
+ }
498
+ this.focusPending = false;
499
+ this.focusActive();
409
500
  }
410
501
  focusActive() {
411
502
  const id = this.controller().activeId();
@@ -428,6 +519,22 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
428
519
  if (!wrap) return null;
429
520
  return wrap.querySelector("button, a, [tabindex]") ?? wrap;
430
521
  }
522
+ focusAdjacentToTrigger(backwards) {
523
+ try {
524
+ const trigger = this.triggerEl();
525
+ const doc = trigger?.ownerDocument;
526
+ if (!trigger || !doc) return;
527
+ const stops = [...doc.querySelectorAll("a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])")].filter((el) => {
528
+ if (el.hasAttribute("disabled") || el.getAttribute("aria-hidden") === "true") return false;
529
+ if (el.closest(".rv-menu-panel")) return false;
530
+ return el.getClientRects().length > 0;
531
+ });
532
+ stops.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
533
+ const at = stops.indexOf(trigger);
534
+ if (at === -1) return;
535
+ stops[backwards ? at - 1 : at + 1]?.focus({ preventScroll: true });
536
+ } catch {}
537
+ }
431
538
  focusTrigger() {
432
539
  try {
433
540
  this.triggerEl()?.focus({ preventScroll: true });
@@ -461,6 +568,34 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
461
568
  isSignal: true,
462
569
  isRequired: false,
463
570
  transformFunction: null
571
+ },
572
+ menuLabel: {
573
+ classPropertyName: "menuLabel",
574
+ publicName: "menuLabel",
575
+ isSignal: true,
576
+ isRequired: false,
577
+ transformFunction: null
578
+ },
579
+ items: {
580
+ classPropertyName: "items",
581
+ publicName: "items",
582
+ isSignal: true,
583
+ isRequired: false,
584
+ transformFunction: null
585
+ },
586
+ panelClass: {
587
+ classPropertyName: "panelClass",
588
+ publicName: "panelClass",
589
+ isSignal: true,
590
+ isRequired: false,
591
+ transformFunction: null
592
+ },
593
+ itemClass: {
594
+ classPropertyName: "itemClass",
595
+ publicName: "itemClass",
596
+ isSignal: true,
597
+ isRequired: false,
598
+ transformFunction: null
464
599
  }
465
600
  },
466
601
  outputs: {
@@ -468,16 +603,25 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
468
603
  opened: "opened",
469
604
  closed: "closed"
470
605
  },
606
+ queries: [{
607
+ propertyName: "projectedItems",
608
+ predicate: RiviniaMenuItem,
609
+ descendants: true,
610
+ isSignal: true
611
+ }],
471
612
  ngImport: i0,
472
613
  template: `
473
614
  <div class="rv-menu-trigger" (click)="onTriggerClick()" (keydown)="onTriggerKey($event)">
474
- <ng-content select="[rv-menu-trigger]"></ng-content>
615
+ <ng-content select="[rv-menu-trigger], [rvMenuTrigger]"></ng-content>
475
616
  </div>
476
617
  @if (controller().open()) {
477
618
  <div
478
619
  #panel
479
620
  class="rv-menu-panel"
621
+ [class]="panelClass()"
480
622
  role="menu"
623
+ [attr.id]="panelId"
624
+ [attr.aria-label]="menuLabel()"
481
625
  [attr.popover]="topLayerSupported ? 'manual' : null"
482
626
  [attr.data-placement]="placement()"
483
627
  [attr.data-flipped]="flipped()"
@@ -485,12 +629,36 @@ var RiviniaMenuComponent = class RiviniaMenuComponent {
485
629
  [attr.data-top-layer]="topLayerSupported"
486
630
  (keydown)="onPanelKey($event)"
487
631
  >
488
- <ng-content select="[rv-menu-panel]"></ng-content>
632
+ @if (dataItems().length > 0) {
633
+ @for (item of dataItems(); track item.id) {
634
+ <button
635
+ rvMenuItem
636
+ [id]="item.id"
637
+ [disabled]="item.disabled ?? false"
638
+ [checked]="checkedFor(item.id)"
639
+ [class]="itemClass()"
640
+ >
641
+ {{ item.label }}
642
+ </button>
643
+ }
644
+ }
645
+ <ng-content select="[rv-menu-panel], [rvMenuPanel]"></ng-content>
489
646
  </div>
490
647
  }
491
648
  `,
492
649
  isInline: true,
493
- styles: [".rv-menu-trigger{display:inline-flex;align-items:center}.rv-menu-panel{display:flex;flex-direction:column;gap:2px}.rv-menu-panel hr{border:0;border-top:1px solid var(--rv-menu-border);margin:6px 4px}rv-menu{position:relative;display:inline-block}.rv-menu-panel[data-top-layer=true]{position:fixed;inset:auto;margin:0}.rv-menu-panel[data-positioned=false]{animation:none}.rv-menu-panel[data-top-layer=true][data-placement^=bottom][data-flipped=true]{transform-origin:bottom center}.rv-menu-panel[data-top-layer=true][data-placement^=top][data-flipped=true]{transform-origin:top center}.rv-menu-panel{--rv-menu-bg: #ffffff;--rv-menu-fg: #111111;--rv-menu-muted: #555555;--rv-menu-border: #e0e0e0;--rv-menu-radius: 12px;--rv-menu-shadow: 0 16px 48px rgba(0, 0, 0, .22);--rv-menu-pad: 6px;--rv-menu-width: 16rem;--rv-menu-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;position:absolute;z-index:var(--rv-menu-z, 500);min-width:var(--rv-menu-width);max-width:calc(100dvw - 2rem);max-height:min(20rem,calc(100dvh - 2rem));overflow-y:auto;background:var(--rv-menu-bg);color:var(--rv-menu-fg);font-family:var(--rv-menu-font);border:1px solid var(--rv-menu-border);border-radius:var(--rv-menu-radius);box-shadow:var(--rv-menu-shadow);padding:var(--rv-menu-pad)}@media(prefers-color-scheme:dark){.rv-menu-panel{--rv-menu-bg: #161616;--rv-menu-fg: #f2f2f2;--rv-menu-muted: #a8a8a8;--rv-menu-border: #333333}}.rv-menu-panel[data-placement=bottom-start]{inset-block-start:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=bottom-end]{inset-block-start:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel[data-placement=top-start]{inset-block-end:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=top-end]{inset-block-end:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel [rvmenuitem]{display:flex;align-items:center;gap:10px;width:100%;min-height:40px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;font-size:14px;line-height:1.4;text-align:start;padding:8px 10px;cursor:pointer}.rv-menu-panel [rvmenuitem][data-active=true]{background:color-mix(in srgb,currentColor 10%,transparent)}.rv-menu-panel [rvmenuitem]:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.rv-menu-panel [rvmenuitem]:focus-visible{outline:2px solid currentColor;outline-offset:-2px}.rv-menu-panel [rvmenuitem][aria-disabled=true]{opacity:.45;cursor:default}.rv-menu-panel [rvmenuitem] svg,.rv-menu-panel [rvmenuitem] [data-icon]{width:16px;height:16px;flex:none;color:var(--rv-menu-muted)}.rv-menu-panel [rvmenuitem][data-active=true] svg,.rv-menu-panel [rvmenuitem][data-active=true] [data-icon]{color:inherit}.rv-menu-panel{--rv-menu-duration: .16s;--rv-menu-easing: cubic-bezier(.16, 1, .3, 1);--rv-menu-from-y: -6px;transform-origin:top center;animation:rv-menu-in var(--rv-menu-duration) var(--rv-menu-easing)}.rv-menu-panel[data-placement=top-start],.rv-menu-panel[data-placement=top-end]{--rv-menu-from-y: 6px;transform-origin:bottom center}@media(prefers-reduced-motion:reduce){.rv-menu-panel{animation:none}}@keyframes rv-menu-in{0%{opacity:0;transform:translateY(var(--rv-menu-from-y)) scale(.96)}}\n"],
650
+ styles: [".rv-menu-trigger{display:inline-flex;align-items:center}.rv-menu-panel{display:flex;flex-direction:column;gap:2px}.rv-menu-panel hr{border:0;border-top:1px solid var(--rv-menu-border, #e0e0e0);margin:6px 4px}rv-menu{position:relative;display:inline-block}.rv-menu-panel[data-top-layer=true]{position:fixed;inset:auto;margin:0}.rv-menu-panel[data-positioned=false]{animation:none}.rv-menu-panel[data-top-layer=true][data-placement^=bottom][data-flipped=true]{transform-origin:bottom center}.rv-menu-panel[data-top-layer=true][data-placement^=top][data-flipped=true]{transform-origin:top center}.rv-menu-panel{position:absolute;z-index:var(--rv-menu-z, 500);min-width:var(--rv-menu-width, 16rem);max-width:calc(100dvw - 2rem);max-height:min(20rem,calc(100dvh - 2rem));overflow-y:auto;background:var(--rv-menu-bg, #ffffff);color:var(--rv-menu-fg, #111111);font-family:var(--rv-menu-font, system-ui, -apple-system, \"Segoe UI\", sans-serif);border:1px solid var(--rv-menu-border, #e0e0e0);border-radius:var(--rv-menu-radius, 12px);box-shadow:var(--rv-menu-shadow, 0 16px 48px rgba(0, 0, 0, .22));padding:var(--rv-menu-pad, 6px)}.rv-menu-panel[data-placement=bottom-start]{inset-block-start:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=bottom-end]{inset-block-start:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel[data-placement=top-start]{inset-block-end:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=top-end]{inset-block-end:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel [rvmenuitem]{display:flex;align-items:center;gap:10px;width:100%;min-height:40px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;font-size:14px;line-height:1.4;text-align:start;padding:8px 10px;cursor:pointer}.rv-menu-panel [rvmenuitem][data-active=true]{background:color-mix(in srgb,currentColor 10%,transparent)}.rv-menu-panel [rvmenuitem]:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.rv-menu-panel [rvmenuitem]:focus-visible{outline:2px solid currentColor;outline-offset:-2px}.rv-menu-panel [rvmenuitem][aria-disabled=true]{opacity:.45;cursor:default}.rv-menu-panel [rvmenuitem] svg,.rv-menu-panel [rvmenuitem] [data-icon]{width:16px;height:16px;flex:none;color:var(--rv-menu-muted)}.rv-menu-panel [rvmenuitem][data-active=true] svg,.rv-menu-panel [rvmenuitem][data-active=true] [data-icon]{color:inherit}.rv-menu-panel{transform-origin:top center;animation:rv-menu-in var(--rv-menu-duration, .16s) var(--rv-menu-easing, cubic-bezier(.16, 1, .3, 1))}.rv-menu-panel[data-placement=top-start],.rv-menu-panel[data-placement=top-end]{--rv-menu-from-y: 6px;transform-origin:bottom center}@media(prefers-reduced-motion:reduce){.rv-menu-panel{animation:none}}@keyframes rv-menu-in{0%{opacity:0;transform:translateY(var(--rv-menu-from-y, -6px)) scale(.96)}}\n"],
651
+ dependencies: [{
652
+ kind: "directive",
653
+ type: RiviniaMenuItem,
654
+ selector: "[rvMenuItem]",
655
+ inputs: [
656
+ "id",
657
+ "disabled",
658
+ "checked",
659
+ "label"
660
+ ]
661
+ }],
494
662
  encapsulation: i0.ViewEncapsulation.None
495
663
  });
496
664
  };
@@ -505,15 +673,19 @@ i0.ɵɵngDeclareClassMetadata({
505
673
  selector: "rv-menu",
506
674
  standalone: true,
507
675
  encapsulation: ViewEncapsulation.None,
676
+ imports: [RiviniaMenuItem],
508
677
  template: `
509
678
  <div class="rv-menu-trigger" (click)="onTriggerClick()" (keydown)="onTriggerKey($event)">
510
- <ng-content select="[rv-menu-trigger]"></ng-content>
679
+ <ng-content select="[rv-menu-trigger], [rvMenuTrigger]"></ng-content>
511
680
  </div>
512
681
  @if (controller().open()) {
513
682
  <div
514
683
  #panel
515
684
  class="rv-menu-panel"
685
+ [class]="panelClass()"
516
686
  role="menu"
687
+ [attr.id]="panelId"
688
+ [attr.aria-label]="menuLabel()"
517
689
  [attr.popover]="topLayerSupported ? 'manual' : null"
518
690
  [attr.data-placement]="placement()"
519
691
  [attr.data-flipped]="flipped()"
@@ -521,11 +693,24 @@ i0.ɵɵngDeclareClassMetadata({
521
693
  [attr.data-top-layer]="topLayerSupported"
522
694
  (keydown)="onPanelKey($event)"
523
695
  >
524
- <ng-content select="[rv-menu-panel]"></ng-content>
696
+ @if (dataItems().length > 0) {
697
+ @for (item of dataItems(); track item.id) {
698
+ <button
699
+ rvMenuItem
700
+ [id]="item.id"
701
+ [disabled]="item.disabled ?? false"
702
+ [checked]="checkedFor(item.id)"
703
+ [class]="itemClass()"
704
+ >
705
+ {{ item.label }}
706
+ </button>
707
+ }
708
+ }
709
+ <ng-content select="[rv-menu-panel], [rvMenuPanel]"></ng-content>
525
710
  </div>
526
711
  }
527
712
  `,
528
- styles: [".rv-menu-trigger{display:inline-flex;align-items:center}.rv-menu-panel{display:flex;flex-direction:column;gap:2px}.rv-menu-panel hr{border:0;border-top:1px solid var(--rv-menu-border);margin:6px 4px}rv-menu{position:relative;display:inline-block}.rv-menu-panel[data-top-layer=true]{position:fixed;inset:auto;margin:0}.rv-menu-panel[data-positioned=false]{animation:none}.rv-menu-panel[data-top-layer=true][data-placement^=bottom][data-flipped=true]{transform-origin:bottom center}.rv-menu-panel[data-top-layer=true][data-placement^=top][data-flipped=true]{transform-origin:top center}.rv-menu-panel{--rv-menu-bg: #ffffff;--rv-menu-fg: #111111;--rv-menu-muted: #555555;--rv-menu-border: #e0e0e0;--rv-menu-radius: 12px;--rv-menu-shadow: 0 16px 48px rgba(0, 0, 0, .22);--rv-menu-pad: 6px;--rv-menu-width: 16rem;--rv-menu-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;position:absolute;z-index:var(--rv-menu-z, 500);min-width:var(--rv-menu-width);max-width:calc(100dvw - 2rem);max-height:min(20rem,calc(100dvh - 2rem));overflow-y:auto;background:var(--rv-menu-bg);color:var(--rv-menu-fg);font-family:var(--rv-menu-font);border:1px solid var(--rv-menu-border);border-radius:var(--rv-menu-radius);box-shadow:var(--rv-menu-shadow);padding:var(--rv-menu-pad)}@media(prefers-color-scheme:dark){.rv-menu-panel{--rv-menu-bg: #161616;--rv-menu-fg: #f2f2f2;--rv-menu-muted: #a8a8a8;--rv-menu-border: #333333}}.rv-menu-panel[data-placement=bottom-start]{inset-block-start:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=bottom-end]{inset-block-start:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel[data-placement=top-start]{inset-block-end:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=top-end]{inset-block-end:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel [rvmenuitem]{display:flex;align-items:center;gap:10px;width:100%;min-height:40px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;font-size:14px;line-height:1.4;text-align:start;padding:8px 10px;cursor:pointer}.rv-menu-panel [rvmenuitem][data-active=true]{background:color-mix(in srgb,currentColor 10%,transparent)}.rv-menu-panel [rvmenuitem]:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.rv-menu-panel [rvmenuitem]:focus-visible{outline:2px solid currentColor;outline-offset:-2px}.rv-menu-panel [rvmenuitem][aria-disabled=true]{opacity:.45;cursor:default}.rv-menu-panel [rvmenuitem] svg,.rv-menu-panel [rvmenuitem] [data-icon]{width:16px;height:16px;flex:none;color:var(--rv-menu-muted)}.rv-menu-panel [rvmenuitem][data-active=true] svg,.rv-menu-panel [rvmenuitem][data-active=true] [data-icon]{color:inherit}.rv-menu-panel{--rv-menu-duration: .16s;--rv-menu-easing: cubic-bezier(.16, 1, .3, 1);--rv-menu-from-y: -6px;transform-origin:top center;animation:rv-menu-in var(--rv-menu-duration) var(--rv-menu-easing)}.rv-menu-panel[data-placement=top-start],.rv-menu-panel[data-placement=top-end]{--rv-menu-from-y: 6px;transform-origin:bottom center}@media(prefers-reduced-motion:reduce){.rv-menu-panel{animation:none}}@keyframes rv-menu-in{0%{opacity:0;transform:translateY(var(--rv-menu-from-y)) scale(.96)}}\n"]
713
+ styles: [".rv-menu-trigger{display:inline-flex;align-items:center}.rv-menu-panel{display:flex;flex-direction:column;gap:2px}.rv-menu-panel hr{border:0;border-top:1px solid var(--rv-menu-border, #e0e0e0);margin:6px 4px}rv-menu{position:relative;display:inline-block}.rv-menu-panel[data-top-layer=true]{position:fixed;inset:auto;margin:0}.rv-menu-panel[data-positioned=false]{animation:none}.rv-menu-panel[data-top-layer=true][data-placement^=bottom][data-flipped=true]{transform-origin:bottom center}.rv-menu-panel[data-top-layer=true][data-placement^=top][data-flipped=true]{transform-origin:top center}.rv-menu-panel{position:absolute;z-index:var(--rv-menu-z, 500);min-width:var(--rv-menu-width, 16rem);max-width:calc(100dvw - 2rem);max-height:min(20rem,calc(100dvh - 2rem));overflow-y:auto;background:var(--rv-menu-bg, #ffffff);color:var(--rv-menu-fg, #111111);font-family:var(--rv-menu-font, system-ui, -apple-system, \"Segoe UI\", sans-serif);border:1px solid var(--rv-menu-border, #e0e0e0);border-radius:var(--rv-menu-radius, 12px);box-shadow:var(--rv-menu-shadow, 0 16px 48px rgba(0, 0, 0, .22));padding:var(--rv-menu-pad, 6px)}.rv-menu-panel[data-placement=bottom-start]{inset-block-start:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=bottom-end]{inset-block-start:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel[data-placement=top-start]{inset-block-end:calc(100% + 6px);inset-inline-start:0}.rv-menu-panel[data-placement=top-end]{inset-block-end:calc(100% + 6px);inset-inline-end:0}.rv-menu-panel [rvmenuitem]{display:flex;align-items:center;gap:10px;width:100%;min-height:40px;border:0;border-radius:8px;background:transparent;color:inherit;font:inherit;font-size:14px;line-height:1.4;text-align:start;padding:8px 10px;cursor:pointer}.rv-menu-panel [rvmenuitem][data-active=true]{background:color-mix(in srgb,currentColor 10%,transparent)}.rv-menu-panel [rvmenuitem]:hover{background:color-mix(in srgb,currentColor 8%,transparent)}.rv-menu-panel [rvmenuitem]:focus-visible{outline:2px solid currentColor;outline-offset:-2px}.rv-menu-panel [rvmenuitem][aria-disabled=true]{opacity:.45;cursor:default}.rv-menu-panel [rvmenuitem] svg,.rv-menu-panel [rvmenuitem] [data-icon]{width:16px;height:16px;flex:none;color:var(--rv-menu-muted)}.rv-menu-panel [rvmenuitem][data-active=true] svg,.rv-menu-panel [rvmenuitem][data-active=true] [data-icon]{color:inherit}.rv-menu-panel{transform-origin:top center;animation:rv-menu-in var(--rv-menu-duration, .16s) var(--rv-menu-easing, cubic-bezier(.16, 1, .3, 1))}.rv-menu-panel[data-placement=top-start],.rv-menu-panel[data-placement=top-end]{--rv-menu-from-y: 6px;transform-origin:bottom center}@media(prefers-reduced-motion:reduce){.rv-menu-panel{animation:none}}@keyframes rv-menu-in{0%{opacity:0;transform:translateY(var(--rv-menu-from-y, -6px)) scale(.96)}}\n"]
529
714
  }]
530
715
  }],
531
716
  ctorParameters: () => [],
@@ -546,6 +731,38 @@ i0.ɵɵngDeclareClassMetadata({
546
731
  required: false
547
732
  }]
548
733
  }],
734
+ menuLabel: [{
735
+ type: i0.Input,
736
+ args: [{
737
+ isSignal: true,
738
+ alias: "menuLabel",
739
+ required: false
740
+ }]
741
+ }],
742
+ items: [{
743
+ type: i0.Input,
744
+ args: [{
745
+ isSignal: true,
746
+ alias: "items",
747
+ required: false
748
+ }]
749
+ }],
750
+ panelClass: [{
751
+ type: i0.Input,
752
+ args: [{
753
+ isSignal: true,
754
+ alias: "panelClass",
755
+ required: false
756
+ }]
757
+ }],
758
+ itemClass: [{
759
+ type: i0.Input,
760
+ args: [{
761
+ isSignal: true,
762
+ alias: "itemClass",
763
+ required: false
764
+ }]
765
+ }],
549
766
  picked: [{
550
767
  type: i0.Output,
551
768
  args: ["picked"]
@@ -557,6 +774,13 @@ i0.ɵɵngDeclareClassMetadata({
557
774
  closed: [{
558
775
  type: i0.Output,
559
776
  args: ["closed"]
777
+ }],
778
+ projectedItems: [{
779
+ type: i0.ContentChildren,
780
+ args: [i0.forwardRef(() => RiviniaMenuItem), {
781
+ descendants: true,
782
+ isSignal: true
783
+ }]
560
784
  }]
561
785
  }
562
786
  });