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