@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,1166 @@
1
+ import { LitElement, html, css, nothing } from "lit";
2
+ import { customElement, property, state, query } from "lit/decorators.js";
3
+ import "@reliability-design/icons/rel-icon";
4
+
5
+ export type SidebarVariant = "sidebar" | "floating" | "inset";
6
+ export type SidebarSide = "left" | "right";
7
+ export type SidebarCollapsible = "offcanvas" | "icon" | "none";
8
+ export type SidebarWidth = "sm" | "md" | "lg";
9
+ export type SidebarChangeReason = "trigger" | "rail" | "overlay" | "escape" | "shortcut" | "api";
10
+
11
+ // ============================================================================
12
+ // rel-sidebar-provider (Root State Manager)
13
+ // ============================================================================
14
+ @customElement("rel-sidebar-provider")
15
+ export class RelSidebarProvider extends LitElement {
16
+ @property({ type: Boolean, reflect: true }) open?: boolean;
17
+ @property({ type: Boolean, attribute: "defaultopen" }) defaultOpen = true;
18
+ @property({ type: Boolean, reflect: true }) collapsed?: boolean;
19
+ @property({ type: Boolean, attribute: "defaultcollapsed" }) defaultCollapsed = false;
20
+ @property({ type: String }) variant: SidebarVariant = "sidebar";
21
+ @property({ type: String }) side: SidebarSide = "left";
22
+ @property({ type: String }) collapsible: SidebarCollapsible = "icon";
23
+ @property({ type: String }) width: SidebarWidth = "md";
24
+ @property({ type: String, attribute: "persist-key" }) persistKey = "rel-sidebar";
25
+ @property({ type: Boolean, attribute: "keyboard-shortcut" }) keyboardShortcut = true;
26
+ @property({ type: Boolean, attribute: "disable-persist" }) disablePersist = false;
27
+ @property({ type: Boolean, attribute: "disable-overlay-close" }) disableOverlayClose = false;
28
+
29
+ @state() private internalOpen = true;
30
+ @state() private internalCollapsed = false;
31
+ @state() public isMobile = false;
32
+
33
+ private mediaQuery: MediaQueryList | null = null;
34
+ private isInitialized = false;
35
+ private _uid = Math.random().toString(36).slice(2);
36
+
37
+ private get _storageKey(): string {
38
+ return `rel-sidebar:${this.id || this._uid}`;
39
+ }
40
+
41
+ static styles = css`
42
+ :host {
43
+ display: contents;
44
+ }
45
+ `;
46
+
47
+ connectedCallback() {
48
+ super.connectedCallback();
49
+
50
+ // Setup mobile detection first (synchronous)
51
+ this.setupMediaQuery();
52
+
53
+ // Load state with mobile awareness
54
+ this.loadInitialState();
55
+
56
+ // Setup keyboard shortcut
57
+ if (this.keyboardShortcut) {
58
+ document.addEventListener("keydown", this.handleKeyDown);
59
+ }
60
+
61
+ this.isInitialized = true;
62
+
63
+ // Notify children of initial state after a microtask
64
+ requestAnimationFrame(() => {
65
+ this.notifyStateChange();
66
+ });
67
+ }
68
+
69
+ disconnectedCallback() {
70
+ super.disconnectedCallback();
71
+ if (this.mediaQuery) {
72
+ this.mediaQuery.removeEventListener("change", this.handleMediaChange);
73
+ }
74
+ document.removeEventListener("keydown", this.handleKeyDown);
75
+ }
76
+
77
+ updated(changed: Map<string, unknown>) {
78
+ if (changed.has("open") && this.isOpenControlled()) {
79
+ this.internalOpen = !!this.open;
80
+ this.notifyStateChange();
81
+ }
82
+ if (changed.has("collapsed") && this.isCollapsedControlled()) {
83
+ this.internalCollapsed = !!this.collapsed;
84
+ this.notifyStateChange();
85
+ }
86
+ }
87
+
88
+ private isOpenControlled() {
89
+ return this.open !== undefined;
90
+ }
91
+
92
+ private isCollapsedControlled() {
93
+ return this.collapsed !== undefined;
94
+ }
95
+
96
+ get activeOpen() {
97
+ return this.isOpenControlled() ? !!this.open : this.internalOpen;
98
+ }
99
+
100
+ get activeCollapsed() {
101
+ return this.isCollapsedControlled() ? !!this.collapsed : this.internalCollapsed;
102
+ }
103
+
104
+ private setupMediaQuery() {
105
+ this.mediaQuery = window.matchMedia("(max-width: 768px)");
106
+ this.isMobile = this.mediaQuery.matches;
107
+ this.mediaQuery.addEventListener("change", this.handleMediaChange);
108
+ }
109
+
110
+ private handleMediaChange = (e: MediaQueryListEvent) => {
111
+ const wasMobile = this.isMobile;
112
+ this.isMobile = e.matches;
113
+
114
+ // When switching to mobile, close the sidebar unless controlled
115
+ if (!wasMobile && this.isMobile && !this.isOpenControlled()) {
116
+ this.internalOpen = false;
117
+ this.persistState();
118
+ }
119
+
120
+ this.notifyStateChange();
121
+ };
122
+
123
+ private isOffcanvasMode(): boolean {
124
+ return this.isMobile || this.collapsible === "offcanvas";
125
+ }
126
+
127
+ private loadInitialState() {
128
+ // If controlled, use controlled value
129
+ if (this.isOpenControlled()) {
130
+ this.internalOpen = !!this.open;
131
+ } else if (this.isOffcanvasMode()) {
132
+ // Mobile/offcanvas: always start closed, ignore persisted state
133
+ this.internalOpen = false;
134
+ } else {
135
+ // Desktop: use persisted state or default
136
+ if (!this.disablePersist) {
137
+ this.loadPersistedState();
138
+ } else {
139
+ this.internalOpen = this.defaultOpen;
140
+ this.internalCollapsed = this.defaultCollapsed;
141
+ }
142
+ }
143
+
144
+ // Load collapsed state
145
+ if (!this.isCollapsedControlled()) {
146
+ if (!this.disablePersist && !this.isOffcanvasMode()) {
147
+ const persisted = this.getPersistedState();
148
+ if (persisted) {
149
+ this.internalCollapsed = persisted.collapsed ?? this.defaultCollapsed;
150
+ } else {
151
+ this.internalCollapsed = this.defaultCollapsed;
152
+ }
153
+ } else {
154
+ this.internalCollapsed = this.defaultCollapsed;
155
+ }
156
+ } else {
157
+ this.internalCollapsed = !!this.collapsed;
158
+ }
159
+ }
160
+
161
+ private getPersistedState(): { open?: boolean; collapsed?: boolean } | null {
162
+ try {
163
+ const stored = localStorage.getItem(this._storageKey);
164
+ if (stored) {
165
+ return JSON.parse(stored);
166
+ }
167
+ } catch (e) {
168
+ // ignore
169
+ }
170
+ return null;
171
+ }
172
+
173
+ private loadPersistedState() {
174
+ const persisted = this.getPersistedState();
175
+ if (persisted) {
176
+ this.internalOpen = persisted.open ?? this.defaultOpen;
177
+ this.internalCollapsed = persisted.collapsed ?? this.defaultCollapsed;
178
+ } else {
179
+ this.internalOpen = this.defaultOpen;
180
+ this.internalCollapsed = this.defaultCollapsed;
181
+ }
182
+ }
183
+
184
+ private persistState() {
185
+ if (this.disablePersist) return;
186
+
187
+ // Don't persist open state for offcanvas mode
188
+ const stateToPersist = this.isOffcanvasMode()
189
+ ? { collapsed: this.activeCollapsed }
190
+ : { open: this.activeOpen, collapsed: this.activeCollapsed };
191
+
192
+ try {
193
+ localStorage.setItem(this._storageKey, JSON.stringify(stateToPersist));
194
+ } catch (e) {
195
+ // Silently fail if localStorage is unavailable
196
+ }
197
+ }
198
+
199
+ private handleKeyDown = (e: KeyboardEvent) => {
200
+ if (!this.keyboardShortcut) return;
201
+
202
+ // Ctrl/Cmd + B
203
+ if ((e.ctrlKey || e.metaKey) && e.key === "b") {
204
+ e.preventDefault();
205
+
206
+ if (this.isMobile || this.collapsible === "offcanvas") {
207
+ // Toggle open state
208
+ this.setOpen(!this.activeOpen, "shortcut");
209
+ } else if (this.collapsible === "icon") {
210
+ // Toggle collapsed state
211
+ this.setCollapsed(!this.activeCollapsed, "shortcut");
212
+ }
213
+ }
214
+ };
215
+
216
+ setOpen(next: boolean, reason: SidebarChangeReason = "api") {
217
+ if (!this.isOpenControlled()) {
218
+ this.internalOpen = next;
219
+ }
220
+
221
+ this.persistState();
222
+ this.notifyStateChange();
223
+ this.dispatchChangeEvent(reason);
224
+ }
225
+
226
+ setCollapsed(next: boolean, reason: SidebarChangeReason = "api") {
227
+ if (!this.isCollapsedControlled()) {
228
+ this.internalCollapsed = next;
229
+ }
230
+
231
+ this.persistState();
232
+ this.notifyStateChange();
233
+ this.dispatchChangeEvent(reason);
234
+ }
235
+
236
+ private dispatchChangeEvent(reason: SidebarChangeReason) {
237
+ this.dispatchEvent(
238
+ new CustomEvent("rel-sidebar-change", {
239
+ detail: {
240
+ open: this.activeOpen,
241
+ collapsed: this.activeCollapsed,
242
+ reason,
243
+ },
244
+ bubbles: true,
245
+ composed: true,
246
+ })
247
+ );
248
+ }
249
+
250
+ private notifyStateChange() {
251
+ // Dispatch a state event that children can listen to
252
+ this.dispatchEvent(
253
+ new CustomEvent("rel-sidebar-state", {
254
+ detail: {
255
+ isOpen: this.activeOpen,
256
+ isCollapsed: this.activeCollapsed,
257
+ isMobile: this.isMobile,
258
+ side: this.side,
259
+ variant: this.variant,
260
+ collapsible: this.collapsible,
261
+ width: this.width,
262
+ },
263
+ bubbles: true,
264
+ composed: true,
265
+ })
266
+ );
267
+ }
268
+
269
+ render() {
270
+ return html`<slot></slot>`;
271
+ }
272
+ }
273
+
274
+ // ============================================================================
275
+ // rel-sidebar (Visual Container)
276
+ // ============================================================================
277
+ @customElement("rel-sidebar")
278
+ export class RelSidebar extends LitElement {
279
+ @property({ type: Boolean }) inset = false;
280
+
281
+ @state() private isOpen = true;
282
+ @state() private isCollapsed = false;
283
+ @state() private isMobile = false;
284
+ @state() private side: SidebarSide = "left";
285
+ @state() private variant: SidebarVariant = "sidebar";
286
+ @state() private collapsible: SidebarCollapsible = "icon";
287
+ @state() private width: SidebarWidth = "md";
288
+
289
+ @query(".sidebar-container") private containerElement!: HTMLElement;
290
+
291
+ private provider: RelSidebarProvider | null = null;
292
+ private triggerElement: HTMLElement | null = null;
293
+ private originalBodyOverflow = "";
294
+ private originalBodyPaddingRight = "";
295
+
296
+ static styles = css`
297
+ :host {
298
+ --sidebar-width-sm: 240px;
299
+ --sidebar-width-md: 280px;
300
+ --sidebar-width-lg: 320px;
301
+ --sidebar-width-collapsed: 56px;
302
+ --sidebar-transition-duration: 0.2s;
303
+ --sidebar-transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
304
+ }
305
+
306
+ .sidebar-wrapper {
307
+ position: relative;
308
+ height: 100%;
309
+ }
310
+
311
+ /* Overlay for offcanvas mode */
312
+ .overlay {
313
+ position: fixed;
314
+ top: 0;
315
+ left: 0;
316
+ right: 0;
317
+ bottom: 0;
318
+ /* TODO: replace scrim fallback with a proper dark-aware token in vars.css */
319
+ background: var(--rel-overlay-scrim, rgba(0, 0, 0, 0.6));
320
+ z-index: 999;
321
+ opacity: 0;
322
+ pointer-events: none;
323
+ transition: opacity var(--sidebar-transition-duration)
324
+ var(--sidebar-transition-timing);
325
+ }
326
+
327
+ .overlay.visible {
328
+ opacity: 1;
329
+ pointer-events: auto;
330
+ }
331
+
332
+ /* Sidebar container */
333
+ .sidebar-container {
334
+ position: fixed;
335
+ top: 0;
336
+ bottom: 0;
337
+ z-index: 1000;
338
+ background: var(--rel-surface-secondary, #fafafa);
339
+ color: var(--rel-text-primary, #333);
340
+ display: flex;
341
+ flex-direction: column;
342
+ transition: transform var(--sidebar-transition-duration)
343
+ var(--sidebar-transition-timing),
344
+ width var(--sidebar-transition-duration)
345
+ var(--sidebar-transition-timing);
346
+ width: var(--sidebar-width);
347
+ outline: none;
348
+ overflow: hidden;
349
+ }
350
+
351
+ /* Left side */
352
+ .sidebar-container[data-side="left"] {
353
+ left: 0;
354
+ border-right: 1px solid var(--rel-border-default, #ddd);
355
+ }
356
+
357
+ .sidebar-container[data-side="left"].offcanvas-hidden {
358
+ transform: translateX(-100%);
359
+ }
360
+
361
+ /* Right side */
362
+ .sidebar-container[data-side="right"] {
363
+ right: 0;
364
+ border-left: 1px solid var(--rel-border-default, #ddd);
365
+ }
366
+
367
+ .sidebar-container[data-side="right"].offcanvas-hidden {
368
+ transform: translateX(100%);
369
+ }
370
+
371
+ /* Floating variant */
372
+ .sidebar-container.variant-floating {
373
+ top: var(--rel-spacing-space-md, 16px);
374
+ bottom: var(--rel-spacing-space-md, 16px);
375
+ background: var(--rel-surface-secondary, #fafafa);
376
+ border: 1px solid var(--rel-border-default, #ddd);
377
+ border-radius: var(--rel-borders-radius-border-radius-300, 16px);
378
+ box-shadow: var(--rel-shadows-shadow-400);
379
+ }
380
+
381
+ .sidebar-container.variant-floating[data-side="left"] {
382
+ left: var(--rel-spacing-space-md, 16px);
383
+ }
384
+
385
+ .sidebar-container.variant-floating[data-side="right"] {
386
+ right: var(--rel-spacing-space-md, 16px);
387
+ }
388
+
389
+ /* Inset variant */
390
+ .sidebar-container.variant-inset {
391
+ position: relative;
392
+ border: none;
393
+ border-radius: 0;
394
+ box-shadow: none;
395
+ }
396
+
397
+ .sidebar-container.variant-inset[data-side="left"] {
398
+ border-right: 1px solid var(--rel-border-default, #ddd);
399
+ }
400
+
401
+ .sidebar-container.variant-inset[data-side="right"] {
402
+ border-left: 1px solid var(--rel-border-default, #ddd);
403
+ }
404
+
405
+ /* Desktop mode - not offcanvas */
406
+ @media (min-width: 769px) {
407
+ .sidebar-container:not(.offcanvas-mode) {
408
+ position: relative;
409
+ }
410
+
411
+ .overlay:not(.offcanvas-mode) {
412
+ display: none;
413
+ }
414
+ }
415
+
416
+ /* Collapsed state */
417
+ .sidebar-container.collapsed {
418
+ width: var(--sidebar-width-collapsed);
419
+ }
420
+
421
+ /* Reduced motion */
422
+ @media (prefers-reduced-motion: reduce) {
423
+ .overlay,
424
+ .sidebar-container {
425
+ transition: none;
426
+ }
427
+ }
428
+ `;
429
+
430
+ connectedCallback() {
431
+ super.connectedCallback();
432
+ this.findProvider();
433
+ }
434
+
435
+ disconnectedCallback() {
436
+ super.disconnectedCallback();
437
+ this.cleanup();
438
+ }
439
+
440
+ private findProvider() {
441
+ let parent = this.parentElement;
442
+ while (parent) {
443
+ if (parent instanceof RelSidebarProvider) {
444
+ this.provider = parent;
445
+ break;
446
+ }
447
+ parent = parent.parentElement;
448
+ }
449
+
450
+ if (this.provider) {
451
+ this.provider.addEventListener(
452
+ "rel-sidebar-state",
453
+ this.handleStateChange,
454
+ );
455
+ // Trigger initial state sync
456
+ this.syncState();
457
+ }
458
+ }
459
+
460
+ private syncState() {
461
+ if (!this.provider) return;
462
+
463
+ const wasOpen = this.isOpen;
464
+ const wasOffcanvas = this.isOffcanvasMode();
465
+
466
+ this.isOpen = this.provider.activeOpen;
467
+ this.isCollapsed = this.provider.activeCollapsed;
468
+ this.isMobile = this.provider.isMobile;
469
+ this.side = this.provider.side;
470
+ this.variant = this.provider.variant;
471
+ this.collapsible = this.provider.collapsible;
472
+ this.width = this.provider.width;
473
+
474
+ const isOffcanvas = this.isOffcanvasMode();
475
+
476
+ // Handle offcanvas behavior
477
+ if (isOffcanvas) {
478
+ if (this.isOpen && !wasOpen) {
479
+ this.lockScroll();
480
+ this.setupFocusTrap();
481
+ } else if (!this.isOpen && wasOpen) {
482
+ this.unlockScroll();
483
+ this.removeFocusTrap();
484
+ }
485
+ } else if (wasOffcanvas && !isOffcanvas) {
486
+ // Switched from offcanvas to normal mode
487
+ this.unlockScroll();
488
+ this.removeFocusTrap();
489
+ }
490
+
491
+ this.requestUpdate();
492
+ }
493
+
494
+ private handleStateChange = (e: Event) => {
495
+ this.syncState();
496
+ };
497
+
498
+ private isOffcanvasMode(): boolean {
499
+ return this.isMobile || this.collapsible === "offcanvas";
500
+ }
501
+
502
+ private lockScroll() {
503
+ if (typeof document === "undefined") return;
504
+
505
+ const scrollbarWidth =
506
+ window.innerWidth - document.documentElement.clientWidth;
507
+
508
+ this.originalBodyOverflow = document.body.style.overflow;
509
+ this.originalBodyPaddingRight = document.body.style.paddingRight;
510
+
511
+ document.body.style.overflow = "hidden";
512
+ if (scrollbarWidth > 0) {
513
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
514
+ }
515
+ }
516
+
517
+ private unlockScroll() {
518
+ if (typeof document === "undefined") return;
519
+
520
+ document.body.style.overflow = this.originalBodyOverflow;
521
+ document.body.style.paddingRight = this.originalBodyPaddingRight;
522
+ }
523
+
524
+ private setupFocusTrap() {
525
+ this.triggerElement = document.activeElement as HTMLElement;
526
+ document.addEventListener("keydown", this.handleKeydown);
527
+
528
+ // Focus first focusable element or container
529
+ requestAnimationFrame(() => {
530
+ const focusable = this.getFocusableElements();
531
+ if (focusable.length > 0) {
532
+ focusable[0].focus();
533
+ } else if (this.containerElement) {
534
+ this.containerElement.setAttribute("tabindex", "-1");
535
+ this.containerElement.focus();
536
+ }
537
+ });
538
+ }
539
+
540
+ private removeFocusTrap() {
541
+ document.removeEventListener("keydown", this.handleKeydown);
542
+
543
+ // Restore focus to trigger
544
+ if (this.triggerElement && this.triggerElement.focus) {
545
+ requestAnimationFrame(() => {
546
+ this.triggerElement?.focus();
547
+ this.triggerElement = null;
548
+ });
549
+ }
550
+ }
551
+
552
+ private getFocusableElements(): HTMLElement[] {
553
+ if (!this.containerElement) return [];
554
+
555
+ const selectors = [
556
+ "a[href]",
557
+ "button:not([disabled])",
558
+ "textarea:not([disabled])",
559
+ "input:not([disabled])",
560
+ "select:not([disabled])",
561
+ '[tabindex]:not([tabindex="-1"])',
562
+ ].join(",");
563
+
564
+ const elements = Array.from(
565
+ this.containerElement.querySelectorAll(selectors),
566
+ ) as HTMLElement[];
567
+
568
+ return elements.filter((el) => el.offsetWidth || el.offsetHeight);
569
+ }
570
+
571
+ private handleKeydown = (e: KeyboardEvent) => {
572
+ if (!this.isOpen || !this.isOffcanvasMode()) return;
573
+
574
+ if (e.key === "Escape") {
575
+ e.preventDefault();
576
+ this.provider?.setOpen(false, "escape");
577
+ return;
578
+ }
579
+
580
+ // Focus trap
581
+ if (e.key === "Tab") {
582
+ const focusable = this.getFocusableElements();
583
+ if (focusable.length === 0) {
584
+ e.preventDefault();
585
+ return;
586
+ }
587
+
588
+ const firstFocusable = focusable[0];
589
+ const lastFocusable = focusable[focusable.length - 1];
590
+ const activeElement =
591
+ this.shadowRoot?.activeElement || document.activeElement;
592
+
593
+ if (e.shiftKey) {
594
+ if (activeElement === firstFocusable) {
595
+ e.preventDefault();
596
+ lastFocusable.focus();
597
+ }
598
+ } else {
599
+ if (activeElement === lastFocusable) {
600
+ e.preventDefault();
601
+ firstFocusable.focus();
602
+ }
603
+ }
604
+ }
605
+ };
606
+
607
+ private handleOverlayClick = () => {
608
+ if (this.provider && !this.provider.disableOverlayClose) {
609
+ this.provider.setOpen(false, "overlay");
610
+ }
611
+ };
612
+
613
+ private cleanup() {
614
+ this.unlockScroll();
615
+ this.removeFocusTrap();
616
+ if (this.provider) {
617
+ this.provider.removeEventListener(
618
+ "rel-sidebar-state",
619
+ this.handleStateChange,
620
+ );
621
+ }
622
+ }
623
+
624
+ private getWidthValue(): string {
625
+ const widthMap: Record<SidebarWidth, string> = {
626
+ sm: "var(--sidebar-width-sm)",
627
+ md: "var(--sidebar-width-md)",
628
+ lg: "var(--sidebar-width-lg)",
629
+ };
630
+ return widthMap[this.width] || widthMap.md;
631
+ }
632
+
633
+ render() {
634
+ const isOffcanvas = this.isOffcanvasMode();
635
+ const showOverlay = isOffcanvas && this.isOpen;
636
+ const containerClasses = [
637
+ "sidebar-container",
638
+ `variant-${this.variant}`,
639
+ this.isCollapsed && !isOffcanvas ? "collapsed" : "",
640
+ isOffcanvas ? "offcanvas-mode" : "",
641
+ isOffcanvas && !this.isOpen ? "offcanvas-hidden" : "",
642
+ ]
643
+ .filter(Boolean)
644
+ .join(" ");
645
+
646
+ return html`
647
+ <div class="sidebar-wrapper">
648
+ ${showOverlay
649
+ ? html`
650
+ <div
651
+ class="overlay visible"
652
+ @click="${this.handleOverlayClick}"
653
+ ></div>
654
+ `
655
+ : ""}
656
+
657
+ <nav
658
+ class="${containerClasses}"
659
+ data-side="${this.side}"
660
+ style="--sidebar-width: ${this.getWidthValue()}"
661
+ role="navigation"
662
+ aria-hidden="${isOffcanvas && !this.isOpen}"
663
+ tabindex="-1"
664
+ >
665
+ <slot></slot>
666
+ </nav>
667
+ </div>
668
+ `;
669
+ }
670
+ }
671
+
672
+ // ============================================================================
673
+ // rel-sidebar-trigger (Toggle Button)
674
+ // ============================================================================
675
+ @customElement("rel-sidebar-trigger")
676
+ export class RelSidebarTrigger extends LitElement {
677
+ @state() private isOpen = true;
678
+ @state() private isCollapsed = false;
679
+
680
+ private provider: RelSidebarProvider | null = null;
681
+
682
+ static styles = css`
683
+ :host {
684
+ display: inline-block;
685
+ }
686
+
687
+ button {
688
+ background: none;
689
+ border: none;
690
+ padding: var(--rel-spacing-space-xs, 8px);
691
+ margin: 0;
692
+ cursor: pointer;
693
+ display: flex;
694
+ align-items: center;
695
+ justify-content: center;
696
+ border-radius: var(--rel-borders-radius-border-radius-200, 8px);
697
+ color: var(--rel-text-heading, #2b2b2b);
698
+ transition: background 0.15s ease;
699
+ }
700
+
701
+ button:hover {
702
+ background: var(--rel-surface-surface, #f5f5f5);
703
+ }
704
+
705
+ button:active {
706
+ background: var(--rel-border-default, #ddd);
707
+ }
708
+
709
+ button:focus-visible {
710
+ outline: 2px solid var(--rel-border-info, #0091ea);
711
+ outline-offset: 2px;
712
+ }
713
+ `;
714
+
715
+ connectedCallback() {
716
+ super.connectedCallback();
717
+ this.findProvider();
718
+ }
719
+
720
+ disconnectedCallback() {
721
+ super.disconnectedCallback();
722
+ if (this.provider) {
723
+ this.provider.removeEventListener(
724
+ "rel-sidebar-state",
725
+ this.handleStateChange,
726
+ );
727
+ }
728
+ }
729
+
730
+ private findProvider() {
731
+ let parent = this.parentElement;
732
+ while (parent) {
733
+ if (parent instanceof RelSidebarProvider) {
734
+ this.provider = parent;
735
+ break;
736
+ }
737
+ parent = parent.parentElement;
738
+ }
739
+
740
+ if (this.provider) {
741
+ this.provider.addEventListener(
742
+ "rel-sidebar-state",
743
+ this.handleStateChange,
744
+ );
745
+ this.syncState();
746
+ }
747
+ }
748
+
749
+ private syncState() {
750
+ if (!this.provider) return;
751
+ this.isOpen = this.provider.activeOpen;
752
+ this.isCollapsed = this.provider.activeCollapsed;
753
+ }
754
+
755
+ private handleStateChange = () => {
756
+ this.syncState();
757
+ };
758
+
759
+ private handleClick = () => {
760
+ if (!this.provider) return;
761
+
762
+ if (this.provider.isMobile || this.provider.collapsible === "offcanvas") {
763
+ this.provider.setOpen(!this.isOpen, "trigger");
764
+ } else if (this.provider.collapsible === "icon") {
765
+ this.provider.setCollapsed(!this.isCollapsed, "rail");
766
+ }
767
+ };
768
+
769
+ render() {
770
+ return html`
771
+ <button
772
+ @click="${this.handleClick}"
773
+ type="button"
774
+ aria-expanded="${this.isOpen}"
775
+ aria-label="Toggle sidebar"
776
+ >
777
+ <slot>
778
+ <svg
779
+ width="20"
780
+ height="20"
781
+ viewBox="0 0 24 24"
782
+ fill="none"
783
+ stroke="currentColor"
784
+ stroke-width="2"
785
+ >
786
+ <line x1="3" y1="12" x2="21" y2="12"></line>
787
+ <line x1="3" y1="6" x2="21" y2="6"></line>
788
+ <line x1="3" y1="18" x2="21" y2="18"></line>
789
+ </svg>
790
+ </slot>
791
+ </button>
792
+ `;
793
+ }
794
+ }
795
+
796
+ // ============================================================================
797
+ // Layout Helpers
798
+ // ============================================================================
799
+
800
+ @customElement("rel-sidebar-header")
801
+ export class RelSidebarHeader extends LitElement {
802
+ static styles = css`
803
+ :host {
804
+ display: block;
805
+ padding: var(--rel-spacing-space-md, 16px);
806
+ border-bottom: 1px solid var(--rel-border-default, #ddd);
807
+ flex-shrink: 0;
808
+ color: var(--rel-text-heading, #2b2b2b);
809
+ }
810
+ `;
811
+
812
+ render() {
813
+ return html`<slot></slot>`;
814
+ }
815
+ }
816
+
817
+ @customElement("rel-sidebar-content")
818
+ export class RelSidebarContent extends LitElement {
819
+ static styles = css`
820
+ :host {
821
+ display: block;
822
+ flex: 1;
823
+ overflow-y: auto;
824
+ padding: var(--rel-spacing-space-sm, 12px) var(--rel-spacing-space-xs, 8px);
825
+ }
826
+
827
+ /* Scrollbar styling */
828
+ :host::-webkit-scrollbar {
829
+ width: 4px;
830
+ }
831
+ :host::-webkit-scrollbar-track {
832
+ background: transparent;
833
+ }
834
+ :host::-webkit-scrollbar-thumb {
835
+ background: var(--rel-border-default, #ddd);
836
+ border-radius: 2px;
837
+ }
838
+ `;
839
+
840
+ render() {
841
+ return html`<slot></slot>`;
842
+ }
843
+ }
844
+
845
+ @customElement("rel-sidebar-footer")
846
+ export class RelSidebarFooter extends LitElement {
847
+ static styles = css`
848
+ :host {
849
+ display: block;
850
+ padding: var(--rel-spacing-space-md, 16px);
851
+ border-top: 1px solid var(--rel-border-default, #ddd);
852
+ flex-shrink: 0;
853
+ color: var(--rel-text-primary, #333);
854
+ }
855
+ `;
856
+
857
+ render() {
858
+ return html`<slot></slot>`;
859
+ }
860
+ }
861
+
862
+ @customElement("rel-sidebar-group")
863
+ export class RelSidebarGroup extends LitElement {
864
+ static styles = css`
865
+ :host {
866
+ display: block;
867
+ margin-bottom: var(--rel-spacing-space-md, 16px);
868
+ }
869
+
870
+ :host(:last-child) {
871
+ margin-bottom: 0;
872
+ }
873
+ `;
874
+
875
+ render() {
876
+ return html`<slot></slot>`;
877
+ }
878
+ }
879
+
880
+ @customElement("rel-sidebar-group-label")
881
+ export class RelSidebarGroupLabel extends LitElement {
882
+ static styles = css`
883
+ :host {
884
+ display: block;
885
+ font-size: var(--rel-fontSizes-xs, 10px);
886
+ font-weight: var(--rel-fontWeights-semibold, 600);
887
+ color: var(--rel-text-placeholder, #a9a9a9);
888
+ text-transform: uppercase;
889
+ letter-spacing: 0.05em;
890
+ padding: var(--rel-spacing-space-xs, 8px) var(--rel-spacing-space-sm, 12px) var(--rel-spacing-space-2xs, 4px);
891
+ margin-bottom: var(--rel-spacing-space-2xs, 4px);
892
+ }
893
+
894
+ :host(.collapsed) {
895
+ text-align: center;
896
+ font-size: var(--rel-fontSizes-xs, 10px);
897
+ padding: var(--rel-spacing-space-xs, 8px) var(--rel-spacing-space-2xs, 4px) var(--rel-spacing-space-2xs, 4px);
898
+ }
899
+ `;
900
+
901
+ render() {
902
+ return html`<slot></slot>`;
903
+ }
904
+ }
905
+
906
+ @customElement("rel-sidebar-group-content")
907
+ export class RelSidebarGroupContent extends LitElement {
908
+ static styles = css`
909
+ :host {
910
+ display: block;
911
+ }
912
+ `;
913
+
914
+ render() {
915
+ return html`<slot></slot>`;
916
+ }
917
+ }
918
+
919
+ // ============================================================================
920
+ // Menu Primitives
921
+ // ============================================================================
922
+
923
+ @customElement("rel-sidebar-menu")
924
+ export class RelSidebarMenu extends LitElement {
925
+ static styles = css`
926
+ :host {
927
+ display: block;
928
+ list-style: none;
929
+ margin: 0;
930
+ padding: 0;
931
+ }
932
+ `;
933
+
934
+ connectedCallback() {
935
+ super.connectedCallback();
936
+ this.addEventListener("keydown", this.handleKeyDown);
937
+ }
938
+
939
+ disconnectedCallback() {
940
+ super.disconnectedCallback();
941
+ this.removeEventListener("keydown", this.handleKeyDown);
942
+ }
943
+
944
+ private handleKeyDown = (e: KeyboardEvent) => {
945
+ if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
946
+
947
+ e.preventDefault();
948
+ const items = Array.from(
949
+ this.querySelectorAll("rel-sidebar-menu-button:not([disabled])"),
950
+ ) as HTMLElement[];
951
+ if (items.length === 0) return;
952
+
953
+ const active = items.findIndex(
954
+ (item) => item.shadowRoot?.activeElement || item === document.activeElement,
955
+ );
956
+ let next: number;
957
+ if (e.key === "ArrowDown") {
958
+ next = active < items.length - 1 ? active + 1 : 0;
959
+ } else {
960
+ next = active > 0 ? active - 1 : items.length - 1;
961
+ }
962
+
963
+ const btn = items[next]?.shadowRoot?.querySelector("button") as HTMLElement | null;
964
+ btn?.focus();
965
+ };
966
+
967
+ render() {
968
+ return html`<slot></slot>`;
969
+ }
970
+ }
971
+
972
+ @customElement("rel-sidebar-menu-item")
973
+ export class RelSidebarMenuItem extends LitElement {
974
+ static styles = css`
975
+ :host {
976
+ display: block;
977
+ margin-bottom: var(--rel-spacing-space-2xs, 4px);
978
+ padding: 0 var(--rel-spacing-space-xs, 8px);
979
+ }
980
+ `;
981
+
982
+ render() {
983
+ return html`<slot></slot>`;
984
+ }
985
+ }
986
+
987
+ @customElement("rel-sidebar-menu-button")
988
+ export class RelSidebarMenuButton extends LitElement {
989
+ @property({ type: Boolean }) active = false;
990
+ @property({ type: Boolean }) disabled = false;
991
+ @property({ type: String }) tooltip = "";
992
+ @property({ type: String }) icon: string = "";
993
+
994
+ @state() private isCollapsed = false;
995
+
996
+ private provider: RelSidebarProvider | null = null;
997
+
998
+ static styles = css`
999
+ :host {
1000
+ display: block;
1001
+ }
1002
+
1003
+ button {
1004
+ width: 100%;
1005
+ display: flex;
1006
+ align-items: center;
1007
+ gap: var(--rel-spacing-space-sm, 12px);
1008
+ padding: var(--rel-spacing-space-xs, 8px) var(--rel-spacing-space-sm, 12px);
1009
+ min-height: 40px;
1010
+ background: none;
1011
+ border: none;
1012
+ border-radius: var(--rel-borders-radius-border-radius-200, 8px);
1013
+ cursor: pointer;
1014
+ font-family: var(--rel-fontFamilies-primary, sans-serif);
1015
+ font-size: var(--rel-fontSizes-md, 14px);
1016
+ font-weight: var(--rel-fontWeights-medium, 500);
1017
+ color: var(--rel-text-primary, #333);
1018
+ transition: background 0.15s ease, color 0.15s ease;
1019
+ text-align: left;
1020
+ position: relative;
1021
+ }
1022
+
1023
+ button:hover:not([disabled]) {
1024
+ background: var(--rel-surface-surface, #f5f5f5);
1025
+ color: var(--rel-text-heading, #2b2b2b);
1026
+ }
1027
+
1028
+ button:active:not([disabled]) {
1029
+ background: var(--rel-border-default, #ddd);
1030
+ }
1031
+
1032
+ button:focus-visible {
1033
+ outline: 2px solid var(--rel-border-info, #0091ea);
1034
+ outline-offset: -2px;
1035
+ }
1036
+
1037
+ button[disabled] {
1038
+ opacity: 0.5;
1039
+ cursor: not-allowed;
1040
+ }
1041
+
1042
+ button.active {
1043
+ background: var(--rel-surface-surface, #f5f5f5);
1044
+ color: var(--rel-text-heading, #2b2b2b);
1045
+ font-weight: var(--rel-fontWeights-semibold, 600);
1046
+ }
1047
+
1048
+ button.active::before {
1049
+ content: "";
1050
+ position: absolute;
1051
+ left: 0;
1052
+ top: 50%;
1053
+ transform: translateY(-50%);
1054
+ width: 3px;
1055
+ height: 20px;
1056
+ background: var(--rel-colors-primary-500, #0091ea);
1057
+ border-radius: 0 2px 2px 0;
1058
+ }
1059
+
1060
+ button.collapsed {
1061
+ justify-content: center;
1062
+ padding: var(--rel-spacing-space-xs, 8px);
1063
+ min-height: 40px;
1064
+ }
1065
+
1066
+ button.collapsed.active::before {
1067
+ display: none;
1068
+ }
1069
+
1070
+ button.collapsed.active {
1071
+ background: var(--rel-surface-surface, #f5f5f5);
1072
+ }
1073
+
1074
+ .icon {
1075
+ flex-shrink: 0;
1076
+ display: flex;
1077
+ align-items: center;
1078
+ justify-content: center;
1079
+ width: 18px;
1080
+ height: 18px;
1081
+ }
1082
+
1083
+ .label {
1084
+ flex: 1;
1085
+ white-space: nowrap;
1086
+ overflow: hidden;
1087
+ text-overflow: ellipsis;
1088
+ line-height: 1.4;
1089
+ }
1090
+
1091
+ button.collapsed .label {
1092
+ display: none;
1093
+ }
1094
+ `;
1095
+
1096
+ connectedCallback() {
1097
+ super.connectedCallback();
1098
+ this.findProvider();
1099
+ }
1100
+
1101
+ disconnectedCallback() {
1102
+ super.disconnectedCallback();
1103
+ if (this.provider) {
1104
+ this.provider.removeEventListener(
1105
+ "rel-sidebar-state",
1106
+ this.handleStateChange,
1107
+ );
1108
+ }
1109
+ }
1110
+
1111
+ private findProvider() {
1112
+ let parent = this.parentElement;
1113
+ while (parent) {
1114
+ if (parent instanceof RelSidebarProvider) {
1115
+ this.provider = parent;
1116
+ break;
1117
+ }
1118
+ parent = parent.parentElement;
1119
+ }
1120
+
1121
+ if (this.provider) {
1122
+ this.provider.addEventListener(
1123
+ "rel-sidebar-state",
1124
+ this.handleStateChange,
1125
+ );
1126
+ this.syncState();
1127
+ }
1128
+ }
1129
+
1130
+ private syncState() {
1131
+ if (!this.provider) return;
1132
+ this.isCollapsed = this.provider.activeCollapsed && !this.provider.isMobile;
1133
+ }
1134
+
1135
+ private handleStateChange = () => {
1136
+ this.syncState();
1137
+ };
1138
+
1139
+ render() {
1140
+ const buttonClass = [
1141
+ this.active ? "active" : "",
1142
+ this.isCollapsed ? "collapsed" : "",
1143
+ ]
1144
+ .filter(Boolean)
1145
+ .join(" ");
1146
+
1147
+ const tooltipTitle = this.isCollapsed && this.tooltip ? this.tooltip : "";
1148
+
1149
+ return html`
1150
+ <button
1151
+ class="${buttonClass}"
1152
+ ?disabled="${this.disabled}"
1153
+ title="${tooltipTitle}"
1154
+ aria-current="${this.active ? "page" : "false"}"
1155
+ part="button"
1156
+ >
1157
+ <span class="icon" part="icon">
1158
+ ${this.icon ? html`<rel-icon name="${this.icon}"></rel-icon>` : nothing}
1159
+ </span>
1160
+ <span class="label" part="label">
1161
+ <slot></slot>
1162
+ </span>
1163
+ </button>
1164
+ `;
1165
+ }
1166
+ }