@reliability-design/web 1.0.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/components/rel-accordion/rel-accordion.ts +20 -31
  2. package/dist/components/rel-alert/rel-alert.ts +81 -11
  3. package/dist/components/rel-avatar/rel-avatar.ts +6 -3
  4. package/dist/components/rel-badge/rel-badge.ts +8 -3
  5. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +6 -6
  6. package/dist/components/rel-button/rel-button.ts +118 -16
  7. package/dist/components/rel-calendar/rel-calendar.ts +16 -9
  8. package/dist/components/rel-card/rel-card.ts +9 -17
  9. package/dist/components/rel-cardmedia/rel-cardmedia.ts +20 -23
  10. package/dist/components/rel-carousel/rel-carousel.ts +596 -469
  11. package/dist/components/rel-checkbox/rel-checkbox.ts +98 -15
  12. package/dist/components/rel-chip/rel-chip.ts +291 -186
  13. package/dist/components/rel-container/rel-container.ts +2 -2
  14. package/dist/components/rel-date-picker/rel-date-picker.ts +23 -19
  15. package/dist/components/rel-divider/rel-divider.ts +12 -3
  16. package/dist/components/rel-drawer/rel-drawer-footer.ts +27 -0
  17. package/dist/components/rel-drawer/rel-drawer.ts +57 -113
  18. package/dist/components/rel-kbd/rel-kbd.ts +94 -118
  19. package/dist/components/rel-layout/rel-layout.ts +49 -58
  20. package/dist/components/rel-menu/rel-menu.ts +359 -327
  21. package/dist/components/rel-menubar/rel-menubar-menu.css +3 -0
  22. package/dist/components/rel-menubar/rel-menubar-menu.ts +2 -1
  23. package/dist/components/rel-menubar/rel-menubar.ts +602 -576
  24. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +240 -143
  25. package/dist/components/rel-nav-menu/rel-nav-menu.ts +227 -331
  26. package/dist/components/rel-navbar/rel-navbar.ts +13 -0
  27. package/dist/components/rel-otp/rel-otp.ts +22 -25
  28. package/dist/components/rel-pagination/rel-pagination.ts +112 -63
  29. package/dist/components/rel-popover/rel-popover-content.ts +6 -5
  30. package/dist/components/rel-popover/rel-popover.ts +42 -20
  31. package/dist/components/rel-progress/rel-progress.ts +10 -9
  32. package/dist/components/rel-radio/rel-radio.ts +228 -155
  33. package/dist/components/rel-range-picker/rel-range-picker.css +349 -0
  34. package/dist/components/rel-range-picker/rel-range-picker.ts +1079 -0
  35. package/dist/components/rel-resizable/rel-resizable-handle.ts +12 -15
  36. package/dist/components/rel-segmented-button/rel-segmented-button.ts +41 -20
  37. package/dist/components/rel-select/rel-select.ts +112 -27
  38. package/dist/components/rel-sheet/rel-sheet.ts +362 -683
  39. package/dist/components/rel-sidebar/rel-sidebar.ts +13 -5
  40. package/dist/components/rel-slider/rel-slider.ts +74 -29
  41. package/dist/components/rel-snackbar/rel-snackbar.ts +67 -37
  42. package/dist/components/rel-stepper/rel-stepper.ts +43 -31
  43. package/dist/components/rel-switch/rel-switch.ts +26 -12
  44. package/dist/components/rel-tab/rel-tab.ts +57 -37
  45. package/dist/components/rel-table/rel-table-body.ts +5 -0
  46. package/dist/components/rel-table/rel-table-caption.ts +6 -1
  47. package/dist/components/rel-table/rel-table-cell.ts +42 -49
  48. package/dist/components/rel-table/rel-table-footer.ts +5 -0
  49. package/dist/components/rel-table/rel-table-head.ts +13 -1
  50. package/dist/components/rel-table/rel-table-header.ts +5 -0
  51. package/dist/components/rel-table/rel-table-row.ts +7 -3
  52. package/dist/components/rel-table/rel-table.ts +8 -2
  53. package/dist/components/rel-textfield/rel-textfield.ts +153 -44
  54. package/dist/components/rel-toast/rel-toast.ts +56 -22
  55. package/dist/components/rel-tooltip/rel-tooltip.ts +52 -9
  56. package/dist/components/rel-topbar/rel-topbar-actions.ts +24 -0
  57. package/dist/components/rel-topbar/rel-topbar-brand.ts +47 -0
  58. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +63 -0
  59. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +28 -0
  60. package/dist/components/rel-topbar/rel-topbar-item.ts +320 -0
  61. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +24 -0
  62. package/dist/components/rel-topbar/rel-topbar-profile.ts +222 -0
  63. package/dist/components/rel-topbar/rel-topbar-utilities.ts +24 -0
  64. package/dist/components/rel-topbar/rel-topbar.ts +438 -0
  65. package/dist/index.d.mts +375 -411
  66. package/dist/index.d.ts +375 -411
  67. package/dist/index.js +7587 -7980
  68. package/dist/index.js.map +1 -1
  69. package/dist/index.mjs +7647 -8023
  70. package/dist/index.mjs.map +1 -1
  71. package/dist/rel-menubar-menu-BEEQDAXW.css +3 -0
  72. package/dist/styles/vars-v2.css +525 -0
  73. package/dist/styles/vars.css +79 -27
  74. package/dist/{vars-ITL2K3KO.css → vars-I2PC74XD.css} +79 -27
  75. package/dist/vars-v2-VMGWTFJZ.css +525 -0
  76. package/package.json +3 -1
  77. package/dist/components/rel-quote-page/README.md +0 -166
  78. package/dist/components/rel-quote-page/rel-quote-page.ts +0 -816
@@ -1,576 +1,602 @@
1
- import { LitElement, html, css } from "lit";
2
- import { customElement, state, query } from "lit/decorators.js";
3
- import { RelMenubarMenu } from "./rel-menubar-menu";
4
-
5
- interface MenuState {
6
- menu: RelMenubarMenu;
7
- open: boolean;
8
- panelElement?: HTMLElement;
9
- }
10
-
11
- @customElement("rel-menubar")
12
- export class RelMenubar extends LitElement {
13
- @state() private menus: MenuState[] = [];
14
- @state() private openMenuIndex: number = -1;
15
- @state() private focusedTriggerIndex: number = 0;
16
- @state() private focusedItemIndex: number = 0;
17
- @state() private openSubmenuElement: HTMLElement | null = null;
18
-
19
- @query(".menubar-container") private container!: HTMLElement;
20
-
21
- private hoverSwitchEnabled = false;
22
-
23
- static styles = css`
24
- :host {
25
- display: block;
26
- }
27
-
28
- .menubar-container {
29
- display: flex;
30
- align-items: center;
31
- background: var(--rel-colors-gray-light);
32
- border: var(--rel-borders-width-stroke-100) solid var(--rel-colors-gray-border);
33
- border-radius: var(--rel-borders-radius-border-radius-200);
34
- padding: var(--rel-spacing-space-3xs);
35
- gap: var(--rel-spacing-space-3xs);
36
- font-family: var(--rel-fontFamilies-primary);
37
- font-size: var(--rel-fontSizes-sm);
38
- }
39
-
40
- .menubar-trigger {
41
- padding: var(--rel-spacing-space-2xs) var(--rel-spacing-space-sm);
42
- border-radius: var(--rel-borders-radius-border-radius-100);
43
- background: transparent;
44
- border: none;
45
- cursor: pointer;
46
- font-family: inherit;
47
- font-size: inherit;
48
- color: var(--rel-colors-gray-heading);
49
- transition: background 0.15s ease;
50
- user-select: none;
51
- outline: none;
52
- }
53
-
54
- .menubar-trigger:hover {
55
- background: var(--rel-surface-secondary, #fafafa);
56
- }
57
-
58
- .menubar-trigger:focus-visible {
59
- outline: 2px solid var(--rel-colors-blue-primary);
60
- outline-offset: 2px;
61
- }
62
-
63
- .menubar-trigger.active {
64
- background: var(--rel-surface-secondary, #fafafa);
65
- }
66
-
67
- .menubar-panel {
68
- position: fixed;
69
- min-width: 220px;
70
- background: var(--rel-colors-white);
71
- border: var(--rel-borders-width-stroke-100) solid var(--rel-colors-gray-border);
72
- border-radius: var(--rel-borders-radius-border-radius-200);
73
- box-shadow: var(--rel-shadows-shadow-300);
74
- padding: var(--rel-spacing-space-2xs) 0;
75
- z-index: 1000;
76
- display: none;
77
- }
78
-
79
- .menubar-panel.open {
80
- display: block;
81
- }
82
-
83
- .menu-item {
84
- position: relative;
85
- display: flex;
86
- align-items: center;
87
- justify-content: space-between;
88
- gap: var(--rel-spacing-space-sm);
89
- padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-md);
90
- cursor: pointer;
91
- transition: background 0.15s ease;
92
- user-select: none;
93
- color: var(--rel-colors-gray-heading);
94
- line-height: var(--rel-lineHeights-sm);
95
- }
96
-
97
- .menu-item:hover,
98
- .menu-item.focused {
99
- background: var(--rel-surface-secondary, #fafafa);
100
- }
101
-
102
- .menu-item[data-disabled="true"] {
103
- opacity: 0.5;
104
- cursor: not-allowed;
105
- }
106
-
107
- .menu-separator {
108
- height: var(--rel-borders-width-stroke-100);
109
- background: var(--rel-colors-gray-divider);
110
- margin: var(--rel-spacing-space-2xs) var(--rel-spacing-space-xs);
111
- }
112
-
113
- .menu-checkbox,
114
- .menu-radio-item {
115
- display: flex;
116
- align-items: center;
117
- gap: var(--rel-spacing-space-xs);
118
- padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-md);
119
- cursor: pointer;
120
- transition: background 0.15s ease;
121
- user-select: none;
122
- color: var(--rel-colors-gray-heading);
123
- line-height: var(--rel-lineHeights-sm);
124
- }
125
-
126
- .menu-checkbox:hover,
127
- .menu-checkbox.focused,
128
- .menu-radio-item:hover,
129
- .menu-radio-item.focused {
130
- background: var(--rel-surface-secondary, #fafafa);
131
- }
132
-
133
- .menu-checkbox input[type="checkbox"],
134
- .menu-radio-item::before {
135
- margin: 0;
136
- }
137
-
138
- .menu-radio-item {
139
- position: relative;
140
- padding-left: calc(var(--rel-spacing-space-md) + var(--rel-spacing-space-md));
141
- }
142
-
143
- .menu-radio-item::before {
144
- content: "";
145
- position: absolute;
146
- left: var(--rel-spacing-space-md);
147
- width: 12px;
148
- height: 12px;
149
- border-radius: 50%;
150
- border: var(--rel-borders-width-stroke-150) solid var(--rel-colors-gray-border);
151
- background: var(--rel-colors-white);
152
- }
153
-
154
- .menu-radio-item.checked::after {
155
- content: "";
156
- position: absolute;
157
- left: calc(var(--rel-spacing-space-md) + 3px);
158
- width: 6px;
159
- height: 6px;
160
- border-radius: 50%;
161
- background: var(--rel-colors-blue-primary);
162
- }
163
-
164
- .menu-submenu-trigger {
165
- position: relative;
166
- }
167
-
168
- .menu-submenu-trigger::after {
169
- content: "▸";
170
- opacity: 0.6;
171
- font-size: var(--rel-fontSizes-sm);
172
- }
173
-
174
- .submenu-panel {
175
- position: fixed;
176
- min-width: 200px;
177
- background: var(--rel-colors-white);
178
- border: var(--rel-borders-width-stroke-100) solid var(--rel-colors-gray-border);
179
- border-radius: var(--rel-borders-radius-border-radius-200);
180
- box-shadow: var(--rel-shadows-shadow-300);
181
- padding: var(--rel-spacing-space-2xs) 0;
182
- z-index: 1001;
183
- display: none;
184
- }
185
-
186
- .submenu-panel.open {
187
- display: block;
188
- }
189
-
190
- .menu-shortcut {
191
- opacity: 0.6;
192
- font-size: var(--rel-fontSizes-xs);
193
- margin-left: auto;
194
- display: flex;
195
- align-items: center;
196
- gap: var(--rel-spacing-space-3xs);
197
- }
198
- `;
199
-
200
- connectedCallback() {
201
- super.connectedCallback();
202
- this.discoverMenus();
203
- document.addEventListener("click", this.handleOutsideClick);
204
- }
205
-
206
- disconnectedCallback() {
207
- super.disconnectedCallback();
208
- document.removeEventListener("click", this.handleOutsideClick);
209
- }
210
-
211
- private discoverMenus() {
212
- const menuElements = Array.from(
213
- this.querySelectorAll("rel-menubar-menu")
214
- ) as RelMenubarMenu[];
215
- this.menus = menuElements.map((menu) => ({ menu, open: false }));
216
- }
217
-
218
- private handleOutsideClick = (e: MouseEvent) => {
219
- const path = e.composedPath() as HTMLElement[];
220
- const isInside = path.some(
221
- (el) =>
222
- el === this ||
223
- el.classList?.contains("menubar-panel") ||
224
- el.classList?.contains("submenu-panel")
225
- );
226
- if (!isInside) {
227
- this.closeAllMenus();
228
- }
229
- };
230
-
231
- private closeAllMenus() {
232
- this.openMenuIndex = -1;
233
- this.openSubmenuElement = null;
234
- this.hoverSwitchEnabled = false;
235
- this.requestUpdate();
236
- }
237
-
238
- private openMenu(index: number, triggerElement: HTMLElement) {
239
- this.openMenuIndex = index;
240
- this.focusedItemIndex = 0;
241
- this.openSubmenuElement = null;
242
- this.requestUpdate();
243
-
244
- // Position the panel below the trigger
245
- this.updateComplete.then(() => {
246
- const panel = this.shadowRoot?.querySelector(
247
- `.menubar-panel[data-menu-index="${index}"]`
248
- ) as HTMLElement;
249
- if (panel && triggerElement) {
250
- const rect = triggerElement.getBoundingClientRect();
251
- panel.style.top = `${rect.bottom + 4}px`;
252
- panel.style.left = `${rect.left}px`;
253
- }
254
- });
255
- }
256
-
257
- private handleTriggerClick(index: number, event: Event) {
258
- const target = event.currentTarget as HTMLElement;
259
- if (this.openMenuIndex === index) {
260
- this.closeAllMenus();
261
- } else {
262
- this.openMenu(index, target);
263
- this.hoverSwitchEnabled = true;
264
- }
265
- }
266
-
267
- private handleTriggerMouseEnter(index: number, event: Event) {
268
- if (this.openMenuIndex !== -1 && this.hoverSwitchEnabled) {
269
- const target = event.currentTarget as HTMLElement;
270
- this.openMenu(index, target);
271
- }
272
- }
273
-
274
- private handleTriggerKeyDown(event: KeyboardEvent) {
275
- const { key } = event;
276
-
277
- if (key === "ArrowRight") {
278
- event.preventDefault();
279
- this.focusedTriggerIndex = Math.min(
280
- this.menus.length - 1,
281
- this.focusedTriggerIndex + 1
282
- );
283
- this.focusTrigger(this.focusedTriggerIndex);
284
- } else if (key === "ArrowLeft") {
285
- event.preventDefault();
286
- this.focusedTriggerIndex = Math.max(0, this.focusedTriggerIndex - 1);
287
- this.focusTrigger(this.focusedTriggerIndex);
288
- } else if (key === "Enter" || key === " ") {
289
- event.preventDefault();
290
- const trigger = this.shadowRoot?.querySelector(
291
- `.menubar-trigger[data-index="${this.focusedTriggerIndex}"]`
292
- ) as HTMLElement;
293
- if (trigger) {
294
- this.openMenu(this.focusedTriggerIndex, trigger);
295
- // Focus first item
296
- setTimeout(() => this.focusMenuItem(0), 50);
297
- }
298
- } else if (key === "ArrowDown" && this.openMenuIndex !== -1) {
299
- event.preventDefault();
300
- this.focusMenuItem(0);
301
- }
302
- }
303
-
304
- private focusTrigger(index: number) {
305
- const trigger = this.shadowRoot?.querySelector(
306
- `.menubar-trigger[data-index="${index}"]`
307
- ) as HTMLElement;
308
- trigger?.focus();
309
- }
310
-
311
- private handlePanelKeyDown(event: KeyboardEvent) {
312
- const { key } = event;
313
- const menuContent = this.menus[this.openMenuIndex]?.menu;
314
- if (!menuContent) return;
315
-
316
- const items = this.getFocusableItems(menuContent);
317
-
318
- if (key === "ArrowDown") {
319
- event.preventDefault();
320
- this.focusedItemIndex = Math.min(items.length - 1, this.focusedItemIndex + 1);
321
- this.requestUpdate();
322
- } else if (key === "ArrowUp") {
323
- event.preventDefault();
324
- this.focusedItemIndex = Math.max(0, this.focusedItemIndex - 1);
325
- this.requestUpdate();
326
- } else if (key === "ArrowRight") {
327
- event.preventDefault();
328
- const currentItem = items[this.focusedItemIndex];
329
- if (currentItem?.hasAttribute("data-submenu")) {
330
- this.openSubmenu(currentItem as HTMLElement);
331
- }
332
- } else if (key === "ArrowLeft") {
333
- event.preventDefault();
334
- if (this.openSubmenuElement) {
335
- this.openSubmenuElement = null;
336
- this.requestUpdate();
337
- }
338
- } else if (key === "Escape") {
339
- event.preventDefault();
340
- this.closeAllMenus();
341
- this.focusTrigger(this.openMenuIndex);
342
- } else if (key === "Enter" || key === " ") {
343
- event.preventDefault();
344
- const currentItem = items[this.focusedItemIndex];
345
- if (currentItem) {
346
- this.handleItemClick(currentItem as HTMLElement);
347
- }
348
- }
349
- }
350
-
351
- private getFocusableItems(menuContent: RelMenubarMenu): Element[] {
352
- return Array.from(menuContent.children).filter(
353
- (el) =>
354
- el.hasAttribute("data-item") ||
355
- el.hasAttribute("data-checkbox") ||
356
- el.hasAttribute("data-radio") ||
357
- el.hasAttribute("data-submenu")
358
- );
359
- }
360
-
361
- private focusMenuItem(index: number) {
362
- this.focusedItemIndex = index;
363
- const panel = this.shadowRoot?.querySelector(
364
- `.menubar-panel[data-menu-index="${this.openMenuIndex}"]`
365
- ) as HTMLElement;
366
- panel?.focus();
367
- }
368
-
369
- private handleItemClick(item: HTMLElement) {
370
- if (item.hasAttribute("data-checkbox")) {
371
- const checkbox = item.querySelector('input[type="checkbox"]') as HTMLInputElement;
372
- if (checkbox) {
373
- checkbox.checked = !checkbox.checked;
374
- this.dispatchChangeEvent("checkbox", item.textContent?.trim() || "", checkbox.checked);
375
- }
376
- } else if (item.hasAttribute("data-radio")) {
377
- const radioGroup = item.closest("[data-radio-group]") as HTMLElement;
378
- const value = item.getAttribute("data-radio") || item.getAttribute("value") || "";
379
- const name = radioGroup?.getAttribute("name") || "";
380
-
381
- // Update visual state
382
- const allRadios = radioGroup?.querySelectorAll("[data-radio]");
383
- allRadios?.forEach((radio) => radio.removeAttribute("data-checked"));
384
- item.setAttribute("data-checked", "true");
385
-
386
- // Update internal value
387
- if (radioGroup) {
388
- radioGroup.setAttribute("value", value);
389
- }
390
-
391
- this.dispatchChangeEvent("radio", name, value);
392
- this.requestUpdate();
393
- } else if (item.hasAttribute("data-submenu")) {
394
- this.openSubmenu(item);
395
- } else {
396
- // Regular item click
397
- this.closeAllMenus();
398
- }
399
- }
400
-
401
- private openSubmenu(submenuTrigger: HTMLElement) {
402
- this.openSubmenuElement = submenuTrigger;
403
- this.requestUpdate();
404
-
405
- this.updateComplete.then(() => {
406
- const submenuPanel = this.shadowRoot?.querySelector(
407
- `.submenu-panel[data-submenu-id="${submenuTrigger.getAttribute("data-submenu") || ""}"]`
408
- ) as HTMLElement;
409
- if (submenuPanel) {
410
- const rect = submenuTrigger.getBoundingClientRect();
411
- submenuPanel.style.top = `${rect.top}px`;
412
- submenuPanel.style.left = `${rect.right + 4}px`;
413
- }
414
- });
415
- }
416
-
417
- private handleSubmenuItemClick(item: HTMLElement) {
418
- // Regular item - just close
419
- this.closeAllMenus();
420
- }
421
-
422
- private dispatchChangeEvent(type: "checkbox" | "radio", labelOrName: string, value: string | boolean) {
423
- this.dispatchEvent(
424
- new CustomEvent("rel-menubar-change", {
425
- detail: { type, [type === "checkbox" ? "label" : "name"]: labelOrName, [type === "checkbox" ? "checked" : "value"]: value },
426
- bubbles: true,
427
- composed: true,
428
- })
429
- );
430
- }
431
-
432
- private renderMenuItem(item: Element, index: number): any {
433
- const isFocused = index === this.focusedItemIndex;
434
-
435
- if (item.hasAttribute("data-item")) {
436
- const button = item as HTMLButtonElement;
437
- const disabled = button.disabled;
438
- const shortcut = button.getAttribute("data-shortcut") || "";
439
-
440
- return html`
441
- <div
442
- class="menu-item ${isFocused ? "focused" : ""}"
443
- data-disabled="${disabled}"
444
- @click="${() => !disabled && this.handleItemClick(item as HTMLElement)}"
445
- >
446
- <span>${button.textContent}</span>
447
- ${shortcut ? html`<span class="menu-shortcut">${shortcut}</span>` : ""}
448
- </div>
449
- `;
450
- }
451
-
452
- if (item.hasAttribute("data-separator")) {
453
- return html`<div class="menu-separator"></div>`;
454
- }
455
-
456
- if (item.hasAttribute("data-checkbox")) {
457
- const label = item as HTMLLabelElement;
458
- const checkbox = label.querySelector('input[type="checkbox"]') as HTMLInputElement;
459
-
460
- return html`
461
- <label
462
- class="menu-checkbox ${isFocused ? "focused" : ""}"
463
- @click="${(e: Event) => {
464
- e.preventDefault();
465
- this.handleItemClick(item as HTMLElement);
466
- }}"
467
- >
468
- <input type="checkbox" .checked="${checkbox?.checked || false}" @click="${(e: Event) => e.stopPropagation()}" />
469
- <span>${label.textContent?.replace(/\s+/g, " ").trim()}</span>
470
- </label>
471
- `;
472
- }
473
-
474
- if (item.hasAttribute("data-submenu")) {
475
- const label = item.getAttribute("label") || item.textContent?.trim() || "";
476
- const submenuId = item.getAttribute("data-submenu") || label;
477
-
478
- return html`
479
- <div
480
- class="menu-item menu-submenu-trigger ${isFocused ? "focused" : ""}"
481
- @click="${() => this.openSubmenu(item as HTMLElement)}"
482
- @mouseenter="${() => this.openSubmenu(item as HTMLElement)}"
483
- >
484
- <span>${label}</span>
485
- </div>
486
- ${this.renderSubmenu(item as HTMLElement, submenuId)}
487
- `;
488
- }
489
-
490
- if (item.hasAttribute("data-radio-group")) {
491
- const radioButtons = Array.from(item.querySelectorAll("[data-radio]"));
492
- const currentValue = item.getAttribute("value") || "";
493
-
494
- return html`
495
- ${radioButtons.map((radio) => {
496
- const value = radio.getAttribute("data-radio") || radio.getAttribute("value") || "";
497
- const isChecked = value === currentValue || radio.hasAttribute("data-checked");
498
- const text = radio.textContent?.trim() || "";
499
-
500
- return html`
501
- <div
502
- class="menu-radio-item ${isChecked ? "checked" : ""} ${isFocused ? "focused" : ""}"
503
- @click="${() => this.handleItemClick(radio as HTMLElement)}"
504
- >
505
- <span>${text}</span>
506
- </div>
507
- `;
508
- })}
509
- `;
510
- }
511
-
512
- return null;
513
- }
514
-
515
- private renderSubmenu(submenuElement: HTMLElement, submenuId: string) {
516
- const isOpen = this.openSubmenuElement === submenuElement;
517
- const submenuItems = Array.from(submenuElement.children);
518
-
519
- return html`
520
- <div class="submenu-panel ${isOpen ? "open" : ""}" data-submenu-id="${submenuId}">
521
- ${submenuItems.map((subItem) => {
522
- if (subItem.hasAttribute("data-item")) {
523
- const button = subItem as HTMLButtonElement;
524
- return html`
525
- <div
526
- class="menu-item"
527
- @click="${() => this.handleSubmenuItemClick(subItem as HTMLElement)}"
528
- >
529
- <span>${button.textContent}</span>
530
- </div>
531
- `;
532
- }
533
- return null;
534
- })}
535
- </div>
536
- `;
537
- }
538
-
539
- private renderMenuPanel(menuState: MenuState, index: number) {
540
- const isOpen = index === this.openMenuIndex;
541
- const items = Array.from(menuState.menu.children);
542
-
543
- return html`
544
- <div
545
- class="menubar-panel ${isOpen ? "open" : ""}"
546
- data-menu-index="${index}"
547
- tabindex="-1"
548
- @keydown="${this.handlePanelKeyDown}"
549
- >
550
- ${items.map((item, idx) => this.renderMenuItem(item, idx))}
551
- </div>
552
- `;
553
- }
554
-
555
- render() {
556
- return html`
557
- <div class="menubar-container">
558
- ${this.menus.map(
559
- (menuState, index) => html`
560
- <button
561
- class="menubar-trigger ${index === this.openMenuIndex ? "active" : ""}"
562
- data-index="${index}"
563
- @click="${(e: Event) => this.handleTriggerClick(index, e)}"
564
- @mouseenter="${(e: Event) => this.handleTriggerMouseEnter(index, e)}"
565
- @keydown="${this.handleTriggerKeyDown}"
566
- >
567
- ${menuState.menu.label}
568
- </button>
569
- `
570
- )}
571
- </div>
572
- ${this.menus.map((menuState, index) => this.renderMenuPanel(menuState, index))}
573
- <slot style="display: none;"></slot>
574
- `;
575
- }
576
- }
1
+ import { LitElement, html } from "lit";
2
+ import { customElement, state } from "lit/decorators.js";
3
+ import { RelMenubarMenu } from "./rel-menubar-menu";
4
+ import "@reliability-design/icons/rel-icon";
5
+ import "../rel-checkbox/rel-checkbox.js";
6
+ import "../rel-radio/rel-radio.js";
7
+ import "../../styles/vars-v2.css";
8
+
9
+ interface MenuState {
10
+ menu: RelMenubarMenu;
11
+ open: boolean;
12
+ panelElement?: HTMLElement;
13
+ }
14
+
15
+ @customElement("rel-menubar")
16
+ export class RelMenubar extends LitElement {
17
+ @state() private menus: MenuState[] = [];
18
+ @state() private openMenuIndex: number = -1;
19
+ @state() private focusedTriggerIndex: number = 0;
20
+ @state() private focusedItemIndex: number = -1;
21
+ @state() private openSubmenuElement: HTMLElement | null = null;
22
+
23
+ private hoverSwitchEnabled = false;
24
+ private _onScroll?: () => void;
25
+
26
+ public static readonly _css = `
27
+ :host {
28
+ display: block;
29
+ }
30
+
31
+ :host > slot {
32
+ display: none;
33
+ }
34
+
35
+ .menubar-container {
36
+ display: flex;
37
+ align-items: stretch;
38
+ background: var(--rel-color-bg-secondary);
39
+ border: var(--rel-borders-width-stroke-100) solid var(--rel-color-border-default);
40
+ border-radius: var(--rel-borders-radius-border-radius-200);
41
+ overflow: hidden;
42
+ padding: 0;
43
+ gap: var(--rel-spacing-space-3xs);
44
+ font-family: var(--rel-fontFamilies-primary);
45
+ font-size: var(--rel-fontSizes-sm);
46
+ height: 40px;
47
+ }
48
+
49
+ .menubar-trigger {
50
+ display: flex;
51
+ align-items: center;
52
+ align-self: stretch;
53
+ height: 100%;
54
+ padding: 0 var(--rel-spacing-space-sm);
55
+ background: transparent;
56
+ border: none;
57
+ cursor: pointer;
58
+ font-family: inherit;
59
+ font-size: inherit;
60
+ color: var(--rel-color-text-heading);
61
+ transition: background 0.15s ease;
62
+ user-select: none;
63
+ outline: none;
64
+ }
65
+
66
+ .menubar-trigger:hover,
67
+ .menubar-trigger.active {
68
+ background: var(--rel-color-bg-selected);
69
+ }
70
+
71
+ .menubar-trigger:focus-visible {
72
+ outline: 2px solid var(--rel-color-border-focus);
73
+ outline-offset: 2px;
74
+ }
75
+
76
+ .menubar-panel {
77
+ position: fixed;
78
+ min-width: 220px;
79
+ background: var(--rel-color-bg-primary);
80
+ border: var(--rel-borders-width-stroke-100) solid var(--rel-color-border-default);
81
+ border-radius: var(--rel-borders-radius-border-radius-200);
82
+ box-shadow: var(--rel-shadows-shadow-300);
83
+ padding: 0;
84
+ overflow: hidden;
85
+ z-index: 1000;
86
+ display: none;
87
+ }
88
+
89
+ .menubar-panel.open {
90
+ display: block;
91
+ }
92
+
93
+ .menu-item {
94
+ position: relative;
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: space-between;
98
+ gap: var(--rel-spacing-space-sm);
99
+ padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-md);
100
+ cursor: pointer;
101
+ transition: background 0.15s ease;
102
+ user-select: none;
103
+ color: var(--rel-color-text-heading);
104
+ line-height: var(--rel-lineHeights-sm);
105
+ }
106
+
107
+ .menu-item:hover,
108
+ .menu-item.focused {
109
+ background: var(--rel-color-bg-selected);
110
+ }
111
+
112
+ .menu-item[data-disabled="true"] {
113
+ opacity: 0.5;
114
+ cursor: not-allowed;
115
+ }
116
+
117
+ .menu-separator {
118
+ height: var(--rel-borders-width-stroke-100);
119
+ background: var(--rel-color-border-divider);
120
+ margin: 0;
121
+ padding: 0;
122
+ }
123
+
124
+ rel-icon {
125
+ display: inline-flex;
126
+ pointer-events: none;
127
+ }
128
+
129
+ .menu-submenu-trigger {
130
+ position: relative;
131
+ }
132
+
133
+ .menu-submenu-trigger rel-icon {
134
+ opacity: 0.6;
135
+ }
136
+
137
+ .submenu-panel {
138
+ position: fixed;
139
+ min-width: 200px;
140
+ background: var(--rel-color-bg-primary);
141
+ border: var(--rel-borders-width-stroke-100) solid var(--rel-color-border-default);
142
+ border-radius: var(--rel-borders-radius-border-radius-200);
143
+ box-shadow: var(--rel-shadows-shadow-300);
144
+ padding: 0;
145
+ overflow: hidden;
146
+ z-index: 1001;
147
+ display: none;
148
+ }
149
+
150
+ .submenu-panel.open {
151
+ display: block;
152
+ }
153
+
154
+ .menu-shortcut {
155
+ opacity: 0.6;
156
+ font-size: var(--rel-fontSizes-xs);
157
+ margin-left: auto;
158
+ display: flex;
159
+ align-items: center;
160
+ gap: var(--rel-spacing-space-3xs);
161
+ }
162
+
163
+ .menubar-panel rel-checkbox {
164
+ display: block;
165
+ width: 100%;
166
+ padding: var(--rel-spacing-space-xs, 8px) var(--rel-spacing-space-md, 16px);
167
+ box-sizing: border-box;
168
+ cursor: pointer;
169
+ }
170
+ `;
171
+
172
+ createRenderRoot() {
173
+ return this;
174
+ }
175
+
176
+ connectedCallback() {
177
+ super.connectedCallback();
178
+ this.discoverMenus();
179
+ document.addEventListener("click", this.handleOutsideClick);
180
+ this._onScroll = () => {
181
+ this._repositionOpenMenu();
182
+ this._repositionOpenSubmenu();
183
+ };
184
+ window.addEventListener('scroll', this._onScroll, { passive: true, capture: true });
185
+ }
186
+
187
+ disconnectedCallback() {
188
+ super.disconnectedCallback();
189
+ document.removeEventListener("click", this.handleOutsideClick);
190
+ if (this._onScroll) {
191
+ window.removeEventListener('scroll', this._onScroll, { capture: true });
192
+ }
193
+ }
194
+
195
+ private _repositionOpenSubmenu() {
196
+ if (!this.openSubmenuElement) return;
197
+ const submenuPanel = this.querySelector('.submenu-panel.open') as HTMLElement;
198
+ if (!submenuPanel) return;
199
+ const trigger = this.querySelector('.menu-submenu-trigger.focused, .menu-submenu-trigger:hover') as HTMLElement
200
+ ?? submenuPanel.previousElementSibling as HTMLElement;
201
+ if (!trigger) return;
202
+ const rect = trigger.getBoundingClientRect();
203
+ submenuPanel.style.top = `${rect.top}px`;
204
+ submenuPanel.style.left = `${rect.right + 4}px`;
205
+ }
206
+
207
+ private _repositionOpenMenu() {
208
+ if (this.openMenuIndex === -1) return;
209
+ const triggers = this.querySelectorAll('.menubar-trigger');
210
+ const trigger = triggers[this.openMenuIndex] as HTMLElement;
211
+ const panel = this.querySelector(`.menubar-panel[data-menu-index="${this.openMenuIndex}"]`) as HTMLElement;
212
+ if (!trigger || !panel) return;
213
+ const rect = trigger.getBoundingClientRect();
214
+ panel.style.top = `${rect.bottom + 4}px`;
215
+ panel.style.left = `${rect.left}px`;
216
+ }
217
+
218
+ private discoverMenus() {
219
+ const menuElements = Array.from(
220
+ this.querySelectorAll("rel-menubar-menu")
221
+ ) as RelMenubarMenu[];
222
+ this.menus = menuElements.map((menu) => ({ menu, open: false }));
223
+ }
224
+
225
+ private handleOutsideClick = (e: MouseEvent) => {
226
+ if (!this.contains(e.target as Node)) {
227
+ this.closeAllMenus();
228
+ }
229
+ };
230
+
231
+ private closeAllMenus() {
232
+ this.openMenuIndex = -1;
233
+ this.openSubmenuElement = null;
234
+ this.hoverSwitchEnabled = false;
235
+ this.requestUpdate();
236
+ }
237
+
238
+ private openMenu(index: number, triggerElement: HTMLElement) {
239
+ this.openMenuIndex = index;
240
+ this.focusedItemIndex = -1;
241
+ this.openSubmenuElement = null;
242
+ this.requestUpdate();
243
+
244
+ // Position the panel below the trigger
245
+ this.updateComplete.then(() => {
246
+ const panel = this.querySelector(
247
+ `.menubar-panel[data-menu-index="${index}"]`
248
+ ) as HTMLElement;
249
+ if (panel && triggerElement) {
250
+ const rect = triggerElement.getBoundingClientRect();
251
+ panel.style.top = `${rect.bottom + 4}px`;
252
+ panel.style.left = `${rect.left}px`;
253
+ }
254
+ });
255
+ }
256
+
257
+ private handleTriggerClick(index: number, event: Event) {
258
+ const target = event.currentTarget as HTMLElement;
259
+ if (this.openMenuIndex === index) {
260
+ this.closeAllMenus();
261
+ } else {
262
+ this.openMenu(index, target);
263
+ this.hoverSwitchEnabled = true;
264
+ }
265
+ }
266
+
267
+ private handleTriggerMouseEnter(index: number, event: Event) {
268
+ if (this.openMenuIndex !== -1 && this.hoverSwitchEnabled) {
269
+ const target = event.currentTarget as HTMLElement;
270
+ this.openMenu(index, target);
271
+ }
272
+ }
273
+
274
+ private handleTriggerKeyDown(event: KeyboardEvent) {
275
+ const { key } = event;
276
+
277
+ if (key === "ArrowRight") {
278
+ event.preventDefault();
279
+ this.focusedTriggerIndex = Math.min(
280
+ this.menus.length - 1,
281
+ this.focusedTriggerIndex + 1
282
+ );
283
+ this.focusTrigger(this.focusedTriggerIndex);
284
+ } else if (key === "ArrowLeft") {
285
+ event.preventDefault();
286
+ this.focusedTriggerIndex = Math.max(0, this.focusedTriggerIndex - 1);
287
+ this.focusTrigger(this.focusedTriggerIndex);
288
+ } else if (key === "Enter" || key === " ") {
289
+ event.preventDefault();
290
+ const trigger = this.querySelector(
291
+ `.menubar-trigger[data-index="${this.focusedTriggerIndex}"]`
292
+ ) as HTMLElement;
293
+ if (trigger) {
294
+ this.openMenu(this.focusedTriggerIndex, trigger);
295
+ this.updateComplete.then(() => {
296
+ const focusTarget = this.querySelector<HTMLElement>(
297
+ "[autofocus], [role='menuitem']"
298
+ );
299
+ if (focusTarget) {
300
+ focusTarget.focus();
301
+ } else {
302
+ this.focusMenuItem(0);
303
+ }
304
+ });
305
+ }
306
+ } else if (key === "ArrowDown" && this.openMenuIndex !== -1) {
307
+ event.preventDefault();
308
+ this.focusMenuItem(0);
309
+ }
310
+ }
311
+
312
+ private focusTrigger(index: number) {
313
+ const trigger = this.querySelector(
314
+ `.menubar-trigger[data-index="${index}"]`
315
+ ) as HTMLElement;
316
+ trigger?.focus();
317
+ }
318
+
319
+ private handlePanelKeyDown(event: KeyboardEvent) {
320
+ const { key } = event;
321
+ const menuContent = this.menus[this.openMenuIndex]?.menu;
322
+ if (!menuContent) return;
323
+
324
+ const items = this.getFocusableItems(menuContent);
325
+
326
+ if (key === "ArrowDown") {
327
+ event.preventDefault();
328
+ this.focusedItemIndex = Math.min(items.length - 1, this.focusedItemIndex + 1);
329
+ this.requestUpdate();
330
+ } else if (key === "ArrowUp") {
331
+ event.preventDefault();
332
+ this.focusedItemIndex = Math.max(0, this.focusedItemIndex - 1);
333
+ this.requestUpdate();
334
+ } else if (key === "ArrowRight") {
335
+ event.preventDefault();
336
+ const currentItem = items[this.focusedItemIndex];
337
+ if (currentItem?.hasAttribute("data-submenu")) {
338
+ this.openSubmenu(currentItem as HTMLElement);
339
+ }
340
+ } else if (key === "ArrowLeft") {
341
+ event.preventDefault();
342
+ if (this.openSubmenuElement) {
343
+ this.openSubmenuElement = null;
344
+ this.requestUpdate();
345
+ }
346
+ } else if (key === "Escape") {
347
+ event.preventDefault();
348
+ this.closeAllMenus();
349
+ this.focusTrigger(this.openMenuIndex);
350
+ } else if (key === "Enter" || key === " ") {
351
+ event.preventDefault();
352
+ const currentItem = items[this.focusedItemIndex];
353
+ if (currentItem) {
354
+ this.handleItemClick(currentItem as HTMLElement);
355
+ }
356
+ }
357
+ }
358
+
359
+ private getFocusableItems(menuContent: RelMenubarMenu): Element[] {
360
+ return Array.from(menuContent.children).filter(
361
+ (el) =>
362
+ el.hasAttribute("data-item") ||
363
+ el.hasAttribute("data-checkbox") ||
364
+ el.hasAttribute("data-radio") ||
365
+ el.hasAttribute("data-submenu")
366
+ );
367
+ }
368
+
369
+ private focusMenuItem(index: number) {
370
+ this.focusedItemIndex = index;
371
+ const panel = this.querySelector(
372
+ `.menubar-panel[data-menu-index="${this.openMenuIndex}"]`
373
+ ) as HTMLElement;
374
+ panel?.focus();
375
+ }
376
+
377
+ private handleItemClick(item: HTMLElement) {
378
+ if (item.hasAttribute("data-checkbox")) {
379
+ const checkbox = item.querySelector('input[type="checkbox"]') as HTMLInputElement;
380
+ const labelText = Array.from(item.childNodes)
381
+ .filter(n => n.nodeType === Node.TEXT_NODE)
382
+ .map(n => n.textContent?.trim())
383
+ .filter(Boolean)
384
+ .join(' ') || item.getAttribute('label') || '';
385
+ if (checkbox) {
386
+ checkbox.checked = !checkbox.checked;
387
+ this.dispatchChangeEvent("checkbox", labelText, checkbox.checked);
388
+ this.requestUpdate();
389
+ }
390
+ } else if (item.hasAttribute("data-radio")) {
391
+ const radioGroup = item.closest("[data-radio-group]") as HTMLElement;
392
+ const value = item.getAttribute("data-radio") || item.getAttribute("value") || "";
393
+ const name = radioGroup?.getAttribute("name") || "";
394
+
395
+ // Update visual state
396
+ const allRadios = radioGroup?.querySelectorAll("[data-radio]");
397
+ allRadios?.forEach((radio) => radio.removeAttribute("data-checked"));
398
+ item.setAttribute("data-checked", "true");
399
+
400
+ // Update internal value
401
+ if (radioGroup) {
402
+ radioGroup.setAttribute("value", value);
403
+ }
404
+
405
+ this.dispatchChangeEvent("radio", name, value);
406
+ this.requestUpdate();
407
+ } else if (item.hasAttribute("data-submenu")) {
408
+ this.openSubmenu(item);
409
+ } else {
410
+ // Regular item click
411
+ this.closeAllMenus();
412
+ }
413
+ }
414
+
415
+ private openSubmenu(submenuTrigger: HTMLElement, renderedTrigger?: HTMLElement, submenuIndex?: number) {
416
+ this.openSubmenuElement = submenuTrigger;
417
+ this.requestUpdate();
418
+
419
+ this.updateComplete.then(() => {
420
+ const submenuId = `submenu-${submenuIndex ?? 0}`;
421
+ const submenuPanel = this.querySelector(
422
+ `.submenu-panel[data-submenu-id="${submenuId}"]`
423
+ ) as HTMLElement;
424
+ if (submenuPanel) {
425
+ const positionTarget = renderedTrigger ?? submenuTrigger;
426
+ const rect = positionTarget.getBoundingClientRect();
427
+ submenuPanel.style.top = `${rect.top}px`;
428
+ submenuPanel.style.left = `${rect.right + 4}px`;
429
+ }
430
+ });
431
+ }
432
+
433
+ private handleSubmenuItemClick(_item: HTMLElement) {
434
+ // Regular item - just close
435
+ this.closeAllMenus();
436
+ }
437
+
438
+ private dispatchChangeEvent(type: "checkbox" | "radio", labelOrName: string, value: string | boolean) {
439
+ const detail =
440
+ type === "checkbox"
441
+ ? { type, label: labelOrName, name: "", checked: value as boolean }
442
+ : { type, label: "", name: labelOrName, value: value as string };
443
+ this.dispatchEvent(
444
+ new CustomEvent("rel-menubar-change", {
445
+ detail,
446
+ bubbles: true,
447
+ composed: true,
448
+ })
449
+ );
450
+ }
451
+
452
+ private renderMenuItem(item: Element, index: number): any {
453
+ const isFocused = index === this.focusedItemIndex;
454
+
455
+ if (item.hasAttribute("data-item")) {
456
+ const button = item as HTMLButtonElement;
457
+ const disabled = button.disabled;
458
+ const shortcut = button.getAttribute("data-shortcut") || "";
459
+
460
+ return html`
461
+ <div
462
+ class="menu-item ${isFocused ? "focused" : ""}"
463
+ data-disabled="${disabled}"
464
+ @click="${() => !disabled && this.handleItemClick(item as HTMLElement)}"
465
+ >
466
+ <span>${button.textContent}</span>
467
+ ${shortcut ? html`<span class="menu-shortcut">${shortcut}</span>` : ""}
468
+ </div>
469
+ `;
470
+ }
471
+
472
+ if (item.hasAttribute("data-separator")) {
473
+ return html`<div class="menu-separator"></div>`;
474
+ }
475
+
476
+ if (item.hasAttribute("data-checkbox")) {
477
+ const label = item as HTMLLabelElement;
478
+ const checkbox = label.querySelector('input[type="checkbox"]') as HTMLInputElement;
479
+ const labelText = Array.from(label.childNodes)
480
+ .filter(n => n.nodeType === Node.TEXT_NODE)
481
+ .map(n => n.textContent?.trim())
482
+ .filter(Boolean)
483
+ .join(' ') || label.getAttribute('label') || '';
484
+
485
+ return html`
486
+ <rel-checkbox
487
+ label=${labelText}
488
+ .status=${checkbox?.checked ? "checked" : "unchecked"}
489
+ @rel-change=${(e: CustomEvent) => {
490
+ if (checkbox) checkbox.checked = e.detail.checked;
491
+ this.dispatchChangeEvent("checkbox", labelText, e.detail.checked);
492
+ }}
493
+ ></rel-checkbox>
494
+ `;
495
+ }
496
+
497
+ if (item.hasAttribute("data-submenu")) {
498
+ const label = item.getAttribute("label") || item.textContent?.trim() || "";
499
+ const submenuId = `submenu-${index}`;
500
+
501
+ return html`
502
+ <div
503
+ class="menu-item menu-submenu-trigger ${isFocused ? "focused" : ""}"
504
+ @click="${(e: Event) => this.openSubmenu(item as HTMLElement, e.currentTarget as HTMLElement, index)}"
505
+ @mouseenter="${(e: Event) => this.openSubmenu(item as HTMLElement, e.currentTarget as HTMLElement, index)}"
506
+ >
507
+ <span>${label}</span>
508
+ <rel-icon name="chevron-right"></rel-icon>
509
+ </div>
510
+ ${this.renderSubmenu(item as HTMLElement, submenuId)}
511
+ `;
512
+ }
513
+
514
+ if (item.hasAttribute("data-radio-group")) {
515
+ const radioButtons = Array.from(item.querySelectorAll("[data-radio]"));
516
+ const currentValue = item.getAttribute("value") || "";
517
+ const groupName = item.getAttribute("name") || "";
518
+
519
+ const options = radioButtons.map(radio => ({
520
+ label: radio.textContent?.trim() || "",
521
+ value: radio.getAttribute("data-radio") || radio.getAttribute("value") || "",
522
+ }));
523
+
524
+ return html`
525
+ <rel-radio
526
+ .options=${options}
527
+ .value=${currentValue}
528
+ @rel-change=${(e: CustomEvent) => {
529
+ const newValue = e.detail.value;
530
+ item.setAttribute("value", newValue);
531
+ this.dispatchChangeEvent("radio", groupName, newValue);
532
+ }}
533
+ ></rel-radio>
534
+ `;
535
+ }
536
+
537
+ return null;
538
+ }
539
+
540
+ private renderSubmenu(submenuElement: HTMLElement, submenuId: string) {
541
+ const isOpen = this.openSubmenuElement === submenuElement;
542
+ const submenuItems = Array.from(submenuElement.children);
543
+
544
+ return html`
545
+ <div class="submenu-panel ${isOpen ? "open" : ""}" data-submenu-id="${submenuId}">
546
+ ${submenuItems.map((subItem) => {
547
+ if (subItem.hasAttribute("data-item")) {
548
+ const button = subItem as HTMLButtonElement;
549
+ return html`
550
+ <div
551
+ class="menu-item"
552
+ @click="${() => this.handleSubmenuItemClick(subItem as HTMLElement)}"
553
+ >
554
+ <span>${button.textContent}</span>
555
+ </div>
556
+ `;
557
+ }
558
+ return null;
559
+ })}
560
+ </div>
561
+ `;
562
+ }
563
+
564
+ private renderMenuPanel(menuState: MenuState, index: number) {
565
+ const isOpen = index === this.openMenuIndex;
566
+ const items = Array.from(menuState.menu.children);
567
+
568
+ return html`
569
+ <div
570
+ class="menubar-panel ${isOpen ? "open" : ""}"
571
+ data-menu-index="${index}"
572
+ tabindex="-1"
573
+ @keydown="${this.handlePanelKeyDown}"
574
+ >
575
+ ${items.map((item, idx) => this.renderMenuItem(item, idx))}
576
+ </div>
577
+ `;
578
+ }
579
+
580
+ render() {
581
+ return html`
582
+ <style>${RelMenubar._css}</style>
583
+ <slot></slot>
584
+ <div class="menubar-container">
585
+ ${this.menus.map(
586
+ (menuState, index) => html`
587
+ <button
588
+ class="menubar-trigger ${index === this.openMenuIndex ? "active" : ""}"
589
+ data-index="${index}"
590
+ @click="${(e: Event) => this.handleTriggerClick(index, e)}"
591
+ @mouseenter="${(e: Event) => this.handleTriggerMouseEnter(index, e)}"
592
+ @keydown="${this.handleTriggerKeyDown}"
593
+ >
594
+ ${menuState.menu.label}
595
+ </button>
596
+ `
597
+ )}
598
+ </div>
599
+ ${this.menus.map((menuState, index) => this.renderMenuPanel(menuState, index))}
600
+ `;
601
+ }
602
+ }