@reliability-design/web 1.0.3 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/rel-accordion/README.md +132 -0
- package/dist/components/rel-accordion/rel-accordion.ts +43 -16
- package/dist/components/rel-alert/README.md +141 -0
- package/dist/components/rel-alert/rel-alert.ts +18 -7
- package/dist/components/rel-avatar/README.md +119 -0
- package/dist/components/rel-badge/README.md +86 -0
- package/dist/components/rel-badge/rel-badge.ts +6 -6
- package/dist/components/rel-breadcrumb/README.md +102 -0
- package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
- package/dist/components/rel-button/README.md +130 -0
- package/dist/components/rel-calendar/README.md +109 -0
- package/dist/components/rel-calendar/index.ts +1 -0
- package/dist/components/rel-calendar/rel-calendar.ts +467 -0
- package/dist/components/rel-card/README.md +94 -0
- package/dist/components/rel-card/rel-card.ts +12 -3
- package/dist/components/rel-cardmedia/README.md +127 -0
- package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
- package/dist/components/rel-carousel/README.md +166 -0
- package/dist/components/rel-carousel/index.ts +1 -0
- package/dist/components/rel-carousel/rel-carousel.ts +469 -0
- package/dist/components/rel-chart/README.md +286 -0
- package/dist/components/rel-chart/index.ts +1 -0
- package/dist/components/rel-chart/rel-chart.ts +736 -0
- package/dist/components/rel-checkbox/README.md +77 -0
- package/dist/components/rel-checkbox/rel-checkbox.ts +18 -10
- package/dist/components/rel-chip/README.md +114 -0
- package/dist/components/rel-chip/rel-chip.ts +20 -4
- package/dist/components/rel-container/README.md +99 -0
- package/dist/components/rel-date-picker/README.md +253 -0
- package/dist/components/rel-date-picker/index.ts +1 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
- package/dist/components/rel-divider/README.md +100 -0
- package/dist/components/rel-drawer/README.md +200 -0
- package/dist/components/rel-drawer/index.ts +1 -0
- package/dist/components/rel-drawer/rel-drawer.ts +621 -0
- package/dist/components/rel-kbd/README.md +151 -0
- package/dist/components/rel-kbd/rel-kbd.ts +118 -0
- package/dist/components/rel-layout/README.md +88 -0
- package/dist/components/rel-menu/README.md +151 -0
- package/dist/components/rel-menu/rel-menu.ts +18 -10
- package/dist/components/rel-menubar/README.md +329 -0
- package/dist/components/rel-menubar/index.ts +2 -0
- package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
- package/dist/components/rel-menubar/rel-menubar.ts +576 -0
- package/dist/components/rel-nav-menu/README.md +289 -0
- package/dist/components/rel-nav-menu/index.ts +3 -0
- package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +143 -0
- package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +331 -0
- package/dist/components/rel-navbar/README.md +126 -0
- package/dist/components/rel-navbar/rel-navbar.ts +91 -0
- package/dist/components/rel-otp/README.md +440 -0
- package/dist/components/rel-otp/index.ts +1 -0
- package/dist/components/rel-otp/rel-otp.ts +504 -0
- package/dist/components/rel-pagination/README.md +410 -0
- package/dist/components/rel-pagination/index.ts +1 -0
- package/dist/components/rel-pagination/rel-pagination.ts +376 -0
- package/dist/components/rel-popover/README.md +568 -0
- package/dist/components/rel-popover/index.ts +2 -0
- package/dist/components/rel-popover/rel-popover-content.ts +55 -0
- package/dist/components/rel-popover/rel-popover.ts +327 -0
- package/dist/components/rel-progress/README.md +110 -0
- package/dist/components/rel-progress/rel-progress.ts +32 -5
- package/dist/components/rel-quote-page/README.md +166 -0
- package/dist/components/rel-quote-page/rel-quote-page.ts +816 -0
- package/dist/components/rel-radio/README.md +120 -0
- package/dist/components/rel-radio/rel-radio.ts +13 -5
- package/dist/components/rel-resizable/README.md +140 -0
- package/dist/components/rel-resizable/index.ts +3 -0
- package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
- package/dist/components/rel-resizable/rel-resizable-handle.ts +149 -0
- package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
- package/dist/components/rel-segmented-button/README.md +141 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
- package/dist/components/rel-select/README.md +107 -0
- package/dist/components/rel-select/rel-select.ts +14 -14
- package/dist/components/rel-sheet/README.md +224 -0
- package/dist/components/rel-sheet/rel-sheet.ts +683 -0
- package/dist/components/rel-sidebar/README.md +209 -0
- package/dist/components/rel-sidebar/rel-sidebar.ts +1158 -0
- package/dist/components/rel-slider/README.md +135 -0
- package/dist/components/rel-slider/rel-slider.ts +19 -7
- package/dist/components/rel-snackbar/README.md +134 -0
- package/dist/components/rel-snackbar/rel-snackbar.ts +67 -2
- package/dist/components/rel-stepper/README.md +160 -0
- package/dist/components/rel-stepper/rel-stepper.ts +6 -6
- package/dist/components/rel-switch/README.md +102 -0
- package/dist/components/rel-switch/rel-switch.ts +15 -6
- package/dist/components/rel-tab/README.md +163 -0
- package/dist/components/rel-tab/rel-tab.ts +36 -14
- package/dist/components/rel-table/README.md +178 -0
- package/dist/components/rel-table/rel-table-body.ts +16 -0
- package/dist/components/rel-table/rel-table-caption.ts +22 -0
- package/dist/components/rel-table/rel-table-cell.ts +49 -0
- package/dist/components/rel-table/rel-table-footer.ts +18 -0
- package/dist/components/rel-table/rel-table-head.ts +33 -0
- package/dist/components/rel-table/rel-table-header.ts +16 -0
- package/dist/components/rel-table/rel-table-row.ts +32 -0
- package/dist/components/rel-table/rel-table.ts +47 -0
- package/dist/components/rel-textfield/README.md +90 -0
- package/dist/components/rel-textfield/rel-textfield.ts +35 -19
- package/dist/components/rel-toast/README.md +148 -0
- package/dist/components/rel-toast/rel-toast.ts +32 -14
- package/dist/components/rel-tooltip/README.md +103 -0
- package/dist/components/rel-tooltip/rel-tooltip.ts +9 -9
- package/dist/index.d.mts +836 -1
- package/dist/index.d.ts +836 -1
- package/dist/index.js +10658 -1493
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10616 -1499
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +293 -183
- package/dist/{vars-6ZIUPIU4.css → vars-ITL2K3KO.css} +293 -183
- package/package.json +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import { customElement, property, state, query } from "lit/decorators.js";
|
|
3
|
+
import { RelNavMenuItem } from "./rel-nav-menu-item";
|
|
4
|
+
|
|
5
|
+
type DensityType = "comfortable" | "compact";
|
|
6
|
+
|
|
7
|
+
@customElement("rel-nav-menu")
|
|
8
|
+
export class RelNavMenu extends LitElement {
|
|
9
|
+
@property({ type: String, reflect: true }) open = "";
|
|
10
|
+
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
11
|
+
@property({ type: String }) density: DensityType = "comfortable";
|
|
12
|
+
|
|
13
|
+
@state() private items: RelNavMenuItem[] = [];
|
|
14
|
+
@state() private focusedIndex = 0;
|
|
15
|
+
@state() private hoverSwitchEnabled = false;
|
|
16
|
+
|
|
17
|
+
@query(".nav-container") private container!: HTMLElement;
|
|
18
|
+
|
|
19
|
+
static styles = css`
|
|
20
|
+
:host {
|
|
21
|
+
display: block;
|
|
22
|
+
font-family: var(--rel-fontFamilies-primary);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.nav-container {
|
|
26
|
+
display: flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
gap: var(--rel-spacing-space-3xs);
|
|
29
|
+
padding: var(--rel-spacing-space-2xs);
|
|
30
|
+
background: var(--rel-colors-gray-light);
|
|
31
|
+
border-radius: var(--rel-borders-radius-border-radius-200);
|
|
32
|
+
border: var(--rel-borders-width-stroke-100) solid var(--rel-colors-gray-border);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.nav-container.compact {
|
|
36
|
+
padding: var(--rel-spacing-space-3xs);
|
|
37
|
+
gap: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.dropdown-panel {
|
|
41
|
+
position: fixed;
|
|
42
|
+
min-width: 320px;
|
|
43
|
+
max-width: 600px;
|
|
44
|
+
background: var(--rel-colors-white);
|
|
45
|
+
border: var(--rel-borders-width-stroke-100) solid var(--rel-colors-gray-border);
|
|
46
|
+
border-radius: var(--rel-borders-radius-border-radius-200);
|
|
47
|
+
box-shadow: var(--rel-shadows-shadow-300);
|
|
48
|
+
padding: var(--rel-spacing-space-xs);
|
|
49
|
+
z-index: 1000;
|
|
50
|
+
display: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dropdown-panel.open {
|
|
54
|
+
display: block;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
:host([disabled]) .nav-container {
|
|
58
|
+
opacity: 0.5;
|
|
59
|
+
pointer-events: none;
|
|
60
|
+
}
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
connectedCallback() {
|
|
64
|
+
super.connectedCallback();
|
|
65
|
+
this.discoverItems();
|
|
66
|
+
document.addEventListener("click", this.handleOutsideClick);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
disconnectedCallback() {
|
|
70
|
+
super.disconnectedCallback();
|
|
71
|
+
document.removeEventListener("click", this.handleOutsideClick);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private discoverItems() {
|
|
75
|
+
const slot = this.shadowRoot?.querySelector("slot");
|
|
76
|
+
if (slot) {
|
|
77
|
+
const nodes = slot.assignedElements() as RelNavMenuItem[];
|
|
78
|
+
this.items = nodes.filter((el) => el.tagName === "REL-NAV-MENU-ITEM");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private handleSlotChange() {
|
|
83
|
+
this.discoverItems();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private handleOutsideClick = (e: MouseEvent) => {
|
|
87
|
+
const path = e.composedPath() as HTMLElement[];
|
|
88
|
+
const isInside = path.some(
|
|
89
|
+
(el) =>
|
|
90
|
+
el === this ||
|
|
91
|
+
el.classList?.contains("dropdown-panel") ||
|
|
92
|
+
el.tagName === "REL-NAV-MENU-ITEM"
|
|
93
|
+
);
|
|
94
|
+
if (!isInside && this.open) {
|
|
95
|
+
this.closePanel();
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
private openPanel(itemId: string) {
|
|
100
|
+
if (this.disabled) return;
|
|
101
|
+
|
|
102
|
+
const wasOpen = this.open === itemId;
|
|
103
|
+
this.open = wasOpen ? "" : itemId;
|
|
104
|
+
|
|
105
|
+
if (!wasOpen) {
|
|
106
|
+
this.hoverSwitchEnabled = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.dispatchEvent(
|
|
110
|
+
new CustomEvent("rel-nav-open-change", {
|
|
111
|
+
detail: { openId: this.open },
|
|
112
|
+
bubbles: true,
|
|
113
|
+
composed: true,
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (this.open) {
|
|
118
|
+
this.updateComplete.then(() => this.positionPanel(itemId));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private closePanel() {
|
|
123
|
+
if (this.open) {
|
|
124
|
+
this.open = "";
|
|
125
|
+
this.hoverSwitchEnabled = false;
|
|
126
|
+
this.dispatchEvent(
|
|
127
|
+
new CustomEvent("rel-nav-open-change", {
|
|
128
|
+
detail: { openId: "" },
|
|
129
|
+
bubbles: true,
|
|
130
|
+
composed: true,
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private positionPanel(itemId: string) {
|
|
137
|
+
const item = this.items.find((i) => i.itemId === itemId);
|
|
138
|
+
if (!item) return;
|
|
139
|
+
|
|
140
|
+
const trigger = item.shadowRoot?.querySelector(".trigger") as HTMLElement;
|
|
141
|
+
const panel = this.shadowRoot?.querySelector(
|
|
142
|
+
`.dropdown-panel[data-item-id="${itemId}"]`
|
|
143
|
+
) as HTMLElement;
|
|
144
|
+
|
|
145
|
+
if (!trigger || !panel) return;
|
|
146
|
+
|
|
147
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
148
|
+
const panelRect = panel.getBoundingClientRect();
|
|
149
|
+
const viewportWidth = window.innerWidth;
|
|
150
|
+
const viewportHeight = window.innerHeight;
|
|
151
|
+
|
|
152
|
+
// Default: position below trigger, start-aligned
|
|
153
|
+
let top = triggerRect.bottom + 8;
|
|
154
|
+
let left = triggerRect.left;
|
|
155
|
+
|
|
156
|
+
// Handle right overflow
|
|
157
|
+
if (left + panelRect.width > viewportWidth) {
|
|
158
|
+
left = viewportWidth - panelRect.width - 16;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handle left overflow
|
|
162
|
+
if (left < 0) {
|
|
163
|
+
left = 16;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Handle bottom overflow (flip above if needed)
|
|
167
|
+
if (top + panelRect.height > viewportHeight) {
|
|
168
|
+
const spaceAbove = triggerRect.top;
|
|
169
|
+
const spaceBelow = viewportHeight - triggerRect.bottom;
|
|
170
|
+
|
|
171
|
+
if (spaceAbove > spaceBelow && spaceAbove > panelRect.height) {
|
|
172
|
+
top = triggerRect.top - panelRect.height - 8;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
panel.style.top = `${top}px`;
|
|
177
|
+
panel.style.left = `${left}px`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private handleItemClick(item: RelNavMenuItem, event: Event) {
|
|
181
|
+
if (this.disabled || item.disabled) return;
|
|
182
|
+
|
|
183
|
+
// If item has href and no content, let default behavior happen
|
|
184
|
+
if (item.href && !this.hasItemContent(item)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
|
|
190
|
+
// If item has content, toggle panel
|
|
191
|
+
if (this.hasItemContent(item)) {
|
|
192
|
+
this.openPanel(item.itemId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private handleItemMouseEnter(item: RelNavMenuItem) {
|
|
197
|
+
if (this.disabled || item.disabled) return;
|
|
198
|
+
|
|
199
|
+
// Hover switching: if a panel is open, hovering another trigger switches to it
|
|
200
|
+
if (this.open && this.hoverSwitchEnabled && this.hasItemContent(item)) {
|
|
201
|
+
this.openPanel(item.itemId);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private handleKeyDown(event: KeyboardEvent) {
|
|
206
|
+
if (this.disabled) return;
|
|
207
|
+
|
|
208
|
+
const { key } = event;
|
|
209
|
+
const focusableItems = this.items.filter((item) => !item.disabled);
|
|
210
|
+
|
|
211
|
+
if (key === "ArrowRight") {
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
this.focusedIndex = Math.min(focusableItems.length - 1, this.focusedIndex + 1);
|
|
214
|
+
this.focusItem(this.focusedIndex);
|
|
215
|
+
} else if (key === "ArrowLeft") {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
this.focusedIndex = Math.max(0, this.focusedIndex - 1);
|
|
218
|
+
this.focusItem(this.focusedIndex);
|
|
219
|
+
} else if (key === "Enter" || key === " ") {
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
const item = focusableItems[this.focusedIndex];
|
|
222
|
+
if (item && this.hasItemContent(item)) {
|
|
223
|
+
this.openPanel(item.itemId);
|
|
224
|
+
}
|
|
225
|
+
} else if (key === "Escape") {
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
if (this.open) {
|
|
228
|
+
this.closePanel();
|
|
229
|
+
const openItem = this.items.find((i) => i.itemId === this.open);
|
|
230
|
+
if (openItem) {
|
|
231
|
+
const trigger = openItem.shadowRoot?.querySelector(".trigger") as HTMLElement;
|
|
232
|
+
trigger?.focus();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} else if (key === "ArrowDown" && this.open) {
|
|
236
|
+
event.preventDefault();
|
|
237
|
+
// Move focus into the panel
|
|
238
|
+
const panel = this.shadowRoot?.querySelector(
|
|
239
|
+
`.dropdown-panel[data-item-id="${this.open}"]`
|
|
240
|
+
) as HTMLElement;
|
|
241
|
+
if (panel) {
|
|
242
|
+
const firstFocusable = panel.querySelector("a, button") as HTMLElement;
|
|
243
|
+
firstFocusable?.focus();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private focusItem(index: number) {
|
|
249
|
+
const focusableItems = this.items.filter((item) => !item.disabled);
|
|
250
|
+
const item = focusableItems[index];
|
|
251
|
+
if (item) {
|
|
252
|
+
const trigger = item.shadowRoot?.querySelector(".trigger") as HTMLElement;
|
|
253
|
+
trigger?.focus();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private hasItemContent(item: RelNavMenuItem): boolean {
|
|
258
|
+
const slot = item.shadowRoot?.querySelector("slot");
|
|
259
|
+
if (!slot) return false;
|
|
260
|
+
const nodes = slot.assignedNodes({ flatten: true });
|
|
261
|
+
return nodes.some(
|
|
262
|
+
(node) => node.nodeType === Node.ELEMENT_NODE || node.textContent?.trim()
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private renderDropdownPanel(item: RelNavMenuItem) {
|
|
267
|
+
if (!this.hasItemContent(item)) return null;
|
|
268
|
+
|
|
269
|
+
const isOpen = this.open === item.itemId;
|
|
270
|
+
|
|
271
|
+
return html`
|
|
272
|
+
<div
|
|
273
|
+
class="dropdown-panel ${isOpen ? "open" : ""}"
|
|
274
|
+
data-item-id="${item.itemId}"
|
|
275
|
+
id="${item.panelId}"
|
|
276
|
+
role="menu"
|
|
277
|
+
@click="${(e: Event) => e.stopPropagation()}"
|
|
278
|
+
>
|
|
279
|
+
${this.getItemContent(item)}
|
|
280
|
+
</div>
|
|
281
|
+
`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private getItemContent(item: RelNavMenuItem) {
|
|
285
|
+
const slot = item.shadowRoot?.querySelector("slot");
|
|
286
|
+
if (!slot) return null;
|
|
287
|
+
|
|
288
|
+
const nodes = slot.assignedNodes({ flatten: true });
|
|
289
|
+
const elements = nodes.filter((node) => node.nodeType === Node.ELEMENT_NODE) as Element[];
|
|
290
|
+
|
|
291
|
+
// Clone and return the content
|
|
292
|
+
return elements.map((el) => {
|
|
293
|
+
const clone = el.cloneNode(true);
|
|
294
|
+
return clone;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
render() {
|
|
299
|
+
return html`
|
|
300
|
+
<nav
|
|
301
|
+
class="nav-container ${this.density === "compact" ? "compact" : ""}"
|
|
302
|
+
role="navigation"
|
|
303
|
+
@keydown="${this.handleKeyDown}"
|
|
304
|
+
>
|
|
305
|
+
<slot @slotchange="${this.handleSlotChange}"></slot>
|
|
306
|
+
</nav>
|
|
307
|
+
|
|
308
|
+
${this.items.map((item) => {
|
|
309
|
+
// Set up click and hover handlers by attaching to the trigger
|
|
310
|
+
this.updateComplete.then(() => {
|
|
311
|
+
const trigger = item.shadowRoot?.querySelector(".trigger") as HTMLElement;
|
|
312
|
+
if (trigger) {
|
|
313
|
+
// Remove old listeners if any
|
|
314
|
+
trigger.onclick = (e) => this.handleItemClick(item, e);
|
|
315
|
+
trigger.onmouseenter = () => this.handleItemMouseEnter(item);
|
|
316
|
+
|
|
317
|
+
// Update active state
|
|
318
|
+
trigger.classList.toggle("active", this.open === item.itemId);
|
|
319
|
+
|
|
320
|
+
// Update ARIA attributes
|
|
321
|
+
if (this.hasItemContent(item)) {
|
|
322
|
+
trigger.setAttribute("aria-expanded", this.open === item.itemId ? "true" : "false");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
return this.renderDropdownPanel(item);
|
|
328
|
+
})}
|
|
329
|
+
`;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# rel-navbar
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
A responsive navigation bar component with sticky positioning support and device-aware styling. Features three layout areas (brand, content, actions) with automatic adjustment for tablet and mobile viewports.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
```html
|
|
8
|
+
<!-- Basic navbar -->
|
|
9
|
+
<rel-navbar>
|
|
10
|
+
<div slot="brand">
|
|
11
|
+
<img src="logo.png" alt="Brand Logo">
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<nav>
|
|
15
|
+
<a href="#home">Home</a>
|
|
16
|
+
<a href="#about">About</a>
|
|
17
|
+
<a href="#contact">Contact</a>
|
|
18
|
+
</nav>
|
|
19
|
+
|
|
20
|
+
<div slot="actions">
|
|
21
|
+
<button>Login</button>
|
|
22
|
+
</div>
|
|
23
|
+
</rel-navbar>
|
|
24
|
+
|
|
25
|
+
<!-- Sticky navbar -->
|
|
26
|
+
<rel-navbar sticky>
|
|
27
|
+
<div slot="brand">MyApp</div>
|
|
28
|
+
<nav>
|
|
29
|
+
<a href="#features">Features</a>
|
|
30
|
+
<a href="#pricing">Pricing</a>
|
|
31
|
+
</nav>
|
|
32
|
+
<div slot="actions">
|
|
33
|
+
<button>Sign Up</button>
|
|
34
|
+
</div>
|
|
35
|
+
</rel-navbar>
|
|
36
|
+
|
|
37
|
+
<!-- Device-specific layout -->
|
|
38
|
+
<rel-navbar device="mobile">
|
|
39
|
+
<div slot="brand">Logo</div>
|
|
40
|
+
<!-- Content slot hidden on mobile -->
|
|
41
|
+
<div slot="actions">
|
|
42
|
+
<button>☰</button>
|
|
43
|
+
</div>
|
|
44
|
+
</rel-navbar>
|
|
45
|
+
|
|
46
|
+
<!-- Tablet layout -->
|
|
47
|
+
<rel-navbar device="tablet">
|
|
48
|
+
<div slot="brand">Brand</div>
|
|
49
|
+
<nav>
|
|
50
|
+
<a href="#link1">Link 1</a>
|
|
51
|
+
<a href="#link2">Link 2</a>
|
|
52
|
+
</nav>
|
|
53
|
+
<div slot="actions">
|
|
54
|
+
<button>Profile</button>
|
|
55
|
+
</div>
|
|
56
|
+
</rel-navbar>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
### Attributes/Properties
|
|
62
|
+
| Name | Type | Default | Description |
|
|
63
|
+
|------|------|---------|-------------|
|
|
64
|
+
| `sticky` (reflected) | `boolean` | `false` | If true, navbar uses `position: sticky` and stays at the top when scrolling |
|
|
65
|
+
| `device` | `"desktop" \| "tablet" \| "mobile"` | `"desktop"` | Device type for responsive styling |
|
|
66
|
+
|
|
67
|
+
## Events
|
|
68
|
+
None.
|
|
69
|
+
|
|
70
|
+
## Slots
|
|
71
|
+
| Slot | Description |
|
|
72
|
+
|------|-------------|
|
|
73
|
+
| `brand` | Brand/logo content on the left side |
|
|
74
|
+
| (default) | Main navigation content (center area, hidden on mobile) |
|
|
75
|
+
| `actions` | Action buttons/items on the right side |
|
|
76
|
+
|
|
77
|
+
## CSS Parts
|
|
78
|
+
| Part | Description |
|
|
79
|
+
|------|-------------|
|
|
80
|
+
| `container` | Main navbar container |
|
|
81
|
+
| `brand` | Brand section wrapper |
|
|
82
|
+
| `content` | Center content section wrapper |
|
|
83
|
+
| `actions` | Actions section wrapper |
|
|
84
|
+
|
|
85
|
+
## Tokens / CSS Variables
|
|
86
|
+
|
|
87
|
+
### Semantic Tokens
|
|
88
|
+
- `--rel-fontFamilies-primary` - Font family
|
|
89
|
+
|
|
90
|
+
### Color Tokens
|
|
91
|
+
- `--rel-colors-white` - Background color
|
|
92
|
+
- `--rel-text-primary` - Text color
|
|
93
|
+
- `--rel-border-default` - Bottom border color
|
|
94
|
+
|
|
95
|
+
### Spacing Tokens
|
|
96
|
+
- `--rel-spacing-space-lg` - Desktop horizontal padding
|
|
97
|
+
- `--rel-spacing-space-md` - Tablet/mobile horizontal padding, brand-actions gap, content padding
|
|
98
|
+
- `--rel-spacing-space-sm` - Action items gap, mobile padding
|
|
99
|
+
|
|
100
|
+
### Custom Properties
|
|
101
|
+
- `--rel-spacing-space-xl` - Content area horizontal padding
|
|
102
|
+
|
|
103
|
+
## Accessibility
|
|
104
|
+
- Uses `role="navigation"` on the main navbar container
|
|
105
|
+
- Ensure slotted navigation links are properly structured with semantic HTML
|
|
106
|
+
- Consider adding ARIA labels for brand and actions areas if content is not descriptive
|
|
107
|
+
|
|
108
|
+
### Keyboard Interactions
|
|
109
|
+
- Standard browser navigation (Tab to navigate through links/buttons)
|
|
110
|
+
|
|
111
|
+
### Focus Behavior
|
|
112
|
+
- No specific focus handling (relies on slotted content)
|
|
113
|
+
|
|
114
|
+
## Notes / Edge Cases
|
|
115
|
+
- **Sticky behavior**: When `sticky` is true, the navbar uses `position: sticky`, `top: 0`, and `z-index: 100`
|
|
116
|
+
- **Device variants**:
|
|
117
|
+
- **desktop**: Height 64px, large padding
|
|
118
|
+
- **tablet**: Height 56px, medium padding
|
|
119
|
+
- **mobile**: Height 56px, small padding, content slot is hidden (`display: none`)
|
|
120
|
+
- The component uses flexbox with `justify-content: space-between` for layout
|
|
121
|
+
- The `.content` section has `flex: 1` to take available space between brand and actions
|
|
122
|
+
- The brand and actions areas are `flex-shrink: 0` to prevent compression
|
|
123
|
+
- The navbar has a bottom border by default
|
|
124
|
+
- The `device` property must be manually set programmatically (does not auto-detect from viewport)
|
|
125
|
+
- Consider hiding or collapsing the content slot on mobile and showing a menu icon in the actions slot
|
|
126
|
+
- The sticky attribute is reflected in HTML when set
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
|
|
4
|
+
@customElement("rel-navbar")
|
|
5
|
+
export class RelNavbar extends LitElement {
|
|
6
|
+
@property({ type: Boolean, reflect: true })
|
|
7
|
+
sticky = false;
|
|
8
|
+
|
|
9
|
+
@property({ type: String })
|
|
10
|
+
device: "desktop" | "tablet" | "mobile" = "desktop";
|
|
11
|
+
|
|
12
|
+
static styles = css`
|
|
13
|
+
:host {
|
|
14
|
+
display: block;
|
|
15
|
+
width: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:host([sticky]) .navbar {
|
|
19
|
+
position: sticky;
|
|
20
|
+
top: 0;
|
|
21
|
+
z-index: 100;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.navbar {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: space-between;
|
|
28
|
+
background: var(--rel-colors-white, #fff);
|
|
29
|
+
color: var(--rel-text-primary, #333);
|
|
30
|
+
border-bottom: 1px solid var(--rel-border-default, #ddd);
|
|
31
|
+
height: 64px;
|
|
32
|
+
padding: 0 var(--rel-spacing-space-lg);
|
|
33
|
+
box-sizing: border-box;
|
|
34
|
+
font-family: var(--rel-fontFamilies-primary, sans-serif);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.brand {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
gap: var(--rel-spacing-space-md);
|
|
41
|
+
flex-shrink: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.content {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: var(--rel-spacing-space-sm);
|
|
48
|
+
flex: 1;
|
|
49
|
+
justify-content: flex-start;
|
|
50
|
+
padding: 0 var(--rel-spacing-space-xl);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.actions {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: var(--rel-spacing-space-sm);
|
|
57
|
+
flex-shrink: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Mobile/Tablet variants */
|
|
61
|
+
:host([device="tablet"]) .navbar {
|
|
62
|
+
height: 56px;
|
|
63
|
+
padding: 0 var(--rel-spacing-space-md);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:host([device="mobile"]) .navbar {
|
|
67
|
+
height: 56px;
|
|
68
|
+
padding: 0 var(--rel-spacing-space-sm);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
:host([device="mobile"]) .content {
|
|
72
|
+
display: none;
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
render() {
|
|
77
|
+
return html`
|
|
78
|
+
<div class="navbar" part="container" role="navigation">
|
|
79
|
+
<div part="brand" class="brand">
|
|
80
|
+
<slot name="brand"></slot>
|
|
81
|
+
</div>
|
|
82
|
+
<div part="content" class="content">
|
|
83
|
+
<slot></slot>
|
|
84
|
+
</div>
|
|
85
|
+
<div part="actions" class="actions">
|
|
86
|
+
<slot name="actions"></slot>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
}
|