@reliability-design/web 1.4.6 → 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.
- package/CHANGELOG.md +211 -49
- package/dist/components/rel-accordion/README.md +132 -132
- package/dist/components/rel-alert/README.md +145 -141
- package/dist/components/rel-autocomplete/README.md +156 -61
- package/dist/components/rel-avatar/README.md +116 -130
- package/dist/components/rel-badge/README.md +98 -93
- package/dist/components/rel-breadcrumb/README.md +96 -102
- package/dist/components/rel-button/README.md +160 -130
- package/dist/components/rel-calendar/README.md +109 -109
- package/dist/components/rel-card/README.md +75 -94
- package/dist/components/rel-cardmedia/README.md +100 -127
- package/dist/components/rel-carousel/README.md +184 -166
- package/dist/components/rel-checkbox/README.md +93 -75
- package/dist/components/rel-chip/README.md +132 -121
- package/dist/components/rel-container/README.md +107 -99
- package/dist/components/rel-date-picker/README.md +200 -289
- package/dist/components/rel-divider/README.md +78 -100
- package/dist/components/rel-drawer/README.md +187 -200
- package/dist/components/rel-kbd/README.md +141 -151
- package/dist/components/rel-menu/README.md +178 -151
- package/dist/components/rel-menubar/README.md +218 -329
- package/dist/components/rel-otp/README.md +170 -440
- package/dist/components/rel-pagination/README.md +100 -424
- package/dist/components/rel-popover/README.md +203 -568
- package/dist/components/rel-progress/README.md +88 -110
- package/dist/components/rel-radio/README.md +131 -138
- package/dist/components/rel-range-picker/README.md +162 -0
- package/dist/components/rel-resizable/README.md +199 -140
- package/dist/components/rel-segmented-button/README.md +155 -141
- package/dist/components/rel-select/README.md +154 -93
- package/dist/components/rel-sheet/README.md +168 -224
- package/dist/components/rel-skeleton/README.md +103 -0
- package/dist/components/rel-slider/README.md +130 -135
- package/dist/components/rel-snackbar/README.md +162 -134
- package/dist/components/rel-stepper/README.md +158 -160
- package/dist/components/rel-switch/README.md +108 -102
- package/dist/components/rel-tab/README.md +171 -163
- package/dist/components/rel-textfield/README.md +159 -69
- package/dist/components/rel-time-picker/README.md +122 -157
- package/dist/components/rel-toast/README.md +157 -148
- package/dist/components/rel-tooltip/README.md +108 -111
- package/dist/components/rel-upload/README.md +220 -0
- package/dist/index.d.mts +1147 -340
- package/dist/index.d.ts +1147 -340
- package/dist/index.js +3451 -3163
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3416 -3118
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +877 -866
- package/dist/{vars-XNMTSRVN.css → vars-JHTUGNRG.css} +877 -866
- package/package.json +3 -2
- package/dist/components/rel-accordion/rel-accordion.ts +0 -336
- package/dist/components/rel-alert/rel-alert.ts +0 -305
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +0 -720
- package/dist/components/rel-avatar/rel-avatar.ts +0 -142
- package/dist/components/rel-badge/rel-badge.ts +0 -175
- package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +0 -106
- package/dist/components/rel-button/rel-button.ts +0 -560
- package/dist/components/rel-calendar/index.ts +0 -1
- package/dist/components/rel-calendar/rel-calendar.ts +0 -565
- package/dist/components/rel-card/rel-card.ts +0 -88
- package/dist/components/rel-cardmedia/rel-cardmedia.ts +0 -88
- package/dist/components/rel-carousel/index.ts +0 -1
- package/dist/components/rel-carousel/rel-carousel.ts +0 -723
- package/dist/components/rel-chart/README.md +0 -286
- package/dist/components/rel-chart/index.ts +0 -1
- package/dist/components/rel-chart/rel-chart.ts +0 -754
- package/dist/components/rel-checkbox/rel-checkbox.ts +0 -382
- package/dist/components/rel-chip/rel-chip.ts +0 -483
- package/dist/components/rel-container/rel-container.ts +0 -95
- package/dist/components/rel-date-picker/date-format.test.ts +0 -43
- package/dist/components/rel-date-picker/date-format.ts +0 -47
- package/dist/components/rel-date-picker/date-parse.test.ts +0 -116
- package/dist/components/rel-date-picker/date-parse.ts +0 -178
- package/dist/components/rel-date-picker/index.ts +0 -1
- package/dist/components/rel-date-picker/rel-date-picker.ts +0 -1676
- package/dist/components/rel-divider/rel-divider.ts +0 -54
- package/dist/components/rel-drawer/index.ts +0 -1
- package/dist/components/rel-drawer/rel-drawer-footer.ts +0 -27
- package/dist/components/rel-drawer/rel-drawer.ts +0 -607
- package/dist/components/rel-kbd/rel-kbd.ts +0 -188
- package/dist/components/rel-layout/rel-layout.ts +0 -49
- package/dist/components/rel-menu/rel-menu.ts +0 -541
- package/dist/components/rel-menubar/index.ts +0 -2
- package/dist/components/rel-menubar/rel-menubar-menu.css +0 -3
- package/dist/components/rel-menubar/rel-menubar-menu.ts +0 -17
- package/dist/components/rel-menubar/rel-menubar.ts +0 -777
- package/dist/components/rel-nav-menu/README.md +0 -289
- package/dist/components/rel-nav-menu/index.ts +0 -3
- package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +0 -243
- package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +0 -72
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +0 -270
- package/dist/components/rel-navbar/README.md +0 -126
- package/dist/components/rel-navbar/rel-navbar.ts +0 -105
- package/dist/components/rel-otp/index.ts +0 -1
- package/dist/components/rel-otp/rel-otp.ts +0 -527
- package/dist/components/rel-pagination/index.ts +0 -1
- package/dist/components/rel-pagination/rel-pagination.ts +0 -522
- package/dist/components/rel-popover/index.ts +0 -2
- package/dist/components/rel-popover/rel-popover-content.ts +0 -81
- package/dist/components/rel-popover/rel-popover.ts +0 -475
- package/dist/components/rel-progress/rel-progress.ts +0 -234
- package/dist/components/rel-radio/rel-radio.ts +0 -481
- package/dist/components/rel-range-picker/rel-range-picker.ts +0 -1688
- package/dist/components/rel-resizable/index.ts +0 -3
- package/dist/components/rel-resizable/rel-resizable-group.ts +0 -405
- package/dist/components/rel-resizable/rel-resizable-handle.ts +0 -150
- package/dist/components/rel-resizable/rel-resizable-panel.ts +0 -58
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +0 -217
- package/dist/components/rel-select/rel-select.ts +0 -1416
- package/dist/components/rel-sheet/rel-sheet.ts +0 -610
- package/dist/components/rel-sidebar/README.md +0 -209
- package/dist/components/rel-sidebar/rel-sidebar.ts +0 -1191
- package/dist/components/rel-skeleton/rel-skeleton.ts +0 -143
- package/dist/components/rel-slider/rel-slider.ts +0 -673
- package/dist/components/rel-snackbar/rel-snackbar.ts +0 -329
- package/dist/components/rel-stepper/rel-stepper.ts +0 -567
- package/dist/components/rel-switch/rel-switch.ts +0 -199
- package/dist/components/rel-tab/rel-tab.ts +0 -264
- package/dist/components/rel-table/README.md +0 -178
- package/dist/components/rel-table/rel-table-body.ts +0 -22
- package/dist/components/rel-table/rel-table-caption.ts +0 -30
- package/dist/components/rel-table/rel-table-cell.ts +0 -43
- package/dist/components/rel-table/rel-table-footer.ts +0 -24
- package/dist/components/rel-table/rel-table-head.ts +0 -47
- package/dist/components/rel-table/rel-table-header.ts +0 -22
- package/dist/components/rel-table/rel-table-row.ts +0 -37
- package/dist/components/rel-table/rel-table.ts +0 -62
- package/dist/components/rel-textfield/rel-textfield.ts +0 -1054
- package/dist/components/rel-time-picker/rel-time-picker.ts +0 -1482
- package/dist/components/rel-time-picker/time-parse.test.ts +0 -67
- package/dist/components/rel-time-picker/time-parse.ts +0 -85
- package/dist/components/rel-toast/rel-toast.ts +0 -206
- package/dist/components/rel-tooltip/rel-tooltip.ts +0 -193
- package/dist/components/rel-topbar/rel-topbar-actions.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar-brand.ts +0 -54
- package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +0 -69
- package/dist/components/rel-topbar/rel-topbar-dropdown.ts +0 -31
- package/dist/components/rel-topbar/rel-topbar-item.ts +0 -324
- package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar-profile.ts +0 -243
- package/dist/components/rel-topbar/rel-topbar-utilities.ts +0 -27
- package/dist/components/rel-topbar/rel-topbar.ts +0 -446
- package/dist/components/rel-upload/UPLOAD-DESIGN-CHANGES.md +0 -134
- package/dist/components/rel-upload/rel-upload-actions.ts +0 -163
- package/dist/components/rel-upload/rel-upload.ts +0 -968
|
@@ -1,1191 +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-label="Sidebar"
|
|
674
|
-
aria-hidden="${isOffcanvas && !this.isOpen}"
|
|
675
|
-
tabindex="-1"
|
|
676
|
-
>
|
|
677
|
-
<slot></slot>
|
|
678
|
-
</nav>
|
|
679
|
-
</div>
|
|
680
|
-
`;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// ============================================================================
|
|
685
|
-
// rel-sidebar-trigger (Toggle Button)
|
|
686
|
-
// ============================================================================
|
|
687
|
-
@customElement("rel-sidebar-trigger")
|
|
688
|
-
export class RelSidebarTrigger extends LitElement {
|
|
689
|
-
@state() private isOpen = true;
|
|
690
|
-
@state() private isCollapsed = false;
|
|
691
|
-
|
|
692
|
-
private provider: RelSidebarProvider | null = null;
|
|
693
|
-
|
|
694
|
-
static styles = css`
|
|
695
|
-
:host {
|
|
696
|
-
display: inline-block;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
button {
|
|
700
|
-
background: none;
|
|
701
|
-
border: none;
|
|
702
|
-
padding: var(--rel-spacing-xs, 8px);
|
|
703
|
-
margin: 0;
|
|
704
|
-
cursor: pointer;
|
|
705
|
-
display: flex;
|
|
706
|
-
align-items: center;
|
|
707
|
-
justify-content: center;
|
|
708
|
-
border-radius: var(--rel-border-radius-sm, 8px);
|
|
709
|
-
color: var(--rel-text-label);
|
|
710
|
-
transition: background 0.15s ease;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
button:hover {
|
|
714
|
-
background: var(--rel-bg-neutral-elevated-1);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
button:active {
|
|
718
|
-
background: var(--rel-border-default);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
button:focus-visible {
|
|
722
|
-
outline: 2px solid var(--rel-border-info);
|
|
723
|
-
outline-offset: 2px;
|
|
724
|
-
}
|
|
725
|
-
`;
|
|
726
|
-
|
|
727
|
-
connectedCallback() {
|
|
728
|
-
super.connectedCallback();
|
|
729
|
-
this.findProvider();
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
disconnectedCallback() {
|
|
733
|
-
super.disconnectedCallback();
|
|
734
|
-
if (this.provider) {
|
|
735
|
-
this.provider.removeEventListener(
|
|
736
|
-
"rel-sidebar-state",
|
|
737
|
-
this.handleStateChange,
|
|
738
|
-
);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
private findProvider() {
|
|
743
|
-
let parent = this.parentElement;
|
|
744
|
-
while (parent) {
|
|
745
|
-
if (parent instanceof RelSidebarProvider) {
|
|
746
|
-
this.provider = parent;
|
|
747
|
-
break;
|
|
748
|
-
}
|
|
749
|
-
parent = parent.parentElement;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
if (this.provider) {
|
|
753
|
-
this.provider.addEventListener(
|
|
754
|
-
"rel-sidebar-state",
|
|
755
|
-
this.handleStateChange,
|
|
756
|
-
);
|
|
757
|
-
this.syncState();
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
private syncState() {
|
|
762
|
-
if (!this.provider) return;
|
|
763
|
-
this.isOpen = this.provider.activeOpen;
|
|
764
|
-
this.isCollapsed = this.provider.activeCollapsed;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
private handleStateChange = () => {
|
|
768
|
-
this.syncState();
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
private handleClick = () => {
|
|
772
|
-
if (!this.provider) return;
|
|
773
|
-
|
|
774
|
-
if (this.provider.isMobile || this.provider.collapsible === "offcanvas") {
|
|
775
|
-
this.provider.setOpen(!this.isOpen, "trigger");
|
|
776
|
-
} else if (this.provider.collapsible === "icon") {
|
|
777
|
-
this.provider.setCollapsed(!this.isCollapsed, "rail");
|
|
778
|
-
}
|
|
779
|
-
};
|
|
780
|
-
|
|
781
|
-
render() {
|
|
782
|
-
const p = this.provider;
|
|
783
|
-
const expanded =
|
|
784
|
-
p && !p.isMobile && p.collapsible === "icon"
|
|
785
|
-
? !this.isCollapsed
|
|
786
|
-
: this.isOpen;
|
|
787
|
-
return html`
|
|
788
|
-
<button
|
|
789
|
-
@click="${this.handleClick}"
|
|
790
|
-
type="button"
|
|
791
|
-
aria-expanded="${expanded}"
|
|
792
|
-
aria-label="Toggle sidebar"
|
|
793
|
-
>
|
|
794
|
-
<slot>
|
|
795
|
-
<svg
|
|
796
|
-
width="20"
|
|
797
|
-
height="20"
|
|
798
|
-
viewBox="0 0 24 24"
|
|
799
|
-
fill="none"
|
|
800
|
-
stroke="currentColor"
|
|
801
|
-
stroke-width="2"
|
|
802
|
-
>
|
|
803
|
-
<line x1="3" y1="12" x2="21" y2="12"></line>
|
|
804
|
-
<line x1="3" y1="6" x2="21" y2="6"></line>
|
|
805
|
-
<line x1="3" y1="18" x2="21" y2="18"></line>
|
|
806
|
-
</svg>
|
|
807
|
-
</slot>
|
|
808
|
-
</button>
|
|
809
|
-
`;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// ============================================================================
|
|
814
|
-
// Layout Helpers
|
|
815
|
-
// ============================================================================
|
|
816
|
-
|
|
817
|
-
@customElement("rel-sidebar-header")
|
|
818
|
-
export class RelSidebarHeader extends LitElement {
|
|
819
|
-
static styles = css`
|
|
820
|
-
:host {
|
|
821
|
-
display: block;
|
|
822
|
-
padding: var(--rel-spacing-md, 16px);
|
|
823
|
-
border-bottom: 1px solid var(--rel-border-default);
|
|
824
|
-
flex-shrink: 0;
|
|
825
|
-
color: var(--rel-text-label);
|
|
826
|
-
}
|
|
827
|
-
`;
|
|
828
|
-
|
|
829
|
-
render() {
|
|
830
|
-
return html`<slot></slot>`;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
@customElement("rel-sidebar-content")
|
|
835
|
-
export class RelSidebarContent extends LitElement {
|
|
836
|
-
static styles = css`
|
|
837
|
-
:host {
|
|
838
|
-
display: block;
|
|
839
|
-
flex: 1;
|
|
840
|
-
overflow-y: auto;
|
|
841
|
-
padding: var(--rel-spacing-sm, 12px) var(--rel-spacing-xs, 8px);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
/* Scrollbar styling */
|
|
845
|
-
:host::-webkit-scrollbar {
|
|
846
|
-
width: 4px;
|
|
847
|
-
}
|
|
848
|
-
:host::-webkit-scrollbar-track {
|
|
849
|
-
background: transparent;
|
|
850
|
-
}
|
|
851
|
-
:host::-webkit-scrollbar-thumb {
|
|
852
|
-
background: var(--rel-border-default);
|
|
853
|
-
border-radius: 2px;
|
|
854
|
-
}
|
|
855
|
-
`;
|
|
856
|
-
|
|
857
|
-
render() {
|
|
858
|
-
return html`<slot></slot>`;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
@customElement("rel-sidebar-footer")
|
|
863
|
-
export class RelSidebarFooter extends LitElement {
|
|
864
|
-
static styles = css`
|
|
865
|
-
:host {
|
|
866
|
-
display: block;
|
|
867
|
-
padding: var(--rel-spacing-md, 16px);
|
|
868
|
-
border-top: 1px solid var(--rel-border-default);
|
|
869
|
-
flex-shrink: 0;
|
|
870
|
-
color: var(--rel-text-default);
|
|
871
|
-
}
|
|
872
|
-
`;
|
|
873
|
-
|
|
874
|
-
render() {
|
|
875
|
-
return html`<slot></slot>`;
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
@customElement("rel-sidebar-group")
|
|
880
|
-
export class RelSidebarGroup extends LitElement {
|
|
881
|
-
static styles = css`
|
|
882
|
-
:host {
|
|
883
|
-
display: block;
|
|
884
|
-
margin-bottom: var(--rel-spacing-md, 16px);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
:host(:last-child) {
|
|
888
|
-
margin-bottom: 0;
|
|
889
|
-
}
|
|
890
|
-
`;
|
|
891
|
-
|
|
892
|
-
render() {
|
|
893
|
-
return html`<slot></slot>`;
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
@customElement("rel-sidebar-group-label")
|
|
898
|
-
export class RelSidebarGroupLabel extends LitElement {
|
|
899
|
-
static styles = css`
|
|
900
|
-
:host {
|
|
901
|
-
display: block;
|
|
902
|
-
color: var(--rel-text-placeholder);
|
|
903
|
-
text-transform: uppercase;
|
|
904
|
-
padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-sm, 12px) var(--rel-spacing-2xs, 4px);
|
|
905
|
-
margin-bottom: var(--rel-spacing-2xs, 4px);
|
|
906
|
-
font-family: var(--rel-body-xs-semibold-font-family);
|
|
907
|
-
font-weight: var(--rel-body-xs-semibold-font-weight);
|
|
908
|
-
font-size: var(--rel-body-xs-semibold-font-size);
|
|
909
|
-
line-height: var(--rel-body-xs-semibold-line-height);
|
|
910
|
-
letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
:host(.collapsed) {
|
|
914
|
-
text-align: center;
|
|
915
|
-
padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-2xs, 4px) var(--rel-spacing-2xs, 4px);
|
|
916
|
-
font-family: var(--rel-body-xs-regular-font-family);
|
|
917
|
-
font-weight: var(--rel-body-xs-regular-font-weight);
|
|
918
|
-
font-size: var(--rel-body-xs-regular-font-size);
|
|
919
|
-
line-height: var(--rel-body-xs-regular-line-height);
|
|
920
|
-
letter-spacing: var(--rel-body-xs-regular-letter-spacing);
|
|
921
|
-
}
|
|
922
|
-
`;
|
|
923
|
-
|
|
924
|
-
render() {
|
|
925
|
-
return html`<slot></slot>`;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
@customElement("rel-sidebar-group-content")
|
|
930
|
-
export class RelSidebarGroupContent extends LitElement {
|
|
931
|
-
static styles = css`
|
|
932
|
-
:host {
|
|
933
|
-
display: block;
|
|
934
|
-
}
|
|
935
|
-
`;
|
|
936
|
-
|
|
937
|
-
render() {
|
|
938
|
-
return html`<slot></slot>`;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// ============================================================================
|
|
943
|
-
// Menu Primitives
|
|
944
|
-
// ============================================================================
|
|
945
|
-
|
|
946
|
-
@customElement("rel-sidebar-menu")
|
|
947
|
-
export class RelSidebarMenu extends LitElement {
|
|
948
|
-
static styles = css`
|
|
949
|
-
:host {
|
|
950
|
-
display: block;
|
|
951
|
-
list-style: none;
|
|
952
|
-
margin: 0;
|
|
953
|
-
padding: 0;
|
|
954
|
-
}
|
|
955
|
-
`;
|
|
956
|
-
|
|
957
|
-
connectedCallback() {
|
|
958
|
-
super.connectedCallback();
|
|
959
|
-
this.addEventListener("keydown", this.handleKeyDown);
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
disconnectedCallback() {
|
|
963
|
-
super.disconnectedCallback();
|
|
964
|
-
this.removeEventListener("keydown", this.handleKeyDown);
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
private handleKeyDown = (e: KeyboardEvent) => {
|
|
968
|
-
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
|
|
969
|
-
|
|
970
|
-
e.preventDefault();
|
|
971
|
-
const items = Array.from(
|
|
972
|
-
this.querySelectorAll("rel-sidebar-menu-button:not([disabled])"),
|
|
973
|
-
) as HTMLElement[];
|
|
974
|
-
if (items.length === 0) return;
|
|
975
|
-
|
|
976
|
-
const active = items.findIndex(
|
|
977
|
-
(item) => item.shadowRoot?.activeElement || item === document.activeElement,
|
|
978
|
-
);
|
|
979
|
-
let next: number;
|
|
980
|
-
if (e.key === "ArrowDown") {
|
|
981
|
-
next = active < items.length - 1 ? active + 1 : 0;
|
|
982
|
-
} else {
|
|
983
|
-
next = active > 0 ? active - 1 : items.length - 1;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
const btn = items[next]?.shadowRoot?.querySelector("button") as HTMLElement | null;
|
|
987
|
-
btn?.focus();
|
|
988
|
-
};
|
|
989
|
-
|
|
990
|
-
render() {
|
|
991
|
-
return html`<slot></slot>`;
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
@customElement("rel-sidebar-menu-item")
|
|
996
|
-
export class RelSidebarMenuItem extends LitElement {
|
|
997
|
-
static styles = css`
|
|
998
|
-
:host {
|
|
999
|
-
display: block;
|
|
1000
|
-
margin-bottom: var(--rel-spacing-2xs, 4px);
|
|
1001
|
-
padding: 0 var(--rel-spacing-xs, 8px);
|
|
1002
|
-
}
|
|
1003
|
-
`;
|
|
1004
|
-
|
|
1005
|
-
render() {
|
|
1006
|
-
return html`<slot></slot>`;
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
@customElement("rel-sidebar-menu-button")
|
|
1011
|
-
export class RelSidebarMenuButton extends LitElement {
|
|
1012
|
-
@property({ type: Boolean }) active = false;
|
|
1013
|
-
@property({ type: Boolean }) disabled = false;
|
|
1014
|
-
@property({ type: String }) tooltip = "";
|
|
1015
|
-
@property({ type: String }) icon: string = "";
|
|
1016
|
-
|
|
1017
|
-
@state() private isCollapsed = false;
|
|
1018
|
-
|
|
1019
|
-
private provider: RelSidebarProvider | null = null;
|
|
1020
|
-
|
|
1021
|
-
static styles = css`
|
|
1022
|
-
:host {
|
|
1023
|
-
display: block;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
button {
|
|
1027
|
-
width: 100%;
|
|
1028
|
-
display: flex;
|
|
1029
|
-
align-items: center;
|
|
1030
|
-
gap: var(--rel-spacing-sm, 12px);
|
|
1031
|
-
padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-sm, 12px);
|
|
1032
|
-
min-height: 40px;
|
|
1033
|
-
background: none;
|
|
1034
|
-
border: none;
|
|
1035
|
-
border-radius: var(--rel-border-radius-sm, 8px);
|
|
1036
|
-
cursor: pointer;
|
|
1037
|
-
color: var(--rel-text-default);
|
|
1038
|
-
transition: background 0.15s ease, color 0.15s ease;
|
|
1039
|
-
text-align: left;
|
|
1040
|
-
position: relative;
|
|
1041
|
-
font-family: var(--rel-body-md-medium-font-family);
|
|
1042
|
-
font-weight: var(--rel-body-md-medium-font-weight);
|
|
1043
|
-
font-size: var(--rel-body-md-medium-font-size);
|
|
1044
|
-
line-height: var(--rel-body-md-medium-line-height);
|
|
1045
|
-
letter-spacing: var(--rel-body-md-medium-letter-spacing);
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
button:hover:not([disabled]) {
|
|
1049
|
-
background: var(--rel-bg-neutral-elevated-1);
|
|
1050
|
-
color: var(--rel-text-label);
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
button:active:not([disabled]) {
|
|
1054
|
-
background: var(--rel-border-default);
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
button:focus-visible {
|
|
1058
|
-
outline: 2px solid var(--rel-border-info);
|
|
1059
|
-
outline-offset: -2px;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
button[disabled] {
|
|
1063
|
-
opacity: 0.5;
|
|
1064
|
-
cursor: not-allowed;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
button.active {
|
|
1068
|
-
background: var(--rel-bg-neutral-elevated-1);
|
|
1069
|
-
color: var(--rel-text-label);
|
|
1070
|
-
font-weight: var(--rel-fonts-weight-body-semibold, 600);
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
button.active::before {
|
|
1074
|
-
content: "";
|
|
1075
|
-
position: absolute;
|
|
1076
|
-
left: 0;
|
|
1077
|
-
top: 50%;
|
|
1078
|
-
transform: translateY(-50%);
|
|
1079
|
-
width: 3px;
|
|
1080
|
-
height: 20px;
|
|
1081
|
-
background: var(--rel-bg-info);
|
|
1082
|
-
border-radius: 0 2px 2px 0;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
button.collapsed {
|
|
1086
|
-
justify-content: center;
|
|
1087
|
-
padding: var(--rel-spacing-xs, 8px);
|
|
1088
|
-
min-height: 40px;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
button.collapsed.active::before {
|
|
1092
|
-
display: none;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
button.collapsed.active {
|
|
1096
|
-
background: var(--rel-bg-neutral-elevated-1);
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
.icon {
|
|
1100
|
-
flex-shrink: 0;
|
|
1101
|
-
display: flex;
|
|
1102
|
-
align-items: center;
|
|
1103
|
-
justify-content: center;
|
|
1104
|
-
width: 18px;
|
|
1105
|
-
height: 18px;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
.label {
|
|
1109
|
-
flex: 1;
|
|
1110
|
-
white-space: nowrap;
|
|
1111
|
-
overflow: hidden;
|
|
1112
|
-
text-overflow: ellipsis;
|
|
1113
|
-
line-height: 1.4;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
button.collapsed .label {
|
|
1117
|
-
display: none;
|
|
1118
|
-
}
|
|
1119
|
-
`;
|
|
1120
|
-
|
|
1121
|
-
connectedCallback() {
|
|
1122
|
-
super.connectedCallback();
|
|
1123
|
-
this.findProvider();
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
disconnectedCallback() {
|
|
1127
|
-
super.disconnectedCallback();
|
|
1128
|
-
if (this.provider) {
|
|
1129
|
-
this.provider.removeEventListener(
|
|
1130
|
-
"rel-sidebar-state",
|
|
1131
|
-
this.handleStateChange,
|
|
1132
|
-
);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
private findProvider() {
|
|
1137
|
-
let parent = this.parentElement;
|
|
1138
|
-
while (parent) {
|
|
1139
|
-
if (parent instanceof RelSidebarProvider) {
|
|
1140
|
-
this.provider = parent;
|
|
1141
|
-
break;
|
|
1142
|
-
}
|
|
1143
|
-
parent = parent.parentElement;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
if (this.provider) {
|
|
1147
|
-
this.provider.addEventListener(
|
|
1148
|
-
"rel-sidebar-state",
|
|
1149
|
-
this.handleStateChange,
|
|
1150
|
-
);
|
|
1151
|
-
this.syncState();
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
private syncState() {
|
|
1156
|
-
if (!this.provider) return;
|
|
1157
|
-
this.isCollapsed = this.provider.activeCollapsed && !this.provider.isMobile;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
private handleStateChange = () => {
|
|
1161
|
-
this.syncState();
|
|
1162
|
-
};
|
|
1163
|
-
|
|
1164
|
-
render() {
|
|
1165
|
-
const buttonClass = [
|
|
1166
|
-
this.active ? "active" : "",
|
|
1167
|
-
this.isCollapsed ? "collapsed" : "",
|
|
1168
|
-
]
|
|
1169
|
-
.filter(Boolean)
|
|
1170
|
-
.join(" ");
|
|
1171
|
-
|
|
1172
|
-
const tooltipTitle = this.isCollapsed && this.tooltip ? this.tooltip : "";
|
|
1173
|
-
|
|
1174
|
-
return html`
|
|
1175
|
-
<button
|
|
1176
|
-
class="${buttonClass}"
|
|
1177
|
-
?disabled="${this.disabled}"
|
|
1178
|
-
title="${tooltipTitle}"
|
|
1179
|
-
aria-current="${this.active ? "page" : "false"}"
|
|
1180
|
-
part="button"
|
|
1181
|
-
>
|
|
1182
|
-
<span class="icon" part="icon">
|
|
1183
|
-
${this.icon ? html`<rel-icon name="${this.icon}"></rel-icon>` : nothing}
|
|
1184
|
-
</span>
|
|
1185
|
-
<span class="label" part="label">
|
|
1186
|
-
<slot></slot>
|
|
1187
|
-
</span>
|
|
1188
|
-
</button>
|
|
1189
|
-
`;
|
|
1190
|
-
}
|
|
1191
|
-
}
|