@reliability-design/web 1.4.4 → 1.4.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.
Files changed (58) hide show
  1. package/dist/components/rel-accordion/rel-accordion.ts +13 -1
  2. package/dist/components/rel-alert/rel-alert.ts +1 -1
  3. package/dist/components/rel-autocomplete/rel-autocomplete.ts +33 -2
  4. package/dist/components/rel-badge/rel-badge.ts +6 -1
  5. package/dist/components/rel-button/rel-button.ts +18 -2
  6. package/dist/components/rel-calendar/rel-calendar.ts +86 -18
  7. package/dist/components/rel-carousel/rel-carousel.ts +45 -2
  8. package/dist/components/rel-checkbox/rel-checkbox.ts +4 -1
  9. package/dist/components/rel-chip/rel-chip.ts +59 -19
  10. package/dist/components/rel-date-picker/date-parse.test.ts +116 -0
  11. package/dist/components/rel-date-picker/date-parse.ts +178 -0
  12. package/dist/components/rel-date-picker/rel-date-picker.ts +675 -93
  13. package/dist/components/rel-drawer/rel-drawer.ts +28 -61
  14. package/dist/components/rel-menu/rel-menu.ts +130 -15
  15. package/dist/components/rel-menubar/rel-menubar.ts +221 -63
  16. package/dist/components/rel-nav-menu/rel-nav-menu.ts +39 -3
  17. package/dist/components/rel-otp/rel-otp.ts +14 -4
  18. package/dist/components/rel-pagination/rel-pagination.ts +13 -0
  19. package/dist/components/rel-popover/rel-popover.ts +64 -5
  20. package/dist/components/rel-progress/rel-progress.ts +13 -4
  21. package/dist/components/rel-radio/README.md +6 -3
  22. package/dist/components/rel-radio/rel-radio.ts +236 -60
  23. package/dist/components/rel-range-picker/rel-range-picker.ts +581 -91
  24. package/dist/components/rel-resizable/rel-resizable-group.ts +20 -0
  25. package/dist/components/rel-segmented-button/rel-segmented-button.ts +55 -13
  26. package/dist/components/rel-select/rel-select.ts +203 -24
  27. package/dist/components/rel-sheet/rel-sheet.ts +184 -18
  28. package/dist/components/rel-sidebar/rel-sidebar.ts +50 -40
  29. package/dist/components/rel-skeleton/rel-skeleton.ts +9 -0
  30. package/dist/components/rel-slider/rel-slider.ts +6 -3
  31. package/dist/components/rel-snackbar/rel-snackbar.ts +20 -16
  32. package/dist/components/rel-stepper/rel-stepper.ts +47 -2
  33. package/dist/components/rel-switch/rel-switch.ts +3 -1
  34. package/dist/components/rel-tab/rel-tab.ts +32 -7
  35. package/dist/components/rel-textfield/rel-textfield.ts +263 -75
  36. package/dist/components/rel-time-picker/rel-time-picker.ts +749 -261
  37. package/dist/components/rel-time-picker/time-parse.test.ts +67 -0
  38. package/dist/components/rel-time-picker/time-parse.ts +85 -0
  39. package/dist/components/rel-toast/rel-toast.ts +4 -2
  40. package/dist/components/rel-tooltip/README.md +13 -5
  41. package/dist/components/rel-tooltip/rel-tooltip.ts +33 -0
  42. package/dist/components/rel-topbar/rel-topbar-actions.ts +1 -1
  43. package/dist/components/rel-topbar/rel-topbar-brand.ts +1 -1
  44. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +1 -1
  45. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +1 -1
  46. package/dist/components/rel-topbar/rel-topbar-item.ts +318 -319
  47. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +1 -1
  48. package/dist/components/rel-topbar/rel-topbar-profile.ts +224 -224
  49. package/dist/components/rel-topbar/rel-topbar-utilities.ts +1 -1
  50. package/dist/components/rel-topbar/rel-topbar.ts +440 -440
  51. package/dist/components/rel-upload/rel-upload.ts +25 -0
  52. package/dist/index.d.mts +461 -23
  53. package/dist/index.d.ts +461 -23
  54. package/dist/index.js +3906 -807
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +4143 -1055
  57. package/dist/index.mjs.map +1 -1
  58. package/package.json +2 -2
@@ -1,446 +1,446 @@
1
- import { LitElement, html, css } from "lit";
2
- import { customElement, property, state } from "lit/decorators.js";
3
- import "@reliability-design/icons/rel-icon";
4
- import "./rel-topbar-brand";
5
- import "./rel-topbar-actions";
6
- import "./rel-topbar-utilities";
7
- import "./rel-topbar-mobile-panel";
8
- import "./rel-topbar-dropdown";
9
- import "./rel-topbar-dropdown-item";
10
-
11
- export type TopbarVariant = "customer" | "carrier";
12
- export type TopbarForceLayout = "auto" | "desktop" | "tablet" | "mobile";
13
-
14
- /**
15
- * `rel-topbar` — Application-level top navigation / header shell.
16
- *
17
- * @deprecated This component's fixed slot structure, hardcoded breakpoint,
18
- * and built-in overflow logic are too rigid for most real layouts. Use the
19
- * "App Topbar" recipe at /docs/recipes/app-topbar instead, composed from
20
- * rel-menu/rel-popover, rel-drawer, and rel-button. Still supported, but
21
- * will be removed in a future major version.
22
- *
23
- * Slots:
24
- * brand — Logo / wordmark region (left)
25
- * (default) — Primary nav items (rel-topbar-item elements)
26
- * actions — CTA / action buttons (customer) or dark-pill area (carrier)
27
- * utilities — Utility icon buttons (notifications, search, etc.)
28
- * profile — rel-topbar-profile tile
29
- * mobile-panel — Collapsed nav panel shown when mobileOpen is true
30
- *
31
- * Events:
32
- * rel-menu-toggle — fired when hamburger is clicked { open: boolean }
33
- * rel-mobile-open-change — alias of rel-menu-toggle for convenience
34
- * rel-nav-select — bubbled from child rel-topbar-item { value, label }
35
- */
36
- @customElement("rel-topbar")
37
- export class RelTopbar extends LitElement {
38
- /** Visual variant — affects action/profile region styling */
39
- @property({ type: String, reflect: true }) variant: TopbarVariant = "customer";
40
-
41
- /** Whether the mobile nav panel is currently open */
42
- @property({ type: Boolean, reflect: true }) mobileOpen = false;
43
-
44
- /** Whether to render the hamburger / close toggle button */
45
- @property({ type: Boolean, reflect: true }) showMenuToggle = false;
46
-
47
- /** Reduce height to compact (48 px) mode */
48
- @property({ type: Boolean, reflect: true }) compact = false;
49
-
50
- /** Stick topbar to top of viewport while scrolling */
51
- @property({ type: Boolean, reflect: true }) sticky = false;
52
-
53
- /**
54
- * Override the responsive layout for demos / storybook.
55
- * 'auto' respects CSS media queries.
56
- */
57
- @property({ type: String }) forceLayout: TopbarForceLayout = "auto";
58
-
59
- private _overflowItems: Element[] = [];
60
- @state() private _overflowCount: number = 0;
61
- @state() private _overflowItemsData: Array<{ label: string; href: string; disabled: boolean }> = [];
62
- private _resizeObserver?: ResizeObserver;
63
- private _resizeTimer?: number;
64
- private _measuring = false;
65
- private _lastNavWidth = 0;
66
-
67
- static styles = css`
68
- /* ─── Component tokens ─────────────────────────────────────────── */
69
- :host {
70
- color: var(--rel-text-default);
71
- --rel-topbar-height: 64px;
72
- --rel-topbar-height-compact: 48px;
73
- --rel-topbar-bg: var(--rel-bg-surface);
74
- --rel-topbar-border-color: var(--rel-border-default);
75
- --rel-topbar-item-color: var(--rel-text-default);
76
- --rel-topbar-item-active-color: var(--rel-text-success);
77
- --rel-topbar-item-active-indicator: var(--rel-bg-brand);
78
- --rel-topbar-item-hover-bg: var(--rel-bg-surface-elevated);
79
- --rel-topbar-divider-color: var(--rel-border-default);
80
- --rel-topbar-carrier-action-bg: var(--rel-text-default);
81
- --rel-topbar-carrier-action-color: var(--rel-bg-surface);
82
- --rel-topbar-z: 100;
83
-
84
- display: block;
85
- width: 100%;
86
- font-family: var(--rel-fonts-font-family, Poppins, sans-serif);
87
- }
88
-
89
- /* ─── Sticky host ───────────────────────────────────────────────── */
90
- :host([sticky]) {
91
- position: sticky;
92
- top: 0;
93
- z-index: var(--rel-topbar-z);
94
- }
95
-
96
- /* ─── Bar shell ─────────────────────────────────────────────────── */
97
- .topbar {
98
- display: flex;
99
- align-items: center;
100
- height: var(--rel-topbar-height);
101
- background: var(--rel-topbar-bg);
102
- border-bottom: var(--rel-stroke-sm, 1px) solid var(--rel-topbar-border-color);
103
- padding: 0 var(--rel-spacing-xl, 24px);
104
- box-sizing: border-box;
105
- gap: var(--rel-spacing-md, 16px);
106
- }
107
-
108
- :host([compact]) .topbar {
109
- height: var(--rel-topbar-height-compact);
110
- }
111
-
112
- /* ─── Brand ─────────────────────────────────────────────────────── */
113
- .brand-region {
114
- display: flex;
115
- align-items: center;
116
- flex-shrink: 0;
117
- }
118
-
119
- /* ─── Nav (primary, desktop) ────────────────────────────────────── */
120
- .nav-region {
121
- display: flex;
122
- align-items: center;
123
- flex: 1 1 0;
124
- gap: var(--rel-spacing-2xs, 4px);
125
- min-width: 0;
126
- }
127
-
128
- /* ─── Right side cluster ────────────────────────────────────────── */
129
- .right-region {
130
- display: flex;
131
- align-items: center;
132
- flex-shrink: 0;
133
- margin-left: auto;
134
- gap: var(--rel-spacing-xs, 8px);
135
- }
136
-
137
- .actions-region,
138
- .utilities-region,
139
- .profile-region {
140
- display: contents;
141
- }
142
-
143
- /* Vertical divider between util/profile when both are populated */
144
- .divider {
145
- width: var(--rel-stroke-sm, 1px);
146
- height: 24px;
147
- background: var(--rel-topbar-divider-color);
148
- flex-shrink: 0;
149
- }
150
-
151
- /* ─── Carrier action pill wrapper ───────────────────────────────── */
152
- :host([variant="carrier"]) .actions-region ::slotted(*) {
153
- display: flex;
154
- align-items: center;
155
- gap: var(--rel-spacing-xs, 8px);
156
- background: var(--rel-topbar-carrier-action-bg);
157
- color: var(--rel-topbar-carrier-action-color);
158
- padding: var(--rel-spacing-2xs, 4px) var(--rel-spacing-sm, 12px);
159
- border-radius: var(--rel-border-radius-full-rounded, 1000px);
160
- }
161
-
162
- /* ─── Mobile toggle button ──────────────────────────────────────── */
163
- .menu-toggle {
164
- display: none;
165
- align-items: center;
166
- justify-content: center;
167
- width: 40px;
168
- height: 40px;
169
- padding: 0;
170
- border: none;
171
- background: transparent;
172
- border-radius: var(--rel-border-radius-sm, 8px);
173
- cursor: pointer;
174
- color: var(--rel-topbar-item-color);
175
- flex-shrink: 0;
176
- }
177
-
178
- .menu-toggle:focus-visible {
179
- outline: 2px solid var(--rel-topbar-item-active-indicator);
180
- outline-offset: 2px;
181
- }
182
-
183
- .menu-toggle:hover {
184
- background: var(--rel-topbar-item-hover-bg);
185
- }
186
-
187
- .menu-toggle svg {
188
- display: block;
189
- }
190
-
191
- /* ─── Mobile panel ──────────────────────────────────────────────── */
192
- .mobile-panel {
193
- display: none;
194
- background: var(--rel-topbar-bg);
195
- border-bottom: var(--rel-stroke-sm, 1px) solid var(--rel-topbar-border-color);
196
- }
197
-
198
- .mobile-panel.open {
199
- display: block;
200
- }
201
-
202
- .mobile-overflow-nav {
203
- display: flex;
204
- flex-direction: column;
205
- padding: var(--rel-spacing-xs, 8px) 0;
206
- border-top: 1px solid var(--rel-topbar-border-color);
207
- }
208
-
209
- .mobile-overflow-item {
210
- display: block;
211
- padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-xl, 24px);
212
- color: var(--rel-topbar-item-color);
1
+ import { LitElement, html, css } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import "@reliability-design/icons/rel-icon";
4
+ import "./rel-topbar-brand";
5
+ import "./rel-topbar-actions";
6
+ import "./rel-topbar-utilities";
7
+ import "./rel-topbar-mobile-panel";
8
+ import "./rel-topbar-dropdown";
9
+ import "./rel-topbar-dropdown-item";
10
+
11
+ export type TopbarVariant = "customer" | "carrier";
12
+ export type TopbarForceLayout = "auto" | "desktop" | "tablet" | "mobile";
13
+
14
+ /**
15
+ * `rel-topbar` — Application-level top navigation / header shell.
16
+ *
17
+ * @deprecated This component's fixed slot structure, hardcoded breakpoint,
18
+ * and built-in overflow logic are too rigid for most real layouts. Use the
19
+ * "Navbar" recipe at /docs/recipes/navbar instead, composed from
20
+ * rel-menu/rel-popover, rel-drawer, and rel-button. Still supported, but
21
+ * will be removed in a future major version.
22
+ *
23
+ * Slots:
24
+ * brand — Logo / wordmark region (left)
25
+ * (default) — Primary nav items (rel-topbar-item elements)
26
+ * actions — CTA / action buttons (customer) or dark-pill area (carrier)
27
+ * utilities — Utility icon buttons (notifications, search, etc.)
28
+ * profile — rel-topbar-profile tile
29
+ * mobile-panel — Collapsed nav panel shown when mobileOpen is true
30
+ *
31
+ * Events:
32
+ * rel-menu-toggle — fired when hamburger is clicked { open: boolean }
33
+ * rel-mobile-open-change — alias of rel-menu-toggle for convenience
34
+ * rel-nav-select — bubbled from child rel-topbar-item { value, label }
35
+ */
36
+ @customElement("rel-topbar")
37
+ export class RelTopbar extends LitElement {
38
+ /** Visual variant — affects action/profile region styling */
39
+ @property({ type: String, reflect: true }) variant: TopbarVariant = "customer";
40
+
41
+ /** Whether the mobile nav panel is currently open */
42
+ @property({ type: Boolean, reflect: true }) mobileOpen = false;
43
+
44
+ /** Whether to render the hamburger / close toggle button */
45
+ @property({ type: Boolean, reflect: true }) showMenuToggle = false;
46
+
47
+ /** Reduce height to compact (48 px) mode */
48
+ @property({ type: Boolean, reflect: true }) compact = false;
49
+
50
+ /** Stick topbar to top of viewport while scrolling */
51
+ @property({ type: Boolean, reflect: true }) sticky = false;
52
+
53
+ /**
54
+ * Override the responsive layout for demos / storybook.
55
+ * 'auto' respects CSS media queries.
56
+ */
57
+ @property({ type: String }) forceLayout: TopbarForceLayout = "auto";
58
+
59
+ private _overflowItems: Element[] = [];
60
+ @state() private _overflowCount: number = 0;
61
+ @state() private _overflowItemsData: Array<{ label: string; href: string; disabled: boolean }> = [];
62
+ private _resizeObserver?: ResizeObserver;
63
+ private _resizeTimer?: number;
64
+ private _measuring = false;
65
+ private _lastNavWidth = 0;
66
+
67
+ static styles = css`
68
+ /* ─── Component tokens ─────────────────────────────────────────── */
69
+ :host {
70
+ color: var(--rel-text-default);
71
+ --rel-topbar-height: 64px;
72
+ --rel-topbar-height-compact: 48px;
73
+ --rel-topbar-bg: var(--rel-bg-surface);
74
+ --rel-topbar-border-color: var(--rel-border-default);
75
+ --rel-topbar-item-color: var(--rel-text-default);
76
+ --rel-topbar-item-active-color: var(--rel-text-success);
77
+ --rel-topbar-item-active-indicator: var(--rel-bg-brand);
78
+ --rel-topbar-item-hover-bg: var(--rel-bg-surface-elevated);
79
+ --rel-topbar-divider-color: var(--rel-border-default);
80
+ --rel-topbar-carrier-action-bg: var(--rel-text-default);
81
+ --rel-topbar-carrier-action-color: var(--rel-bg-surface);
82
+ --rel-topbar-z: 100;
83
+
84
+ display: block;
85
+ width: 100%;
86
+ font-family: var(--rel-fonts-font-family, Poppins, sans-serif);
87
+ }
88
+
89
+ /* ─── Sticky host ───────────────────────────────────────────────── */
90
+ :host([sticky]) {
91
+ position: sticky;
92
+ top: 0;
93
+ z-index: var(--rel-topbar-z);
94
+ }
95
+
96
+ /* ─── Bar shell ─────────────────────────────────────────────────── */
97
+ .topbar {
98
+ display: flex;
99
+ align-items: center;
100
+ height: var(--rel-topbar-height);
101
+ background: var(--rel-topbar-bg);
102
+ border-bottom: var(--rel-stroke-sm, 1px) solid var(--rel-topbar-border-color);
103
+ padding: 0 var(--rel-spacing-xl, 24px);
104
+ box-sizing: border-box;
105
+ gap: var(--rel-spacing-md, 16px);
106
+ }
107
+
108
+ :host([compact]) .topbar {
109
+ height: var(--rel-topbar-height-compact);
110
+ }
111
+
112
+ /* ─── Brand ─────────────────────────────────────────────────────── */
113
+ .brand-region {
114
+ display: flex;
115
+ align-items: center;
116
+ flex-shrink: 0;
117
+ }
118
+
119
+ /* ─── Nav (primary, desktop) ────────────────────────────────────── */
120
+ .nav-region {
121
+ display: flex;
122
+ align-items: center;
123
+ flex: 1 1 0;
124
+ gap: var(--rel-spacing-2xs, 4px);
125
+ min-width: 0;
126
+ }
127
+
128
+ /* ─── Right side cluster ────────────────────────────────────────── */
129
+ .right-region {
130
+ display: flex;
131
+ align-items: center;
132
+ flex-shrink: 0;
133
+ margin-left: auto;
134
+ gap: var(--rel-spacing-xs, 8px);
135
+ }
136
+
137
+ .actions-region,
138
+ .utilities-region,
139
+ .profile-region {
140
+ display: contents;
141
+ }
142
+
143
+ /* Vertical divider between util/profile when both are populated */
144
+ .divider {
145
+ width: var(--rel-stroke-sm, 1px);
146
+ height: 24px;
147
+ background: var(--rel-topbar-divider-color);
148
+ flex-shrink: 0;
149
+ }
150
+
151
+ /* ─── Carrier action pill wrapper ───────────────────────────────── */
152
+ :host([variant="carrier"]) .actions-region ::slotted(*) {
153
+ display: flex;
154
+ align-items: center;
155
+ gap: var(--rel-spacing-xs, 8px);
156
+ background: var(--rel-topbar-carrier-action-bg);
157
+ color: var(--rel-topbar-carrier-action-color);
158
+ padding: var(--rel-spacing-2xs, 4px) var(--rel-spacing-sm, 12px);
159
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
160
+ }
161
+
162
+ /* ─── Mobile toggle button ──────────────────────────────────────── */
163
+ .menu-toggle {
164
+ display: none;
165
+ align-items: center;
166
+ justify-content: center;
167
+ width: 40px;
168
+ height: 40px;
169
+ padding: 0;
170
+ border: none;
171
+ background: transparent;
172
+ border-radius: var(--rel-border-radius-sm, 8px);
173
+ cursor: pointer;
174
+ color: var(--rel-topbar-item-color);
175
+ flex-shrink: 0;
176
+ }
177
+
178
+ .menu-toggle:focus-visible {
179
+ outline: 2px solid var(--rel-topbar-item-active-indicator);
180
+ outline-offset: 2px;
181
+ }
182
+
183
+ .menu-toggle:hover {
184
+ background: var(--rel-topbar-item-hover-bg);
185
+ }
186
+
187
+ .menu-toggle svg {
188
+ display: block;
189
+ }
190
+
191
+ /* ─── Mobile panel ──────────────────────────────────────────────── */
192
+ .mobile-panel {
193
+ display: none;
194
+ background: var(--rel-topbar-bg);
195
+ border-bottom: var(--rel-stroke-sm, 1px) solid var(--rel-topbar-border-color);
196
+ }
197
+
198
+ .mobile-panel.open {
199
+ display: block;
200
+ }
201
+
202
+ .mobile-overflow-nav {
203
+ display: flex;
204
+ flex-direction: column;
205
+ padding: var(--rel-spacing-xs, 8px) 0;
206
+ border-top: 1px solid var(--rel-topbar-border-color);
207
+ }
208
+
209
+ .mobile-overflow-item {
210
+ display: block;
211
+ padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-xl, 24px);
212
+ color: var(--rel-topbar-item-color);
213
213
  text-decoration: none;
214
214
  font-family: var(--rel-body-md-medium-font-family);
215
215
  font-weight: var(--rel-body-md-medium-font-weight);
216
216
  font-size: var(--rel-body-md-medium-font-size);
217
217
  line-height: var(--rel-body-md-medium-line-height);
218
218
  letter-spacing: var(--rel-body-md-medium-letter-spacing);
219
- }
220
-
221
- .mobile-overflow-item:hover {
222
- background: var(--rel-topbar-item-hover-bg);
223
- }
224
-
225
- .mobile-overflow-item.disabled {
226
- opacity: 0.45;
227
- pointer-events: none;
228
- }
229
-
230
- /* ─── Responsive: auto mode (CSS media queries) ─────────────────── */
231
- @media (max-width: 768px) {
232
- .topbar:not(.force-desktop) .nav-region {
233
- display: none;
234
- }
235
-
236
- .topbar:not(.force-desktop) .menu-toggle {
237
- display: flex;
238
- }
239
- }
240
-
241
- @media (min-width: 769px) {
242
- .topbar:not(.force-mobile):not(.force-tablet) .menu-toggle {
243
- display: none;
244
- }
245
- }
246
-
247
- /* ─── forceLayout overrides ─────────────────────────────────────── */
248
- .topbar.force-mobile .nav-region,
249
- .topbar.force-tablet .nav-region {
250
- display: none;
251
- }
252
-
253
- .topbar.force-mobile .menu-toggle,
254
- .topbar.force-tablet .menu-toggle {
255
- display: flex;
256
- }
257
-
258
- .topbar.force-desktop .nav-region {
259
- display: flex;
260
- }
261
-
262
- .topbar.force-desktop .menu-toggle {
263
- display: none;
264
- }
265
- `;
266
-
267
- override firstUpdated() {
268
- const navRegion = this.renderRoot.querySelector(".nav-region") as HTMLElement;
269
- if (navRegion) {
270
- this._resizeObserver = new ResizeObserver(() => {
271
- clearTimeout(this._resizeTimer);
272
- this._resizeTimer = window.setTimeout(() => this._measureNav(), 150);
273
- });
274
- this._resizeObserver.observe(navRegion);
275
- }
276
- requestAnimationFrame(() => this._measureNav());
277
- }
278
-
279
- override disconnectedCallback() {
280
- super.disconnectedCallback();
281
- this._resizeObserver?.disconnect();
282
- clearTimeout(this._resizeTimer);
283
- }
284
-
285
- private _measureNav() {
286
- if (this._measuring) return;
287
- this._measuring = true;
288
- const navRegion = this.renderRoot.querySelector(".nav-region") as HTMLElement;
289
- if (!navRegion) return;
290
- const navSlot = this.renderRoot.querySelector('slot[name="nav"]') as HTMLSlotElement;
291
- if (!navSlot) return;
292
- const items = Array.from(navSlot.assignedElements()).filter(
293
- el => el.tagName.toLowerCase() !== "rel-topbar-item" || !el.hasAttribute("data-more")
294
- );
295
- const navWidth = navRegion.getBoundingClientRect().width;
296
- if (Math.abs(navWidth - this._lastNavWidth) < 2) return;
297
- this._lastNavWidth = navWidth;
298
- const available = navWidth - 120;
299
-
300
- let accumulated = 0;
301
- let firstOverflowIndex = items.length;
302
- for (let i = 0; i < items.length; i++) {
303
- accumulated += items[i].getBoundingClientRect().width;
304
- if (accumulated > available) {
305
- firstOverflowIndex = i;
306
- break;
307
- }
308
- }
309
-
310
- const overflowingElements = Array.from(items).slice(firstOverflowIndex);
311
- this._overflowItems = overflowingElements;
312
- this._overflowCount = overflowingElements.length;
313
- this._overflowItemsData = overflowingElements.map(el => ({
314
- label: el.getAttribute("label") ?? "",
315
- href: el.getAttribute("href") ?? "",
316
- disabled: el.hasAttribute("disabled"),
317
- }));
318
-
319
- for (const el of items) {
320
- if (overflowingElements.includes(el)) {
321
- el.setAttribute("hidden", "");
322
- } else {
323
- el.removeAttribute("hidden");
324
- }
325
- }
326
-
327
- this._measuring = false;
328
- }
329
-
330
- private onRouterSlotChange(e: Event) {
331
- const slot = e.target as HTMLSlotElement;
332
- for (const el of slot.assignedElements({ flatten: true })) {
333
- const tag = (el as HTMLElement).tagName.toLowerCase();
334
- const targetSlot =
335
- tag === "rel-topbar-brand" ? "brand" :
336
- tag === "rel-topbar-actions" ? "actions" :
337
- tag === "rel-topbar-utilities" ? "utilities" :
338
- tag === "rel-topbar-profile" ? "profile" :
339
- tag === "rel-topbar-mobile-panel" ? "mobile-panel" :
340
- "nav";
341
- if ((el as HTMLElement).slot !== targetSlot) {
342
- (el as HTMLElement).slot = targetSlot;
343
- }
344
- }
345
- }
346
-
347
- private get layoutClass(): string {
348
- if (this.forceLayout === "auto") return "";
349
- return `force-${this.forceLayout}`;
350
- }
351
-
352
- private handleMenuToggle() {
353
- this.mobileOpen = !this.mobileOpen;
354
- const detail = { open: this.mobileOpen };
355
- this.dispatchEvent(
356
- new CustomEvent("rel-menu-toggle", { detail, bubbles: true, composed: true })
357
- );
358
- this.dispatchEvent(
359
- new CustomEvent("rel-mobile-open-change", { detail, bubbles: true, composed: true })
360
- );
361
- }
362
-
363
- render() {
364
- const topbarClass = ["topbar", this.layoutClass].filter(Boolean).join(" ");
365
- const menuLabel = this.mobileOpen ? "Close menu" : "Open menu";
366
-
367
- return html`
368
- <!-- router: auto-assigns slot names by tagName; stays empty after redistribution -->
369
- <slot style="display:none" @slotchange=${this.onRouterSlotChange}></slot>
370
-
371
- <header>
372
- <div class=${topbarClass} role="banner">
373
- <div class="brand-region">
374
- <slot name="brand"></slot>
375
- </div>
376
-
377
- <nav class="nav-region" aria-label="Primary">
378
- <slot name="nav" @slotchange=${this._measureNav}></slot>
379
- ${this._overflowCount > 0 ? html`
380
- <rel-topbar-item label="More" expandable data-more>
381
- <rel-topbar-dropdown slot="dropdown">
382
- ${this._overflowItemsData.map(item => html`
383
- <rel-topbar-dropdown-item
384
- label=${item.label}
385
- href=${item.href}
386
- ?disabled=${item.disabled}
387
- ></rel-topbar-dropdown-item>
388
- `)}
389
- </rel-topbar-dropdown>
390
- </rel-topbar-item>
391
- ` : null}
392
- </nav>
393
-
394
- <div class="right-region">
395
- <div class="actions-region">
396
- <slot name="actions"></slot>
397
- </div>
398
- <div class="utilities-region">
399
- <slot name="utilities"></slot>
400
- </div>
401
- <div class="profile-region">
402
- <slot name="profile"></slot>
403
- </div>
404
-
405
- ${this.showMenuToggle
406
- ? html`
407
- <button
408
- class="menu-toggle"
409
- aria-label=${menuLabel}
410
- aria-expanded=${this.mobileOpen}
411
- aria-controls="mobile-panel"
412
- @click=${this.handleMenuToggle}
413
- >
414
- ${this.mobileOpen
415
- ? html`<rel-icon name="close" size="md"></rel-icon>`
416
- : html`<rel-icon name="hamburger" size="md"></rel-icon>`}
417
- </button>
418
- `
419
- : null}
420
- </div>
421
- </div>
422
- </header>
423
-
424
- <div
425
- class="mobile-panel ${this.mobileOpen ? "open" : ""}"
426
- id="mobile-panel"
427
- role="region"
428
- aria-label="Mobile navigation"
429
- >
430
- <slot name="mobile-panel"></slot>
431
- ${this._overflowItemsData.length > 0 ? html`
432
- <div class="mobile-overflow-nav">
433
- ${this._overflowItemsData.map(item => html`
434
- <a
435
- class="mobile-overflow-item ${item.disabled ? "disabled" : ""}"
436
- href=${item.href}
437
- ?aria-disabled=${item.disabled}
438
- >${item.label}</a>
439
- `)}
440
- </div>
441
- ` : null}
442
- </div>
443
- `;
444
- }
445
- }
446
-
219
+ }
220
+
221
+ .mobile-overflow-item:hover {
222
+ background: var(--rel-topbar-item-hover-bg);
223
+ }
224
+
225
+ .mobile-overflow-item.disabled {
226
+ opacity: 0.45;
227
+ pointer-events: none;
228
+ }
229
+
230
+ /* ─── Responsive: auto mode (CSS media queries) ─────────────────── */
231
+ @media (max-width: 768px) {
232
+ .topbar:not(.force-desktop) .nav-region {
233
+ display: none;
234
+ }
235
+
236
+ .topbar:not(.force-desktop) .menu-toggle {
237
+ display: flex;
238
+ }
239
+ }
240
+
241
+ @media (min-width: 769px) {
242
+ .topbar:not(.force-mobile):not(.force-tablet) .menu-toggle {
243
+ display: none;
244
+ }
245
+ }
246
+
247
+ /* ─── forceLayout overrides ─────────────────────────────────────── */
248
+ .topbar.force-mobile .nav-region,
249
+ .topbar.force-tablet .nav-region {
250
+ display: none;
251
+ }
252
+
253
+ .topbar.force-mobile .menu-toggle,
254
+ .topbar.force-tablet .menu-toggle {
255
+ display: flex;
256
+ }
257
+
258
+ .topbar.force-desktop .nav-region {
259
+ display: flex;
260
+ }
261
+
262
+ .topbar.force-desktop .menu-toggle {
263
+ display: none;
264
+ }
265
+ `;
266
+
267
+ override firstUpdated() {
268
+ const navRegion = this.renderRoot.querySelector(".nav-region") as HTMLElement;
269
+ if (navRegion) {
270
+ this._resizeObserver = new ResizeObserver(() => {
271
+ clearTimeout(this._resizeTimer);
272
+ this._resizeTimer = window.setTimeout(() => this._measureNav(), 150);
273
+ });
274
+ this._resizeObserver.observe(navRegion);
275
+ }
276
+ requestAnimationFrame(() => this._measureNav());
277
+ }
278
+
279
+ override disconnectedCallback() {
280
+ super.disconnectedCallback();
281
+ this._resizeObserver?.disconnect();
282
+ clearTimeout(this._resizeTimer);
283
+ }
284
+
285
+ private _measureNav() {
286
+ if (this._measuring) return;
287
+ this._measuring = true;
288
+ const navRegion = this.renderRoot.querySelector(".nav-region") as HTMLElement;
289
+ if (!navRegion) return;
290
+ const navSlot = this.renderRoot.querySelector('slot[name="nav"]') as HTMLSlotElement;
291
+ if (!navSlot) return;
292
+ const items = Array.from(navSlot.assignedElements()).filter(
293
+ el => el.tagName.toLowerCase() !== "rel-topbar-item" || !el.hasAttribute("data-more")
294
+ );
295
+ const navWidth = navRegion.getBoundingClientRect().width;
296
+ if (Math.abs(navWidth - this._lastNavWidth) < 2) return;
297
+ this._lastNavWidth = navWidth;
298
+ const available = navWidth - 120;
299
+
300
+ let accumulated = 0;
301
+ let firstOverflowIndex = items.length;
302
+ for (let i = 0; i < items.length; i++) {
303
+ accumulated += items[i].getBoundingClientRect().width;
304
+ if (accumulated > available) {
305
+ firstOverflowIndex = i;
306
+ break;
307
+ }
308
+ }
309
+
310
+ const overflowingElements = Array.from(items).slice(firstOverflowIndex);
311
+ this._overflowItems = overflowingElements;
312
+ this._overflowCount = overflowingElements.length;
313
+ this._overflowItemsData = overflowingElements.map(el => ({
314
+ label: el.getAttribute("label") ?? "",
315
+ href: el.getAttribute("href") ?? "",
316
+ disabled: el.hasAttribute("disabled"),
317
+ }));
318
+
319
+ for (const el of items) {
320
+ if (overflowingElements.includes(el)) {
321
+ el.setAttribute("hidden", "");
322
+ } else {
323
+ el.removeAttribute("hidden");
324
+ }
325
+ }
326
+
327
+ this._measuring = false;
328
+ }
329
+
330
+ private onRouterSlotChange(e: Event) {
331
+ const slot = e.target as HTMLSlotElement;
332
+ for (const el of slot.assignedElements({ flatten: true })) {
333
+ const tag = (el as HTMLElement).tagName.toLowerCase();
334
+ const targetSlot =
335
+ tag === "rel-topbar-brand" ? "brand" :
336
+ tag === "rel-topbar-actions" ? "actions" :
337
+ tag === "rel-topbar-utilities" ? "utilities" :
338
+ tag === "rel-topbar-profile" ? "profile" :
339
+ tag === "rel-topbar-mobile-panel" ? "mobile-panel" :
340
+ "nav";
341
+ if ((el as HTMLElement).slot !== targetSlot) {
342
+ (el as HTMLElement).slot = targetSlot;
343
+ }
344
+ }
345
+ }
346
+
347
+ private get layoutClass(): string {
348
+ if (this.forceLayout === "auto") return "";
349
+ return `force-${this.forceLayout}`;
350
+ }
351
+
352
+ private handleMenuToggle() {
353
+ this.mobileOpen = !this.mobileOpen;
354
+ const detail = { open: this.mobileOpen };
355
+ this.dispatchEvent(
356
+ new CustomEvent("rel-menu-toggle", { detail, bubbles: true, composed: true })
357
+ );
358
+ this.dispatchEvent(
359
+ new CustomEvent("rel-mobile-open-change", { detail, bubbles: true, composed: true })
360
+ );
361
+ }
362
+
363
+ render() {
364
+ const topbarClass = ["topbar", this.layoutClass].filter(Boolean).join(" ");
365
+ const menuLabel = this.mobileOpen ? "Close menu" : "Open menu";
366
+
367
+ return html`
368
+ <!-- router: auto-assigns slot names by tagName; stays empty after redistribution -->
369
+ <slot style="display:none" @slotchange=${this.onRouterSlotChange}></slot>
370
+
371
+ <header>
372
+ <div class=${topbarClass} role="banner">
373
+ <div class="brand-region">
374
+ <slot name="brand"></slot>
375
+ </div>
376
+
377
+ <nav class="nav-region" aria-label="Primary">
378
+ <slot name="nav" @slotchange=${this._measureNav}></slot>
379
+ ${this._overflowCount > 0 ? html`
380
+ <rel-topbar-item label="More" expandable data-more>
381
+ <rel-topbar-dropdown slot="dropdown">
382
+ ${this._overflowItemsData.map(item => html`
383
+ <rel-topbar-dropdown-item
384
+ label=${item.label}
385
+ href=${item.href}
386
+ ?disabled=${item.disabled}
387
+ ></rel-topbar-dropdown-item>
388
+ `)}
389
+ </rel-topbar-dropdown>
390
+ </rel-topbar-item>
391
+ ` : null}
392
+ </nav>
393
+
394
+ <div class="right-region">
395
+ <div class="actions-region">
396
+ <slot name="actions"></slot>
397
+ </div>
398
+ <div class="utilities-region">
399
+ <slot name="utilities"></slot>
400
+ </div>
401
+ <div class="profile-region">
402
+ <slot name="profile"></slot>
403
+ </div>
404
+
405
+ ${this.showMenuToggle
406
+ ? html`
407
+ <button
408
+ class="menu-toggle"
409
+ aria-label=${menuLabel}
410
+ aria-expanded=${this.mobileOpen}
411
+ aria-controls="mobile-panel"
412
+ @click=${this.handleMenuToggle}
413
+ >
414
+ ${this.mobileOpen
415
+ ? html`<rel-icon name="close" size="md"></rel-icon>`
416
+ : html`<rel-icon name="hamburger" size="md"></rel-icon>`}
417
+ </button>
418
+ `
419
+ : null}
420
+ </div>
421
+ </div>
422
+ </header>
423
+
424
+ <div
425
+ class="mobile-panel ${this.mobileOpen ? "open" : ""}"
426
+ id="mobile-panel"
427
+ role="region"
428
+ aria-label="Mobile navigation"
429
+ >
430
+ <slot name="mobile-panel"></slot>
431
+ ${this._overflowItemsData.length > 0 ? html`
432
+ <div class="mobile-overflow-nav">
433
+ ${this._overflowItemsData.map(item => html`
434
+ <a
435
+ class="mobile-overflow-item ${item.disabled ? "disabled" : ""}"
436
+ href=${item.href}
437
+ ?aria-disabled=${item.disabled}
438
+ >${item.label}</a>
439
+ `)}
440
+ </div>
441
+ ` : null}
442
+ </div>
443
+ `;
444
+ }
445
+ }
446
+