@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,359 +1,359 @@
1
- import { LitElement, html, css } from "lit";
2
- import { customElement, property, state } from "lit/decorators.js";
3
-
4
- export type MenuType = "default" | "checkbox" | "arrow";
5
-
6
- export interface MenuItem {
7
- label: string;
8
- active?: boolean;
9
- checked?: boolean;
10
- disabled?: boolean;
11
- submenu?: MenuItem[];
12
- open?: boolean;
13
- }
14
-
15
- @customElement("rel-menu-item")
16
- export class RelMenuItem extends LitElement {
17
- @property({ type: String }) label: string = "";
18
- @property({ type: String }) value: string = "";
19
- @property({ type: Boolean }) disabled: boolean = false;
20
- @property({ type: String }) icon: string = "";
21
-
22
- render() {
23
- return html``;
24
- }
25
- }
26
-
27
- @customElement("rel-menu")
28
- export class RelMenu extends LitElement {
29
- @property({ type: String }) type: MenuType = "default";
30
-
31
- @property({ type: Array }) items: MenuItem[] = [];
32
-
33
- @state() private focusedIndex: number = 0;
34
- @state() private _menuItems: RelMenuItem[] = [];
35
-
36
- static styles = css`
37
- :host {
38
- display: inline-block;
39
- }
40
-
41
- .rel-menu {
42
- position: relative;
43
- display: inline-block;
44
- background: var(--rel-color-bg-primary);
45
- color: var(--rel-color-text-primary);
46
- border-radius: var(--rel-borders-radius-border-radius-300);
47
- box-shadow: var(--rel-shadows-shadow-300);
48
- min-width: 220px;
49
- font-family: var(--rel-fontFamilies-primary, sans-serif);
50
- font-size: var(--rel-fontSizes-sm);
51
- padding: var(--rel-spacing-space-3xs) 0;
52
- outline: none;
53
- overflow: hidden;
54
- }
55
-
56
- .rel-menu:focus-visible {
57
- outline: 2px solid var(--rel-colors-blue-primary, #0091ea);
58
- outline-offset: 2px;
59
- }
60
-
61
- .rel-menu-item {
62
- position: relative;
63
- display: flex;
64
- align-items: center;
65
- gap: var(--rel-spacing-space-xs);
66
- padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-sm);
67
- line-height: var(--rel-lineHeights-sm);
68
- cursor: pointer;
69
- transition: background 0.15s ease, color 0.15s ease;
70
- user-select: none;
71
- }
72
-
73
- .rel-menu-item:hover {
74
- background: var(--rel-color-bg-hover);
75
- }
76
-
77
- .rel-menu-item:active {
78
- background: var(--rel-color-bg-hover);
79
- }
80
-
81
- .rel-menu-item.active {
82
- background: var(--rel-color-bg-hover);
83
- font-weight: var(--rel-fontWeights-medium);
84
- }
85
-
86
- .rel-menu-item.disabled {
87
- background-color: transparent;
88
- color: var(--rel-text-disabled, #9e9e9e);
89
- cursor: not-allowed;
90
- }
91
-
92
- .rel-menu-item[data-focused="true"] {
93
- outline: 2px solid var(--rel-colors-blue-primary, #0091ea);
94
- outline-offset: -2px;
95
- }
96
-
97
- .menu-check-row {
98
- display: inline-flex;
99
- align-items: center;
100
- gap: var(--rel-spacing-space-2xs);
101
- width: 100%;
102
- }
103
-
104
- .menu-checkbox {
105
- width: 16px;
106
- height: 16px;
107
- cursor: pointer;
108
- }
109
-
110
- .menu-checkbox:disabled {
111
- cursor: not-allowed;
112
- }
113
-
114
- .menu-label {
115
- flex: 1 1 auto;
116
- }
117
-
118
- .chevron {
119
- font-size: var(--rel-fontSizes-lg);
120
- opacity: 0.75;
121
- transform: translateY(1px);
122
- }
123
-
124
- .rel-submenu {
125
- position: absolute;
126
- top: calc(-1 * var(--rel-spacing-space-3xs));
127
- left: calc(100% + var(--rel-spacing-space-2xs));
128
- min-width: 200px;
129
- background: var(--rel-color-bg-primary);
130
- color: var(--rel-color-text-primary);
131
- border-radius: var(--rel-borders-radius-border-radius-300);
132
- box-shadow: var(--rel-shadows-shadow-300);
133
- padding: var(--rel-spacing-space-3xs) 0;
134
- display: none;
135
- z-index: 20;
136
- }
137
-
138
- .rel-submenu.visible {
139
- display: block;
140
- }
141
-
142
- .rel-submenu .rel-menu-item {
143
- padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-sm);
144
- }
145
- `;
146
-
147
- private _onSlotChange(e: Event) {
148
- const slot = e.target as HTMLSlotElement;
149
- this._menuItems = slot
150
- .assignedElements()
151
- .filter((el): el is RelMenuItem => el instanceof RelMenuItem);
152
- }
153
-
154
- private get _effectiveItems(): MenuItem[] {
155
- if (this._menuItems.length > 0) {
156
- return this._menuItems.map((el) => ({
157
- label: el.label,
158
- disabled: el.disabled,
159
- }));
160
- }
161
- return this.items;
162
- }
163
-
164
- connectedCallback() {
165
- super.connectedCallback();
166
- document.addEventListener("click", this.handleDocumentClick);
167
- }
168
-
169
- disconnectedCallback() {
170
- super.disconnectedCallback();
171
- document.removeEventListener("click", this.handleDocumentClick);
172
- }
173
-
174
- private handleDocumentClick = (e: MouseEvent) => {
175
- const path = e.composedPath() as HTMLElement[];
176
- const hit = path.some((n) => n?.classList?.contains("rel-menu"));
177
- if (!hit) {
178
- this._effectiveItems.forEach((i) => (i.open = false));
179
- this.requestUpdate();
180
- }
181
- };
182
-
183
- private onItemEnter(item: MenuItem) {
184
- if (this.type === "arrow" && item.submenu?.length) {
185
- this._effectiveItems.forEach((i) => {
186
- if (i !== item) i.open = false;
187
- });
188
- item.open = true;
189
- this.requestUpdate();
190
- }
191
- }
192
-
193
- private onItemLeave(_item: MenuItem) {
194
- // Keep open so user can move into submenu
195
- }
196
-
197
- private onItemClick(item: MenuItem, index: number, ev?: Event) {
198
- ev?.stopPropagation();
199
- if (item.disabled) return;
200
-
201
- this.focusedIndex = index;
202
-
203
- if (this.type === "checkbox") {
204
- item.checked = !item.checked;
205
- this.dispatchEvent(
206
- new CustomEvent("rel-check", {
207
- detail: { item, checked: item.checked },
208
- bubbles: true,
209
- composed: true,
210
- })
211
- );
212
- this.requestUpdate();
213
- return;
214
- }
215
-
216
- if (this.type === "arrow" && item.submenu?.length) {
217
- const next = !item.open;
218
- this._effectiveItems.forEach((i) => (i.open = false));
219
- item.open = next;
220
- this.requestUpdate();
221
- return;
222
- }
223
-
224
- // Default: set active
225
- this._effectiveItems.forEach((i) => (i.active = false));
226
- item.active = true;
227
- this.dispatchEvent(
228
- new CustomEvent("rel-select", {
229
- detail: { item },
230
- bubbles: true,
231
- composed: true,
232
- })
233
- );
234
- this.requestUpdate();
235
- }
236
-
237
- private onSubClick(parent: MenuItem, sub: MenuItem, ev?: Event) {
238
- ev?.stopPropagation();
239
- this._effectiveItems.forEach((i) => {
240
- i.active = false;
241
- i.submenu?.forEach((s) => (s.active = false));
242
- });
243
- sub.active = true;
244
- parent.open = false;
245
- this.dispatchEvent(
246
- new CustomEvent("rel-submenu-select", {
247
- detail: { parent, sub },
248
- bubbles: true,
249
- composed: true,
250
- })
251
- );
252
- this.requestUpdate();
253
- }
254
-
255
- private toggleCheckbox(item: MenuItem, ev: Event) {
256
- ev.stopPropagation();
257
- if (!item.disabled) {
258
- item.checked = !item.checked;
259
- this.dispatchEvent(
260
- new CustomEvent("rel-check", {
261
- detail: { item, checked: item.checked },
262
- bubbles: true,
263
- composed: true,
264
- })
265
- );
266
- this.requestUpdate();
267
- }
268
- }
269
-
270
- private onKeydown(ev: KeyboardEvent) {
271
- const last = this._effectiveItems.length - 1;
272
-
273
- if (ev.key === "ArrowDown") {
274
- this.focusedIndex = Math.min(last, this.focusedIndex + 1);
275
- ev.preventDefault();
276
- } else if (ev.key === "ArrowUp") {
277
- this.focusedIndex = Math.max(0, this.focusedIndex - 1);
278
- ev.preventDefault();
279
- } else if (ev.key === "ArrowRight") {
280
- const item = this._effectiveItems[this.focusedIndex];
281
- if (this.type === "arrow" && item?.submenu?.length) {
282
- item.open = true;
283
- this.requestUpdate();
284
- }
285
- } else if (ev.key === "ArrowLeft" || ev.key === "Escape") {
286
- this._effectiveItems.forEach((i) => (i.open = false));
287
- this.requestUpdate();
288
- } else if (ev.key === "Enter" || ev.key === " ") {
289
- const item = this._effectiveItems[this.focusedIndex];
290
- this.onItemClick(item, this.focusedIndex);
291
- ev.preventDefault();
292
- }
293
- }
294
-
295
- private renderMenuItem(item: MenuItem, index: number) {
296
- const isFocused = this.focusedIndex === index;
297
-
298
- return html`
299
- <div
300
- class="rel-menu-item ${item.active ? "active" : ""} ${item.disabled ? "disabled" : ""}"
301
- role="menuitem"
302
- ?data-focused="${isFocused}"
303
- @mouseenter="${() => this.onItemEnter(item)}"
304
- @mouseleave="${() => this.onItemLeave(item)}"
305
- @click="${(e: Event) => this.onItemClick(item, index, e)}">
306
- ${this.type === "checkbox"
307
- ? html`
308
- <div class="menu-check-row">
309
- <input
310
- class="menu-checkbox"
311
- type="checkbox"
312
- .checked="${item.checked || false}"
313
- ?disabled="${item.disabled}"
314
- @click="${(e: Event) => this.toggleCheckbox(item, e)}" />
315
- <span class="menu-label">${item.label}</span>
316
- </div>
317
- `
318
- : html`
319
- <span class="menu-label">${item.label}</span>
320
- ${this.type === "arrow" && item.submenu?.length
321
- ? html`<span class="chevron">▸</span>`
322
- : ""}
323
- `}
324
- ${this.type === "arrow" && item.submenu?.length
325
- ? html`
326
- <div
327
- class="rel-submenu ${item.open ? "visible" : ""}"
328
- @click="${(e: Event) => e.stopPropagation()}">
329
- ${item.submenu.map(
330
- (sub) => html`
331
- <div
332
- class="rel-menu-item ${sub.active ? "active" : ""}"
333
- role="menuitem"
334
- @click="${(e: Event) => this.onSubClick(item, sub, e)}">
335
- <span class="menu-label">${sub.label}</span>
336
- </div>
337
- `
338
- )}
339
- </div>
340
- `
341
- : ""}
342
- </div>
343
- `;
344
- }
345
-
346
- render() {
347
- return html`
348
- <slot name="items" style="display:none;" @slotchange="${this._onSlotChange}"></slot>
349
- <div
350
- class="rel-menu"
351
- role="menu"
352
- tabindex="0"
353
- @keydown="${this.onKeydown}"
354
- @click="${(e: Event) => e.stopPropagation()}">
355
- ${this._effectiveItems.map((item, index) => this.renderMenuItem(item, index))}
356
- </div>
357
- `;
358
- }
359
- }
1
+ import { LitElement, html } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import "../../styles/vars-v2.css";
4
+
5
+ export type MenuType = "default" | "checkbox" | "arrow";
6
+
7
+ export interface MenuItem {
8
+ label: string;
9
+ active?: boolean;
10
+ checked?: boolean;
11
+ disabled?: boolean;
12
+ submenu?: MenuItem[];
13
+ open?: boolean;
14
+ }
15
+
16
+ @customElement("rel-menu-item")
17
+ export class RelMenuItem extends LitElement {
18
+ @property({ type: String }) label: string = "";
19
+ @property({ type: String }) value: string = "";
20
+ @property({ type: Boolean }) disabled: boolean = false;
21
+ @property({ type: String }) icon: string = "";
22
+
23
+ createRenderRoot() {
24
+ return this;
25
+ }
26
+
27
+ render() {
28
+ return html``;
29
+ }
30
+ }
31
+
32
+ @customElement("rel-menu")
33
+ export class RelMenu extends LitElement {
34
+ @property({ type: String }) type: MenuType = "default";
35
+
36
+ @property({ type: Array }) items: MenuItem[] = [];
37
+
38
+ @state() private focusedIndex: number = 0;
39
+ @state() private _items: MenuItem[] = [];
40
+
41
+ public static readonly _css = `
42
+ :host {
43
+ display: inline-block;
44
+ }
45
+
46
+ .rel-menu {
47
+ position: relative;
48
+ display: inline-block;
49
+ background: var(--rel-color-bg-primary);
50
+ color: var(--rel-color-text-primary);
51
+ border-radius: var(--rel-borders-radius-border-radius-300);
52
+ box-shadow: var(--rel-shadows-shadow-300);
53
+ min-width: 220px;
54
+ font-family: var(--rel-fontFamilies-primary, sans-serif);
55
+ font-size: var(--rel-fontSizes-sm);
56
+ padding: var(--rel-spacing-space-3xs) 0;
57
+ outline: none;
58
+ overflow: hidden;
59
+ }
60
+
61
+ .rel-menu:focus-visible,
62
+ :host(:focus-visible) .rel-menu {
63
+ outline: 2px solid var(--rel-color-border-focus);
64
+ outline-offset: 2px;
65
+ }
66
+
67
+ .rel-menu-item {
68
+ position: relative;
69
+ display: flex;
70
+ align-items: center;
71
+ gap: var(--rel-spacing-space-xs);
72
+ padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-sm);
73
+ line-height: var(--rel-lineHeights-sm);
74
+ cursor: pointer;
75
+ transition: background 0.15s ease, color 0.15s ease;
76
+ user-select: none;
77
+ }
78
+
79
+ .rel-menu-item:hover {
80
+ background: var(--rel-color-bg-hover);
81
+ }
82
+
83
+ .rel-menu-item:active {
84
+ background: var(--rel-color-bg-hover);
85
+ }
86
+
87
+ .rel-menu-item.active {
88
+ background: var(--rel-color-bg-hover);
89
+ font-weight: var(--rel-fontWeights-medium);
90
+ }
91
+
92
+ .rel-menu-item.disabled {
93
+ background-color: transparent;
94
+ color: var(--rel-color-text-disabled);
95
+ cursor: not-allowed;
96
+ }
97
+
98
+ .rel-menu-item[data-focused="true"] {
99
+ outline: 2px solid var(--rel-color-border-focus);
100
+ outline-offset: -2px;
101
+ }
102
+
103
+ .menu-check-row {
104
+ display: inline-flex;
105
+ align-items: center;
106
+ gap: var(--rel-spacing-space-2xs);
107
+ width: 100%;
108
+ }
109
+
110
+ .menu-checkbox {
111
+ width: 16px;
112
+ height: 16px;
113
+ cursor: pointer;
114
+ }
115
+
116
+ .menu-checkbox:disabled {
117
+ cursor: not-allowed;
118
+ }
119
+
120
+ .menu-label {
121
+ flex: 1 1 auto;
122
+ }
123
+
124
+ .chevron {
125
+ font-size: var(--rel-fontSizes-lg);
126
+ opacity: 0.75;
127
+ transform: translateY(1px);
128
+ }
129
+
130
+ .rel-submenu {
131
+ position: absolute;
132
+ top: calc(-1 * var(--rel-spacing-space-3xs));
133
+ left: calc(100% + var(--rel-spacing-space-2xs));
134
+ min-width: 200px;
135
+ background: var(--rel-color-bg-primary);
136
+ color: var(--rel-color-text-primary);
137
+ border-radius: var(--rel-borders-radius-border-radius-300);
138
+ box-shadow: var(--rel-shadows-shadow-300);
139
+ padding: var(--rel-spacing-space-3xs) 0;
140
+ visibility: hidden;
141
+ pointer-events: none;
142
+ z-index: 20;
143
+ }
144
+
145
+ .rel-submenu.visible {
146
+ visibility: visible;
147
+ pointer-events: auto;
148
+ }
149
+
150
+ .rel-submenu .rel-menu-item {
151
+ padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-sm);
152
+ }
153
+ `;
154
+
155
+ createRenderRoot() {
156
+ return this;
157
+ }
158
+
159
+ connectedCallback() {
160
+ super.connectedCallback();
161
+ document.addEventListener("click", this.handleDocumentClick);
162
+ }
163
+
164
+ disconnectedCallback() {
165
+ super.disconnectedCallback();
166
+ document.removeEventListener("click", this.handleDocumentClick);
167
+ }
168
+
169
+ updated(changedProperties: Map<string, unknown>) {
170
+ if (changedProperties.has('items')) {
171
+ this._items = this.items.map(item => ({ ...item }));
172
+ }
173
+ }
174
+
175
+ private handleDocumentClick = (e: MouseEvent) => {
176
+ if (!this.contains(e.target as Node)) {
177
+ this._items = this._items.map(i => ({ ...i, open: false }));
178
+ }
179
+ };
180
+
181
+ private onItemEnter(item: MenuItem, index: number) {
182
+ if (this.type === "arrow" && item.submenu?.length) {
183
+ this._items = this._items.map((i, idx) => ({ ...i, open: idx === index }));
184
+ }
185
+ }
186
+
187
+ private onItemLeave(_item: MenuItem) {
188
+ // Keep open so user can move into submenu
189
+ }
190
+
191
+ private onItemClick(item: MenuItem, index: number, ev?: Event) {
192
+ ev?.stopPropagation();
193
+ if (item.disabled) return;
194
+
195
+ this.focusedIndex = index;
196
+
197
+ if (this.type === "checkbox") {
198
+ const updated = { ...item, checked: !item.checked };
199
+ this._items = this._items.map((i, idx) => idx === index ? updated : i);
200
+ this.dispatchEvent(
201
+ new CustomEvent("rel-check", {
202
+ detail: { item: updated, checked: updated.checked },
203
+ bubbles: true,
204
+ composed: true,
205
+ })
206
+ );
207
+ return;
208
+ }
209
+
210
+ if (this.type === "arrow" && item.submenu?.length) {
211
+ const next = !item.open;
212
+ this._items = this._items.map((i, idx) => ({
213
+ ...i,
214
+ open: idx === index ? next : false,
215
+ }));
216
+ return;
217
+ }
218
+
219
+ // Default: set active
220
+ const updated = { ...item, active: true };
221
+ this._items = this._items.map((i, idx) => idx === index ? updated : { ...i, active: false });
222
+ this.dispatchEvent(
223
+ new CustomEvent("rel-select", {
224
+ detail: { item: updated },
225
+ bubbles: true,
226
+ composed: true,
227
+ })
228
+ );
229
+ }
230
+
231
+ private onSubClick(parentIndex: number, subIndex: number, ev?: Event) {
232
+ ev?.stopPropagation();
233
+ this._items = this._items.map((i, idx) => ({
234
+ ...i,
235
+ active: false,
236
+ open: false,
237
+ submenu: i.submenu?.map((s, sIdx) => ({
238
+ ...s,
239
+ active: idx === parentIndex && sIdx === subIndex,
240
+ })),
241
+ }));
242
+ this.dispatchEvent(
243
+ new CustomEvent("rel-submenu-select", {
244
+ detail: {
245
+ parent: this._items[parentIndex],
246
+ sub: this._items[parentIndex]?.submenu?.[subIndex],
247
+ },
248
+ bubbles: true,
249
+ composed: true,
250
+ })
251
+ );
252
+ }
253
+
254
+ private toggleCheckbox(item: MenuItem, ev: Event) {
255
+ ev.stopPropagation();
256
+ if (!item.disabled) {
257
+ item.checked = !item.checked;
258
+ this.dispatchEvent(
259
+ new CustomEvent("rel-check", {
260
+ detail: { item, checked: item.checked },
261
+ bubbles: true,
262
+ composed: true,
263
+ })
264
+ );
265
+ this.requestUpdate();
266
+ }
267
+ }
268
+
269
+ private onKeydown(ev: KeyboardEvent) {
270
+ const last = this._items.length - 1;
271
+
272
+ if (ev.key === "ArrowDown") {
273
+ this.focusedIndex = Math.min(last, this.focusedIndex + 1);
274
+ ev.preventDefault();
275
+ } else if (ev.key === "ArrowUp") {
276
+ this.focusedIndex = Math.max(0, this.focusedIndex - 1);
277
+ ev.preventDefault();
278
+ } else if (ev.key === "ArrowRight") {
279
+ const item = this._items[this.focusedIndex];
280
+ if (this.type === "arrow" && item?.submenu?.length) {
281
+ this._items = this._items.map((i, idx) => ({
282
+ ...i,
283
+ open: idx === this.focusedIndex ? true : i.open,
284
+ }));
285
+ }
286
+ } else if (ev.key === "ArrowLeft" || ev.key === "Escape") {
287
+ this._items = this._items.map(i => ({ ...i, open: false }));
288
+ } else if (ev.key === "Enter" || ev.key === " ") {
289
+ const item = this._items[this.focusedIndex];
290
+ this.onItemClick(item, this.focusedIndex);
291
+ ev.preventDefault();
292
+ }
293
+ }
294
+
295
+ private renderMenuItem(item: MenuItem, index: number) {
296
+ const isFocused = this.focusedIndex === index;
297
+
298
+ return html`
299
+ <div
300
+ class="rel-menu-item ${item.active ? "active" : ""} ${item.disabled ? "disabled" : ""}"
301
+ role="menuitem"
302
+ ?data-focused="${isFocused}"
303
+ @mouseenter="${() => this.onItemEnter(item, index)}"
304
+ @mouseleave="${() => this.onItemLeave(item)}"
305
+ @click="${(e: Event) => this.onItemClick(item, index, e)}">
306
+ ${this.type === "checkbox"
307
+ ? html`
308
+ <div class="menu-check-row">
309
+ <input
310
+ class="menu-checkbox"
311
+ type="checkbox"
312
+ .checked="${item.checked || false}"
313
+ ?disabled="${item.disabled}"
314
+ @click="${(e: Event) => this.toggleCheckbox(item, e)}" />
315
+ <span class="menu-label">${item.label}</span>
316
+ </div>
317
+ `
318
+ : html`
319
+ <span class="menu-label">${item.label}</span>
320
+ ${this.type === "arrow" && item.submenu?.length
321
+ ? html`<span class="chevron">▸</span>`
322
+ : ""}
323
+ `}
324
+ ${this.type === "arrow" && item.submenu?.length
325
+ ? html`
326
+ <div
327
+ class="rel-submenu ${item.open ? "visible" : ""}"
328
+ @click="${(e: Event) => e.stopPropagation()}">
329
+ ${item.submenu.map(
330
+ (sub, subIndex) => html`
331
+ <div
332
+ class="rel-menu-item ${sub.active ? "active" : ""}"
333
+ role="menuitem"
334
+ @click="${(e: Event) => this.onSubClick(index, subIndex, e)}">
335
+ <span class="menu-label">${sub.label}</span>
336
+ </div>
337
+ `
338
+ )}
339
+ </div>
340
+ `
341
+ : ""}
342
+ </div>
343
+ `;
344
+ }
345
+
346
+ render() {
347
+ return html`
348
+ <style>${RelMenu._css}</style>
349
+ <div
350
+ class="rel-menu"
351
+ role="menu"
352
+ tabindex="0"
353
+ @keydown="${this.onKeydown}"
354
+ @click="${(e: Event) => e.stopPropagation()}">
355
+ ${this._items.map((item, index) => this.renderMenuItem(item, index))}
356
+ </div>
357
+ `;
358
+ }
359
+ }