@reliability-design/web 1.0.2 → 1.0.6

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 (145) hide show
  1. package/dist/components/rel-accordion/README.md +132 -0
  2. package/dist/components/rel-accordion/rel-accordion.ts +61 -29
  3. package/dist/components/rel-alert/README.md +141 -0
  4. package/dist/components/rel-alert/rel-alert.ts +110 -67
  5. package/dist/components/rel-avatar/README.md +119 -0
  6. package/dist/components/rel-avatar/rel-avatar.ts +8 -15
  7. package/dist/components/rel-badge/README.md +86 -0
  8. package/dist/components/rel-badge/rel-badge.ts +28 -21
  9. package/dist/components/rel-breadcrumb/README.md +102 -0
  10. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +24 -18
  11. package/dist/components/rel-button/README.md +130 -0
  12. package/dist/components/rel-button/rel-button.ts +39 -59
  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 +21 -11
  18. package/dist/components/rel-cardmedia/README.md +127 -0
  19. package/dist/components/rel-cardmedia/rel-cardmedia.ts +22 -18
  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 +469 -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 +36 -45
  28. package/dist/components/rel-chip/README.md +114 -0
  29. package/dist/components/rel-chip/rel-chip.ts +47 -122
  30. package/dist/components/rel-container/README.md +99 -0
  31. package/dist/components/rel-container/rel-container.ts +1 -6
  32. package/dist/components/rel-date-picker/README.md +253 -0
  33. package/dist/components/rel-date-picker/index.ts +1 -0
  34. package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
  35. package/dist/components/rel-divider/README.md +100 -0
  36. package/dist/components/rel-divider/rel-divider.ts +37 -37
  37. package/dist/components/rel-drawer/README.md +200 -0
  38. package/dist/components/rel-drawer/index.ts +1 -0
  39. package/dist/components/rel-drawer/rel-drawer.ts +621 -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-layout/rel-layout.ts +12 -14
  44. package/dist/components/rel-menu/README.md +151 -0
  45. package/dist/components/rel-menu/rel-menu.ts +18 -10
  46. package/dist/components/rel-menubar/README.md +329 -0
  47. package/dist/components/rel-menubar/index.ts +2 -0
  48. package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
  49. package/dist/components/rel-menubar/rel-menubar.ts +576 -0
  50. package/dist/components/rel-nav-menu/README.md +289 -0
  51. package/dist/components/rel-nav-menu/index.ts +3 -0
  52. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +143 -0
  53. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
  54. package/dist/components/rel-nav-menu/rel-nav-menu.ts +331 -0
  55. package/dist/components/rel-navbar/README.md +126 -0
  56. package/dist/components/rel-navbar/rel-navbar.ts +91 -0
  57. package/dist/components/rel-otp/README.md +440 -0
  58. package/dist/components/rel-otp/index.ts +1 -0
  59. package/dist/components/rel-otp/rel-otp.ts +504 -0
  60. package/dist/components/rel-pagination/README.md +410 -0
  61. package/dist/components/rel-pagination/index.ts +1 -0
  62. package/dist/components/rel-pagination/rel-pagination.ts +376 -0
  63. package/dist/components/rel-popover/README.md +568 -0
  64. package/dist/components/rel-popover/index.ts +2 -0
  65. package/dist/components/rel-popover/rel-popover-content.ts +55 -0
  66. package/dist/components/rel-popover/rel-popover.ts +327 -0
  67. package/dist/components/rel-progress/README.md +110 -0
  68. package/dist/components/rel-progress/rel-progress.ts +58 -20
  69. package/dist/components/rel-quote-page/README.md +166 -0
  70. package/dist/components/rel-quote-page/rel-quote-page.ts +816 -0
  71. package/dist/components/rel-radio/README.md +120 -0
  72. package/dist/components/rel-radio/rel-radio.ts +41 -73
  73. package/dist/components/rel-resizable/README.md +140 -0
  74. package/dist/components/rel-resizable/index.ts +3 -0
  75. package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
  76. package/dist/components/rel-resizable/rel-resizable-handle.ts +149 -0
  77. package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
  78. package/dist/components/rel-segmented-button/README.md +141 -0
  79. package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
  80. package/dist/components/rel-select/README.md +107 -0
  81. package/dist/components/rel-select/rel-select.ts +14 -14
  82. package/dist/components/rel-sheet/README.md +224 -0
  83. package/dist/components/rel-sheet/rel-sheet.ts +683 -0
  84. package/dist/components/rel-sidebar/README.md +209 -0
  85. package/dist/components/rel-sidebar/rel-sidebar.ts +1158 -0
  86. package/dist/components/rel-slider/README.md +135 -0
  87. package/dist/components/rel-slider/rel-slider.ts +19 -7
  88. package/dist/components/rel-snackbar/README.md +134 -0
  89. package/dist/components/rel-snackbar/rel-snackbar.ts +73 -2
  90. package/dist/components/rel-stepper/README.md +160 -0
  91. package/dist/components/rel-stepper/rel-stepper.ts +29 -8
  92. package/dist/components/rel-switch/README.md +102 -0
  93. package/dist/components/rel-switch/rel-switch.ts +22 -11
  94. package/dist/components/rel-tab/README.md +163 -0
  95. package/dist/components/rel-tab/rel-tab.ts +46 -22
  96. package/dist/components/rel-table/README.md +178 -0
  97. package/dist/components/rel-table/rel-table-body.ts +16 -0
  98. package/dist/components/rel-table/rel-table-caption.ts +22 -0
  99. package/dist/components/rel-table/rel-table-cell.ts +49 -0
  100. package/dist/components/rel-table/rel-table-footer.ts +18 -0
  101. package/dist/components/rel-table/rel-table-head.ts +33 -0
  102. package/dist/components/rel-table/rel-table-header.ts +16 -0
  103. package/dist/components/rel-table/rel-table-row.ts +32 -0
  104. package/dist/components/rel-table/rel-table.ts +47 -0
  105. package/dist/components/rel-textfield/README.md +90 -0
  106. package/dist/components/rel-textfield/rel-textfield.ts +37 -21
  107. package/dist/components/rel-toast/README.md +148 -0
  108. package/dist/components/rel-toast/rel-toast.ts +32 -14
  109. package/dist/components/rel-tooltip/README.md +103 -0
  110. package/dist/components/rel-tooltip/rel-tooltip.ts +9 -9
  111. package/dist/index.d.mts +846 -7
  112. package/dist/index.d.ts +846 -7
  113. package/dist/index.js +10860 -1803
  114. package/dist/index.js.map +1 -1
  115. package/dist/index.mjs +10820 -1809
  116. package/dist/index.mjs.map +1 -1
  117. package/dist/styles/vars.css +293 -183
  118. package/dist/vars-ITL2K3KO.css +294 -0
  119. package/package.json +2 -2
  120. package/dist/components/rel-accordion/rel-accordion.stories.ts +0 -313
  121. package/dist/components/rel-alert/rel-alert.stories.ts +0 -412
  122. package/dist/components/rel-avatar/rel-avatar.stories.ts +0 -327
  123. package/dist/components/rel-badge/rel-badge.stories.ts +0 -350
  124. package/dist/components/rel-breadcrumb/rel-breadcrumb.stories.ts +0 -306
  125. package/dist/components/rel-button/rel-button.stories.ts +0 -356
  126. package/dist/components/rel-card/rel-card.stories.ts +0 -299
  127. package/dist/components/rel-cardmedia/rel-cardmedia.stories.ts +0 -368
  128. package/dist/components/rel-checkbox/rel-checkbox.stories.ts +0 -414
  129. package/dist/components/rel-chip/rel-chip.stories.ts +0 -552
  130. package/dist/components/rel-container/rel-container.stories.ts +0 -239
  131. package/dist/components/rel-divider/rel-divider.stories.ts +0 -229
  132. package/dist/components/rel-layout/rel-layout.stories.ts +0 -713
  133. package/dist/components/rel-menu/rel-menu.stories.ts +0 -910
  134. package/dist/components/rel-progress/rel-progress.stories.ts +0 -817
  135. package/dist/components/rel-radio/rel-radio.stories.ts +0 -1048
  136. package/dist/components/rel-segmented-button/rel-segmented-button.stories.ts +0 -1079
  137. package/dist/components/rel-select/rel-select.stories.ts +0 -1201
  138. package/dist/components/rel-slider/rel-slider.stories.ts +0 -730
  139. package/dist/components/rel-snackbar/rel-snackbar.stories.ts +0 -808
  140. package/dist/components/rel-stepper/rel-stepper.stories.ts +0 -752
  141. package/dist/components/rel-switch/rel-switch.stories.ts +0 -441
  142. package/dist/components/rel-tab/rel-tab.stories.ts +0 -492
  143. package/dist/components/rel-textfield/rel-textfield.stories.ts +0 -710
  144. package/dist/components/rel-toast/rel-toast.stories.ts +0 -394
  145. package/dist/components/rel-tooltip/rel-tooltip.stories.ts +0 -364
@@ -0,0 +1,576 @@
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
+ }