@reliability-design/web 1.0.3 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/components/rel-accordion/README.md +132 -0
  2. package/dist/components/rel-accordion/rel-accordion.ts +47 -16
  3. package/dist/components/rel-alert/README.md +141 -0
  4. package/dist/components/rel-alert/rel-alert.ts +101 -20
  5. package/dist/components/rel-avatar/README.md +119 -0
  6. package/dist/components/rel-avatar/rel-avatar.ts +3 -2
  7. package/dist/components/rel-badge/README.md +86 -0
  8. package/dist/components/rel-badge/rel-badge.ts +11 -7
  9. package/dist/components/rel-breadcrumb/README.md +102 -0
  10. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
  11. package/dist/components/rel-button/README.md +130 -0
  12. package/dist/components/rel-button/rel-button.ts +64 -8
  13. package/dist/components/rel-calendar/README.md +109 -0
  14. package/dist/components/rel-calendar/index.ts +1 -0
  15. package/dist/components/rel-calendar/rel-calendar.ts +467 -0
  16. package/dist/components/rel-card/README.md +94 -0
  17. package/dist/components/rel-card/rel-card.ts +12 -3
  18. package/dist/components/rel-cardmedia/README.md +127 -0
  19. package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
  20. package/dist/components/rel-carousel/README.md +166 -0
  21. package/dist/components/rel-carousel/index.ts +1 -0
  22. package/dist/components/rel-carousel/rel-carousel.ts +468 -0
  23. package/dist/components/rel-chart/README.md +286 -0
  24. package/dist/components/rel-chart/index.ts +1 -0
  25. package/dist/components/rel-chart/rel-chart.ts +736 -0
  26. package/dist/components/rel-checkbox/README.md +77 -0
  27. package/dist/components/rel-checkbox/rel-checkbox.ts +51 -12
  28. package/dist/components/rel-chip/README.md +114 -0
  29. package/dist/components/rel-chip/rel-chip.ts +83 -9
  30. package/dist/components/rel-container/README.md +99 -0
  31. package/dist/components/rel-date-picker/README.md +253 -0
  32. package/dist/components/rel-date-picker/index.ts +1 -0
  33. package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
  34. package/dist/components/rel-divider/README.md +100 -0
  35. package/dist/components/rel-divider/rel-divider.ts +9 -0
  36. package/dist/components/rel-drawer/README.md +200 -0
  37. package/dist/components/rel-drawer/index.ts +1 -0
  38. package/dist/components/rel-drawer/rel-drawer-footer.ts +29 -0
  39. package/dist/components/rel-drawer/rel-drawer.ts +612 -0
  40. package/dist/components/rel-kbd/README.md +151 -0
  41. package/dist/components/rel-kbd/rel-kbd.ts +118 -0
  42. package/dist/components/rel-layout/README.md +88 -0
  43. package/dist/components/rel-menu/README.md +151 -0
  44. package/dist/components/rel-menu/rel-menu.ts +64 -24
  45. package/dist/components/rel-menubar/README.md +329 -0
  46. package/dist/components/rel-menubar/index.ts +2 -0
  47. package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
  48. package/dist/components/rel-menubar/rel-menubar.ts +605 -0
  49. package/dist/components/rel-nav-menu/README.md +289 -0
  50. package/dist/components/rel-nav-menu/index.ts +3 -0
  51. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +240 -0
  52. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
  53. package/dist/components/rel-nav-menu/rel-nav-menu.ts +227 -0
  54. package/dist/components/rel-navbar/README.md +126 -0
  55. package/dist/components/rel-navbar/rel-navbar.ts +104 -0
  56. package/dist/components/rel-otp/README.md +440 -0
  57. package/dist/components/rel-otp/index.ts +1 -0
  58. package/dist/components/rel-otp/rel-otp.ts +504 -0
  59. package/dist/components/rel-pagination/README.md +410 -0
  60. package/dist/components/rel-pagination/index.ts +1 -0
  61. package/dist/components/rel-pagination/rel-pagination.ts +381 -0
  62. package/dist/components/rel-popover/README.md +568 -0
  63. package/dist/components/rel-popover/index.ts +2 -0
  64. package/dist/components/rel-popover/rel-popover-content.ts +55 -0
  65. package/dist/components/rel-popover/rel-popover.ts +330 -0
  66. package/dist/components/rel-progress/README.md +110 -0
  67. package/dist/components/rel-progress/rel-progress.ts +32 -5
  68. package/dist/components/rel-radio/README.md +120 -0
  69. package/dist/components/rel-radio/rel-radio.ts +31 -6
  70. package/dist/components/rel-resizable/README.md +140 -0
  71. package/dist/components/rel-resizable/index.ts +3 -0
  72. package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
  73. package/dist/components/rel-resizable/rel-resizable-handle.ts +145 -0
  74. package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
  75. package/dist/components/rel-segmented-button/README.md +141 -0
  76. package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
  77. package/dist/components/rel-select/README.md +107 -0
  78. package/dist/components/rel-select/rel-select.ts +28 -14
  79. package/dist/components/rel-sheet/README.md +224 -0
  80. package/dist/components/rel-sheet/rel-sheet.ts +684 -0
  81. package/dist/components/rel-sidebar/README.md +209 -0
  82. package/dist/components/rel-sidebar/rel-sidebar.ts +1166 -0
  83. package/dist/components/rel-slider/README.md +135 -0
  84. package/dist/components/rel-slider/rel-slider.ts +44 -9
  85. package/dist/components/rel-snackbar/README.md +134 -0
  86. package/dist/components/rel-snackbar/rel-snackbar.ts +109 -14
  87. package/dist/components/rel-stepper/README.md +160 -0
  88. package/dist/components/rel-stepper/rel-stepper.ts +38 -27
  89. package/dist/components/rel-switch/README.md +102 -0
  90. package/dist/components/rel-switch/rel-switch.ts +34 -12
  91. package/dist/components/rel-tab/README.md +163 -0
  92. package/dist/components/rel-tab/rel-tab.ts +79 -23
  93. package/dist/components/rel-table/README.md +178 -0
  94. package/dist/components/rel-table/rel-table-body.ts +21 -0
  95. package/dist/components/rel-table/rel-table-caption.ts +27 -0
  96. package/dist/components/rel-table/rel-table-cell.ts +42 -0
  97. package/dist/components/rel-table/rel-table-footer.ts +23 -0
  98. package/dist/components/rel-table/rel-table-head.ts +45 -0
  99. package/dist/components/rel-table/rel-table-header.ts +21 -0
  100. package/dist/components/rel-table/rel-table-row.ts +36 -0
  101. package/dist/components/rel-table/rel-table.ts +53 -0
  102. package/dist/components/rel-textfield/README.md +90 -0
  103. package/dist/components/rel-textfield/rel-textfield.ts +55 -39
  104. package/dist/components/rel-toast/README.md +148 -0
  105. package/dist/components/rel-toast/rel-toast.ts +63 -24
  106. package/dist/components/rel-tooltip/README.md +103 -0
  107. package/dist/components/rel-tooltip/rel-tooltip.ts +123 -80
  108. package/dist/components/rel-topbar/rel-topbar-actions.ts +23 -0
  109. package/dist/components/rel-topbar/rel-topbar-brand.ts +46 -0
  110. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +33 -0
  111. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +28 -0
  112. package/dist/components/rel-topbar/rel-topbar-item.ts +315 -0
  113. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +23 -0
  114. package/dist/components/rel-topbar/rel-topbar-profile.ts +248 -0
  115. package/dist/components/rel-topbar/rel-topbar-utilities.ts +23 -0
  116. package/dist/components/rel-topbar/rel-topbar.ts +329 -0
  117. package/dist/index.d.mts +831 -7
  118. package/dist/index.d.ts +831 -7
  119. package/dist/index.js +9500 -1564
  120. package/dist/index.js.map +1 -1
  121. package/dist/index.mjs +9240 -1344
  122. package/dist/index.mjs.map +1 -1
  123. package/dist/styles/vars.css +187 -25
  124. package/dist/vars-TBB4BMSD.css +346 -0
  125. package/package.json +2 -1
  126. package/dist/vars-6ZIUPIU4.css +0 -184
@@ -0,0 +1,605 @@
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
+ }