@reliability-design/web 1.4.5 → 1.4.7

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