@reliability-design/web 1.0.6 → 1.2.0

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 (78) hide show
  1. package/dist/components/rel-accordion/rel-accordion.ts +20 -31
  2. package/dist/components/rel-alert/rel-alert.ts +81 -11
  3. package/dist/components/rel-avatar/rel-avatar.ts +6 -3
  4. package/dist/components/rel-badge/rel-badge.ts +8 -3
  5. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +6 -6
  6. package/dist/components/rel-button/rel-button.ts +118 -16
  7. package/dist/components/rel-calendar/rel-calendar.ts +16 -9
  8. package/dist/components/rel-card/rel-card.ts +9 -17
  9. package/dist/components/rel-cardmedia/rel-cardmedia.ts +20 -23
  10. package/dist/components/rel-carousel/rel-carousel.ts +596 -469
  11. package/dist/components/rel-checkbox/rel-checkbox.ts +98 -15
  12. package/dist/components/rel-chip/rel-chip.ts +291 -186
  13. package/dist/components/rel-container/rel-container.ts +2 -2
  14. package/dist/components/rel-date-picker/rel-date-picker.ts +23 -19
  15. package/dist/components/rel-divider/rel-divider.ts +12 -3
  16. package/dist/components/rel-drawer/rel-drawer-footer.ts +27 -0
  17. package/dist/components/rel-drawer/rel-drawer.ts +57 -113
  18. package/dist/components/rel-kbd/rel-kbd.ts +94 -118
  19. package/dist/components/rel-layout/rel-layout.ts +49 -58
  20. package/dist/components/rel-menu/rel-menu.ts +359 -327
  21. package/dist/components/rel-menubar/rel-menubar-menu.css +3 -0
  22. package/dist/components/rel-menubar/rel-menubar-menu.ts +2 -1
  23. package/dist/components/rel-menubar/rel-menubar.ts +602 -576
  24. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +240 -143
  25. package/dist/components/rel-nav-menu/rel-nav-menu.ts +227 -331
  26. package/dist/components/rel-navbar/rel-navbar.ts +13 -0
  27. package/dist/components/rel-otp/rel-otp.ts +22 -25
  28. package/dist/components/rel-pagination/rel-pagination.ts +112 -63
  29. package/dist/components/rel-popover/rel-popover-content.ts +6 -5
  30. package/dist/components/rel-popover/rel-popover.ts +42 -20
  31. package/dist/components/rel-progress/rel-progress.ts +10 -9
  32. package/dist/components/rel-radio/rel-radio.ts +228 -155
  33. package/dist/components/rel-range-picker/rel-range-picker.css +349 -0
  34. package/dist/components/rel-range-picker/rel-range-picker.ts +1079 -0
  35. package/dist/components/rel-resizable/rel-resizable-handle.ts +12 -15
  36. package/dist/components/rel-segmented-button/rel-segmented-button.ts +41 -20
  37. package/dist/components/rel-select/rel-select.ts +112 -27
  38. package/dist/components/rel-sheet/rel-sheet.ts +362 -683
  39. package/dist/components/rel-sidebar/rel-sidebar.ts +13 -5
  40. package/dist/components/rel-slider/rel-slider.ts +74 -29
  41. package/dist/components/rel-snackbar/rel-snackbar.ts +67 -37
  42. package/dist/components/rel-stepper/rel-stepper.ts +43 -31
  43. package/dist/components/rel-switch/rel-switch.ts +26 -12
  44. package/dist/components/rel-tab/rel-tab.ts +57 -37
  45. package/dist/components/rel-table/rel-table-body.ts +5 -0
  46. package/dist/components/rel-table/rel-table-caption.ts +6 -1
  47. package/dist/components/rel-table/rel-table-cell.ts +42 -49
  48. package/dist/components/rel-table/rel-table-footer.ts +5 -0
  49. package/dist/components/rel-table/rel-table-head.ts +13 -1
  50. package/dist/components/rel-table/rel-table-header.ts +5 -0
  51. package/dist/components/rel-table/rel-table-row.ts +7 -3
  52. package/dist/components/rel-table/rel-table.ts +8 -2
  53. package/dist/components/rel-textfield/rel-textfield.ts +153 -44
  54. package/dist/components/rel-toast/rel-toast.ts +56 -22
  55. package/dist/components/rel-tooltip/rel-tooltip.ts +52 -9
  56. package/dist/components/rel-topbar/rel-topbar-actions.ts +24 -0
  57. package/dist/components/rel-topbar/rel-topbar-brand.ts +47 -0
  58. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +63 -0
  59. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +28 -0
  60. package/dist/components/rel-topbar/rel-topbar-item.ts +320 -0
  61. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +24 -0
  62. package/dist/components/rel-topbar/rel-topbar-profile.ts +222 -0
  63. package/dist/components/rel-topbar/rel-topbar-utilities.ts +24 -0
  64. package/dist/components/rel-topbar/rel-topbar.ts +438 -0
  65. package/dist/index.d.mts +375 -411
  66. package/dist/index.d.ts +375 -411
  67. package/dist/index.js +7587 -7980
  68. package/dist/index.js.map +1 -1
  69. package/dist/index.mjs +7647 -8023
  70. package/dist/index.mjs.map +1 -1
  71. package/dist/rel-menubar-menu-BEEQDAXW.css +3 -0
  72. package/dist/styles/vars-v2.css +525 -0
  73. package/dist/styles/vars.css +79 -27
  74. package/dist/{vars-ITL2K3KO.css → vars-I2PC74XD.css} +79 -27
  75. package/dist/vars-v2-VMGWTFJZ.css +525 -0
  76. package/package.json +3 -1
  77. package/dist/components/rel-quote-page/README.md +0 -166
  78. package/dist/components/rel-quote-page/rel-quote-page.ts +0 -816
@@ -1,683 +1,362 @@
1
- import { LitElement, html, css } from "lit";
2
- import { customElement, property, state, query } from "lit/decorators.js";
3
-
4
- export type SheetSide = "top" | "right" | "bottom" | "left";
5
- export type SheetSize = "sm" | "md" | "lg" | "xl";
6
- export type SheetCloseReason = "trigger" | "close" | "overlay" | "escape" | "api";
7
-
8
- // ============================================================================
9
- // rel-sheet (Controller/Root Component)
10
- // ============================================================================
11
- @customElement("rel-sheet")
12
- export class RelSheet extends LitElement {
13
- @property({ type: Boolean, reflect: true }) open?: boolean;
14
- @property({ type: Boolean, attribute: "defaultopen" }) defaultOpen = false;
15
- @property({ type: Boolean }) disabled = false;
16
- @property({ type: Boolean }) modal = true;
17
-
18
- @state() private internalOpen = false;
19
-
20
- static styles = css`
21
- :host {
22
- display: contents;
23
- }
24
- `;
25
-
26
- connectedCallback() {
27
- super.connectedCallback();
28
- this.internalOpen = this.isControlled() ? !!this.open : this.defaultOpen;
29
- }
30
-
31
- updated(changed: Map<string, unknown>) {
32
- if (changed.has("open") && this.isControlled()) {
33
- this.internalOpen = !!this.open;
34
- }
35
- }
36
-
37
- private isControlled() {
38
- return this.open !== undefined;
39
- }
40
-
41
- get activeOpen() {
42
- return this.isControlled() ? !!this.open : this.internalOpen;
43
- }
44
-
45
- setOpen(next: boolean, reason: SheetCloseReason = "api") {
46
- if (!this.isControlled()) {
47
- this.internalOpen = next;
48
- }
49
-
50
- this.dispatchEvent(
51
- new CustomEvent("rel-open-change", {
52
- detail: { open: next, reason },
53
- bubbles: true,
54
- composed: true,
55
- })
56
- );
57
- }
58
-
59
- render() {
60
- return html`<slot></slot>`;
61
- }
62
- }
63
-
64
- // ============================================================================
65
- // rel-sheet-trigger (Opens Sheet)
66
- // ============================================================================
67
- @customElement("rel-sheet-trigger")
68
- export class RelSheetTrigger extends LitElement {
69
- static styles = css`
70
- :host {
71
- display: inline-block;
72
- }
73
-
74
- button {
75
- background: none;
76
- border: none;
77
- padding: 0;
78
- margin: 0;
79
- font: inherit;
80
- cursor: pointer;
81
- display: contents;
82
- }
83
- `;
84
-
85
- private getSheet(): RelSheet | null {
86
- let parent = this.parentElement;
87
- while (parent) {
88
- if (parent instanceof RelSheet) {
89
- return parent;
90
- }
91
- parent = parent.parentElement;
92
- }
93
- return null;
94
- }
95
-
96
- private handleClick = () => {
97
- const sheet = this.getSheet();
98
- if (sheet && !sheet.disabled) {
99
- sheet.setOpen(!sheet.activeOpen, "trigger");
100
- }
101
- };
102
-
103
- render() {
104
- return html`
105
- <button @click="${this.handleClick}" type="button">
106
- <slot></slot>
107
- </button>
108
- `;
109
- }
110
- }
111
-
112
- // ============================================================================
113
- // rel-sheet-content (Panel)
114
- // ============================================================================
115
- @customElement("rel-sheet-content")
116
- export class RelSheetContent extends LitElement {
117
- @property({ type: String }) side: SheetSide = "right";
118
- @property({ type: String }) size: SheetSize = "md";
119
- @property({ type: Boolean, attribute: "hide-overlay" }) hideOverlay = false;
120
- @property({ type: Boolean, attribute: "close-on-overlay" }) closeOnOverlay = true;
121
- @property({ type: Boolean, attribute: "show-close" }) showClose = true;
122
-
123
- @query(".panel") private panelElement!: HTMLElement;
124
- @query(".overlay") private overlayElement!: HTMLElement;
125
-
126
- private sheet: RelSheet | null = null;
127
- private previousFocus: HTMLElement | null = null;
128
- private originalBodyOverflow = "";
129
- private originalBodyPaddingRight = "";
130
- private isOpen = false;
131
-
132
- static styles = css`
133
- :host {
134
- --sheet-transition-duration: 0.3s;
135
- --sheet-transition-timing: cubic-bezier(0.32, 0.72, 0, 1);
136
- --sheet-size-sm: 320px;
137
- --sheet-size-md: 420px;
138
- --sheet-size-lg: 540px;
139
- --sheet-size-xl: 640px;
140
- }
141
-
142
- .sheet-container {
143
- position: fixed;
144
- top: 0;
145
- left: 0;
146
- right: 0;
147
- bottom: 0;
148
- z-index: 1000;
149
- pointer-events: none;
150
- }
151
-
152
- .sheet-container.open {
153
- pointer-events: auto;
154
- }
155
-
156
- /* Overlay */
157
- .overlay {
158
- position: absolute;
159
- top: 0;
160
- left: 0;
161
- right: 0;
162
- bottom: 0;
163
- background: var(--rel-overlay-scrim);
164
- opacity: 0;
165
- transition: opacity var(--sheet-transition-duration) var(--sheet-transition-timing);
166
- }
167
-
168
- .sheet-container.open .overlay {
169
- opacity: 1;
170
- }
171
-
172
- .overlay.hidden {
173
- display: none;
174
- }
175
-
176
- /* Panel base */
177
- .panel {
178
- position: absolute;
179
- background: var(--rel-colors-white);
180
- box-shadow: var(--rel-shadows-shadow-500);
181
- display: flex;
182
- flex-direction: column;
183
- outline: none;
184
- font-family: var(--rel-fontFamilies-primary);
185
- transition: transform var(--sheet-transition-duration) var(--sheet-transition-timing);
186
- overflow: hidden;
187
- }
188
-
189
- /* Right side (default) */
190
- .panel[data-side="right"] {
191
- top: 0;
192
- right: 0;
193
- bottom: 0;
194
- width: var(--sheet-width);
195
- max-width: 90vw;
196
- border-top-left-radius: var(--rel-borders-radius-border-radius-300);
197
- border-bottom-left-radius: var(--rel-borders-radius-border-radius-300);
198
- transform: translateX(100%);
199
- }
200
-
201
- .sheet-container.open .panel[data-side="right"] {
202
- transform: translateX(0);
203
- }
204
-
205
- /* Left side */
206
- .panel[data-side="left"] {
207
- top: 0;
208
- left: 0;
209
- bottom: 0;
210
- width: var(--sheet-width);
211
- max-width: 90vw;
212
- border-top-right-radius: var(--rel-borders-radius-border-radius-300);
213
- border-bottom-right-radius: var(--rel-borders-radius-border-radius-300);
214
- transform: translateX(-100%);
215
- }
216
-
217
- .sheet-container.open .panel[data-side="left"] {
218
- transform: translateX(0);
219
- }
220
-
221
- /* Top side */
222
- .panel[data-side="top"] {
223
- top: 0;
224
- left: 0;
225
- right: 0;
226
- height: var(--sheet-width);
227
- max-height: 80vh;
228
- border-bottom-left-radius: var(--rel-borders-radius-border-radius-300);
229
- border-bottom-right-radius: var(--rel-borders-radius-border-radius-300);
230
- transform: translateY(-100%);
231
- }
232
-
233
- .sheet-container.open .panel[data-side="top"] {
234
- transform: translateY(0);
235
- }
236
-
237
- /* Bottom side */
238
- .panel[data-side="bottom"] {
239
- bottom: 0;
240
- left: 0;
241
- right: 0;
242
- height: var(--sheet-width);
243
- max-height: 80vh;
244
- border-top-left-radius: var(--rel-borders-radius-border-radius-300);
245
- border-top-right-radius: var(--rel-borders-radius-border-radius-300);
246
- transform: translateY(100%);
247
- }
248
-
249
- .sheet-container.open .panel[data-side="bottom"] {
250
- transform: translateY(0);
251
- }
252
-
253
- /* Panel content wrapper */
254
- .panel-content {
255
- flex: 1;
256
- overflow-y: auto;
257
- padding: var(--rel-spacing-space-lg);
258
- }
259
-
260
- /* Close button */
261
- .close-button {
262
- position: absolute;
263
- top: var(--rel-spacing-space-md);
264
- right: var(--rel-spacing-space-md);
265
- background: none;
266
- border: none;
267
- cursor: pointer;
268
- padding: var(--rel-spacing-space-xs);
269
- display: flex;
270
- align-items: center;
271
- justify-content: center;
272
- border-radius: var(--rel-borders-radius-border-radius-200);
273
- color: var(--rel-colors-gray-heading);
274
- font-size: 20px;
275
- line-height: 1;
276
- z-index: 1;
277
- transition: background 0.2s;
278
- }
279
-
280
- .close-button:hover {
281
- background: var(--rel-colors-gray-surface);
282
- }
283
-
284
- .close-button:focus-visible {
285
- outline: 2px solid var(--rel-colors-primary-500);
286
- outline-offset: 2px;
287
- }
288
-
289
- /* Left side close button positioning */
290
- .panel[data-side="left"] .close-button {
291
- right: auto;
292
- left: var(--rel-spacing-space-md);
293
- }
294
-
295
- /* Reduced motion support */
296
- @media (prefers-reduced-motion: reduce) {
297
- .overlay,
298
- .panel {
299
- transition: none;
300
- }
301
-
302
- .sheet-container .panel {
303
- transform: none;
304
- }
305
-
306
- .sheet-container:not(.open) .panel {
307
- display: none;
308
- }
309
- }
310
- `;
311
-
312
- connectedCallback() {
313
- super.connectedCallback();
314
- this.findSheet();
315
- }
316
-
317
- disconnectedCallback() {
318
- super.disconnectedCallback();
319
- this.cleanup();
320
- }
321
-
322
- private findSheet() {
323
- let parent = this.parentElement;
324
- while (parent) {
325
- if (parent instanceof RelSheet) {
326
- this.sheet = parent;
327
- break;
328
- }
329
- parent = parent.parentElement;
330
- }
331
-
332
- if (this.sheet) {
333
- this.sheet.addEventListener("rel-open-change", this.handleSheetOpenChange);
334
- }
335
- }
336
-
337
- private handleSheetOpenChange = (e: Event) => {
338
- const customEvent = e as CustomEvent<{ open: boolean }>;
339
- const nextOpen = customEvent.detail.open;
340
-
341
- if (nextOpen !== this.isOpen) {
342
- this.isOpen = nextOpen;
343
- if (nextOpen) {
344
- this.handleOpen();
345
- } else {
346
- this.handleClose();
347
- }
348
- this.requestUpdate();
349
- }
350
- };
351
-
352
- private handleOpen() {
353
- this.previousFocus = document.activeElement as HTMLElement;
354
- this.lockScroll();
355
-
356
- if (this.sheet?.modal) {
357
- setTimeout(() => {
358
- this.focusPanel();
359
- }, 50);
360
- }
361
-
362
- document.addEventListener("keydown", this.handleKeydown);
363
- }
364
-
365
- private handleClose() {
366
- this.unlockScroll();
367
- document.removeEventListener("keydown", this.handleKeydown);
368
-
369
- if (this.previousFocus && this.previousFocus.focus) {
370
- setTimeout(() => {
371
- this.previousFocus?.focus();
372
- this.previousFocus = null;
373
- }, 50);
374
- }
375
- }
376
-
377
- private cleanup() {
378
- this.unlockScroll();
379
- document.removeEventListener("keydown", this.handleKeydown);
380
- if (this.sheet) {
381
- this.sheet.removeEventListener("rel-open-change", this.handleSheetOpenChange);
382
- }
383
- }
384
-
385
- private lockScroll() {
386
- if (typeof document === "undefined") return;
387
-
388
- const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
389
-
390
- this.originalBodyOverflow = document.body.style.overflow;
391
- this.originalBodyPaddingRight = document.body.style.paddingRight;
392
-
393
- document.body.style.overflow = "hidden";
394
- if (scrollbarWidth > 0) {
395
- document.body.style.paddingRight = `${scrollbarWidth}px`;
396
- }
397
- }
398
-
399
- private unlockScroll() {
400
- if (typeof document === "undefined") return;
401
-
402
- document.body.style.overflow = this.originalBodyOverflow;
403
- document.body.style.paddingRight = this.originalBodyPaddingRight;
404
- }
405
-
406
- private focusPanel() {
407
- if (!this.panelElement) return;
408
-
409
- const focusable = this.getFocusableElements();
410
- if (focusable.length > 0) {
411
- focusable[0].focus();
412
- } else {
413
- this.panelElement.setAttribute("tabindex", "-1");
414
- this.panelElement.focus();
415
- }
416
- }
417
-
418
- private getFocusableElements(): HTMLElement[] {
419
- if (!this.panelElement) return [];
420
-
421
- const selectors = [
422
- 'a[href]',
423
- 'button:not([disabled])',
424
- 'textarea:not([disabled])',
425
- 'input:not([disabled])',
426
- 'select:not([disabled])',
427
- '[tabindex]:not([tabindex="-1"])',
428
- ].join(",");
429
-
430
- const elements = Array.from(
431
- this.panelElement.querySelectorAll(selectors)
432
- ) as HTMLElement[];
433
-
434
- // Also check slotted content
435
- const slots = this.panelElement.querySelectorAll("slot");
436
- slots.forEach((slot) => {
437
- const assigned = slot.assignedElements({ flatten: true });
438
- assigned.forEach((el) => {
439
- const focusableInSlot = el.querySelectorAll(selectors);
440
- elements.push(...(Array.from(focusableInSlot) as HTMLElement[]));
441
- });
442
- });
443
-
444
- return elements.filter((el) => el.offsetWidth || el.offsetHeight);
445
- }
446
-
447
- private handleKeydown = (e: KeyboardEvent) => {
448
- if (!this.isOpen) return;
449
-
450
- if (e.key === "Escape") {
451
- e.preventDefault();
452
- this.sheet?.setOpen(false, "escape");
453
- return;
454
- }
455
-
456
- // Focus trap
457
- if (e.key === "Tab" && this.sheet?.modal) {
458
- const focusable = this.getFocusableElements();
459
- if (focusable.length === 0) {
460
- e.preventDefault();
461
- return;
462
- }
463
-
464
- const firstFocusable = focusable[0];
465
- const lastFocusable = focusable[focusable.length - 1];
466
- const activeElement = this.shadowRoot?.activeElement || document.activeElement;
467
-
468
- if (e.shiftKey) {
469
- if (activeElement === firstFocusable) {
470
- e.preventDefault();
471
- lastFocusable.focus();
472
- }
473
- } else {
474
- if (activeElement === lastFocusable) {
475
- e.preventDefault();
476
- firstFocusable.focus();
477
- }
478
- }
479
- }
480
- };
481
-
482
- private handleOverlayClick = (e: MouseEvent) => {
483
- if (!this.closeOnOverlay) return;
484
-
485
- if (e.target === this.overlayElement) {
486
- this.sheet?.setOpen(false, "overlay");
487
- }
488
- };
489
-
490
- private handleCloseClick = () => {
491
- this.sheet?.setOpen(false, "close");
492
- };
493
-
494
- private getSizeValue(): string {
495
- const sizeMap: Record<SheetSize, string> = {
496
- sm: "var(--sheet-size-sm)",
497
- md: "var(--sheet-size-md)",
498
- lg: "var(--sheet-size-lg)",
499
- xl: "var(--sheet-size-xl)",
500
- };
501
- return sizeMap[this.size] || sizeMap.md;
502
- }
503
-
504
- private getTitle(): string | null {
505
- const titleEl = this.querySelector("rel-sheet-title");
506
- return titleEl?.textContent?.trim() || null;
507
- }
508
-
509
- private getDescription(): string | null {
510
- const descEl = this.querySelector("rel-sheet-description");
511
- return descEl?.textContent?.trim() || null;
512
- }
513
-
514
- render() {
515
- const title = this.getTitle();
516
- const description = this.getDescription();
517
- const titleId = title ? "sheet-title" : undefined;
518
- const descId = description ? "sheet-description" : undefined;
519
-
520
- return html`
521
- <div class="sheet-container ${this.isOpen ? "open" : ""}">
522
- <div
523
- part="overlay"
524
- class="overlay ${this.hideOverlay ? "hidden" : ""}"
525
- @click="${this.handleOverlayClick}"></div>
526
-
527
- <div
528
- part="panel"
529
- class="panel"
530
- data-side="${this.side}"
531
- style="--sheet-width: ${this.getSizeValue()}"
532
- role="dialog"
533
- aria-modal="${this.sheet?.modal ?? true}"
534
- aria-labelledby="${titleId || ""}"
535
- aria-describedby="${descId || ""}"
536
- tabindex="-1">
537
- ${this.showClose
538
- ? html`
539
- <button
540
- class="close-button"
541
- @click="${this.handleCloseClick}"
542
- aria-label="Close"
543
- type="button">
544
- ✕
545
- </button>
546
- `
547
- : ""}
548
-
549
- <div class="panel-content">
550
- <slot></slot>
551
- </div>
552
- </div>
553
- </div>
554
- `;
555
- }
556
- }
557
-
558
- // ============================================================================
559
- // rel-sheet-header (Layout Helper)
560
- // ============================================================================
561
- @customElement("rel-sheet-header")
562
- export class RelSheetHeader extends LitElement {
563
- static styles = css`
564
- :host {
565
- display: block;
566
- padding-bottom: var(--rel-spacing-space-md);
567
- border-bottom: 1px solid var(--rel-colors-gray-border);
568
- margin-bottom: var(--rel-spacing-space-lg);
569
- }
570
- `;
571
-
572
- render() {
573
- return html`<slot></slot>`;
574
- }
575
- }
576
-
577
- // ============================================================================
578
- // rel-sheet-footer (Layout Helper)
579
- // ============================================================================
580
- @customElement("rel-sheet-footer")
581
- export class RelSheetFooter extends LitElement {
582
- static styles = css`
583
- :host {
584
- display: block;
585
- padding-top: var(--rel-spacing-space-md);
586
- border-top: 1px solid var(--rel-colors-gray-border);
587
- margin-top: var(--rel-spacing-space-lg);
588
- }
589
- `;
590
-
591
- render() {
592
- return html`<slot></slot>`;
593
- }
594
- }
595
-
596
- // ============================================================================
597
- // rel-sheet-title (Layout Helper)
598
- // ============================================================================
599
- @customElement("rel-sheet-title")
600
- export class RelSheetTitle extends LitElement {
601
- static styles = css`
602
- :host {
603
- display: block;
604
- font-size: 20px;
605
- font-weight: 600;
606
- color: var(--rel-colors-gray-heading);
607
- margin: 0 0 var(--rel-spacing-space-xs) 0;
608
- padding-right: var(--rel-spacing-space-lg);
609
- }
610
- `;
611
-
612
- render() {
613
- return html`<slot></slot>`;
614
- }
615
- }
616
-
617
- // ============================================================================
618
- // rel-sheet-description (Layout Helper)
619
- // ============================================================================
620
- @customElement("rel-sheet-description")
621
- export class RelSheetDescription extends LitElement {
622
- static styles = css`
623
- :host {
624
- display: block;
625
- font-size: 14px;
626
- color: var(--rel-colors-gray-body);
627
- margin: 0;
628
- line-height: 1.5;
629
- }
630
- `;
631
-
632
- render() {
633
- return html`<slot></slot>`;
634
- }
635
- }
636
-
637
- // ============================================================================
638
- // rel-sheet-close (Close Button Wrapper)
639
- // ============================================================================
640
- @customElement("rel-sheet-close")
641
- export class RelSheetClose extends LitElement {
642
- static styles = css`
643
- :host {
644
- display: inline-block;
645
- }
646
-
647
- button {
648
- background: none;
649
- border: none;
650
- padding: 0;
651
- margin: 0;
652
- font: inherit;
653
- cursor: pointer;
654
- display: contents;
655
- }
656
- `;
657
-
658
- private getSheet(): RelSheet | null {
659
- let parent = this.parentElement;
660
- while (parent) {
661
- if (parent instanceof RelSheet) {
662
- return parent;
663
- }
664
- parent = parent.parentElement;
665
- }
666
- return null;
667
- }
668
-
669
- private handleClick = () => {
670
- const sheet = this.getSheet();
671
- if (sheet) {
672
- sheet.setOpen(false, "close");
673
- }
674
- };
675
-
676
- render() {
677
- return html`
678
- <button @click="${this.handleClick}" type="button">
679
- <slot></slot>
680
- </button>
681
- `;
682
- }
683
- }
1
+ import { LitElement, html, css } from "lit";
2
+ import { customElement, property, query } from "lit/decorators.js";
3
+ import "../../styles/vars-v2.css";
4
+ import "@reliability-design/icons/rel-icon";
5
+
6
+ export type ModalSide = "top" | "bottom" | "center";
7
+
8
+ @customElement("rel-modal")
9
+ export class RelModal extends LitElement {
10
+ @property({ type: Boolean, reflect: true }) open = false;
11
+ @property({ type: String }) side: ModalSide = "bottom";
12
+ @property({ type: String }) title = "";
13
+ @property({ type: String }) description = "";
14
+ @property({ type: Boolean }) dismissible = true;
15
+ @property({ type: Boolean }) modal = true;
16
+
17
+ @query(".overlay") private overlayElement!: HTMLElement;
18
+
19
+ private previousFocus: HTMLElement | null = null;
20
+ private originalBodyOverflow = "";
21
+ private originalBodyPaddingRight = "";
22
+
23
+ static styles = css`
24
+ :host {
25
+ --modal-transition-duration: 0.3s;
26
+ --modal-transition-timing: cubic-bezier(0.32, 0.72, 0, 1);
27
+ }
28
+
29
+ .modal-container {
30
+ position: fixed;
31
+ top: 0;
32
+ left: 0;
33
+ right: 0;
34
+ bottom: 0;
35
+ z-index: 1000;
36
+ pointer-events: none;
37
+ }
38
+
39
+ .modal-container.open {
40
+ pointer-events: auto;
41
+ }
42
+
43
+ .overlay {
44
+ position: absolute;
45
+ top: 0;
46
+ left: 0;
47
+ right: 0;
48
+ bottom: 0;
49
+ background: var(--rel-overlay-scrim);
50
+ opacity: 0;
51
+ transition: opacity var(--modal-transition-duration) var(--modal-transition-timing);
52
+ }
53
+
54
+ .modal-container.open .overlay {
55
+ opacity: 1;
56
+ }
57
+
58
+ .overlay.no-modal {
59
+ display: none !important;
60
+ }
61
+
62
+ .panel {
63
+ position: absolute;
64
+ background: var(--rel-color-bg-primary);
65
+ box-shadow: var(--rel-shadows-shadow-500);
66
+ display: flex;
67
+ flex-direction: column;
68
+ overflow: hidden;
69
+ outline: none;
70
+ font-family: var(--rel-fontFamilies-primary);
71
+ width: clamp(360px, 49vw, 708px);
72
+ left: 50%;
73
+ transition: transform var(--modal-transition-duration) var(--modal-transition-timing);
74
+ }
75
+
76
+ .panel[data-side="bottom"] {
77
+ bottom: 48px;
78
+ left: 50%;
79
+ width: clamp(360px, 49vw, 708px);
80
+ max-height: calc(100vh - 96px);
81
+ border-radius: var(--rel-borders-radius-border-radius-200);
82
+ transform: translateX(-50%) translateY(calc(100% + 48px));
83
+ }
84
+
85
+ .modal-container.open .panel[data-side="bottom"] {
86
+ transform: translateX(-50%) translateY(0);
87
+ }
88
+
89
+ .panel[data-side="top"] {
90
+ top: 48px;
91
+ left: 50%;
92
+ width: clamp(360px, 49vw, 708px);
93
+ max-height: calc(100vh - 96px);
94
+ border-radius: var(--rel-borders-radius-border-radius-200);
95
+ transform: translateX(-50%) translateY(calc(-100% - 48px));
96
+ }
97
+
98
+ .modal-container.open .panel[data-side="top"] {
99
+ transform: translateX(-50%) translateY(0);
100
+ }
101
+
102
+ .panel[data-side="center"] {
103
+ top: 50%;
104
+ left: 50%;
105
+ width: clamp(360px, 49vw, 708px);
106
+ max-height: calc(100vh - 96px);
107
+ border-radius: var(--rel-borders-radius-border-radius-200);
108
+ transform: translate(-50%, -50%);
109
+ }
110
+
111
+ .modal-container.open .panel[data-side="center"] {
112
+ transform: translate(-50%, -50%);
113
+ }
114
+
115
+ .header {
116
+ display: flex;
117
+ align-items: flex-start;
118
+ justify-content: space-between;
119
+ padding: var(--rel-spacing-space-md) var(--rel-spacing-space-lg);
120
+ border-bottom: var(--rel-borders-width-stroke-100) solid var(--rel-color-border-default);
121
+ flex-shrink: 0;
122
+ }
123
+
124
+ .header.empty {
125
+ display: none;
126
+ }
127
+
128
+ .header-text {
129
+ display: flex;
130
+ flex-direction: column;
131
+ gap: var(--rel-spacing-space-2xs);
132
+ }
133
+
134
+ .title {
135
+ font-size: var(--rel-fontSizes-lg);
136
+ font-weight: var(--rel-fontWeights-semibold);
137
+ color: var(--rel-color-text-heading);
138
+ margin: 0;
139
+ }
140
+
141
+ .description {
142
+ font-size: var(--rel-fontSizes-sm);
143
+ color: var(--rel-color-text-primary);
144
+ margin: 0;
145
+ }
146
+
147
+ .close-button {
148
+ background: transparent;
149
+ border: none;
150
+ cursor: pointer;
151
+ padding: var(--rel-spacing-space-2xs);
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ align-self: flex-start;
156
+ flex-shrink: 0;
157
+ border-radius: var(--rel-borders-radius-border-radius-100);
158
+ color: var(--rel-color-text-primary);
159
+ transition: background 0.15s ease;
160
+ }
161
+
162
+ .close-button:hover {
163
+ background: var(--rel-color-bg-tertiary);
164
+ }
165
+
166
+ .close-button:focus-visible {
167
+ outline: 2px solid var(--rel-color-border-focus);
168
+ outline-offset: 2px;
169
+ }
170
+
171
+ .content {
172
+ flex: 1;
173
+ overflow-y: auto;
174
+ padding: var(--rel-spacing-space-lg);
175
+ }
176
+
177
+ .footer {
178
+ display: none;
179
+ }
180
+
181
+ :host([has-footer]) .footer {
182
+ display: flex;
183
+ flex-shrink: 0;
184
+ padding: var(--rel-spacing-space-md) var(--rel-spacing-space-lg);
185
+ border-top: var(--rel-borders-width-stroke-100) solid var(--rel-color-border-default);
186
+ justify-content: flex-end;
187
+ gap: var(--rel-spacing-space-xs);
188
+ }
189
+
190
+ @media (prefers-reduced-motion: reduce) {
191
+ .overlay,
192
+ .panel {
193
+ transition: none;
194
+ }
195
+
196
+ .modal-container .panel {
197
+ transform: none;
198
+ }
199
+
200
+ .modal-container:not(.open) .panel {
201
+ display: none;
202
+ }
203
+ }
204
+ `;
205
+
206
+ connectedCallback() {
207
+ super.connectedCallback();
208
+ if (this.open) {
209
+ this.lockScroll();
210
+ }
211
+ this.updateComplete.then(() => {
212
+ const footerSlot = this.shadowRoot?.querySelector(
213
+ 'slot[name="footer"]'
214
+ ) as HTMLSlotElement | null;
215
+ if (footerSlot) {
216
+ const sync = () => {
217
+ const hasContent = footerSlot.assignedElements({ flatten: true }).length > 0;
218
+ this.toggleAttribute('has-footer', hasContent);
219
+ };
220
+ footerSlot.addEventListener('slotchange', sync);
221
+ sync();
222
+ }
223
+ });
224
+ }
225
+
226
+ disconnectedCallback() {
227
+ super.disconnectedCallback();
228
+ this.unlockScroll();
229
+ document.removeEventListener("keydown", this.handleKeydown);
230
+ }
231
+
232
+ updated(changed: Map<string, unknown>) {
233
+ if (changed.has("open")) {
234
+ if (this.open) {
235
+ this.handleOpen();
236
+ } else {
237
+ this.handleClose();
238
+ }
239
+ }
240
+ }
241
+
242
+ private handleOpen() {
243
+ this.previousFocus = document.activeElement as HTMLElement;
244
+ this.lockScroll();
245
+ document.addEventListener("keydown", this.handleKeydown);
246
+ this.dispatchEvent(
247
+ new CustomEvent("rel-open-change", {
248
+ detail: true,
249
+ bubbles: true,
250
+ composed: true,
251
+ })
252
+ );
253
+ }
254
+
255
+ private handleClose() {
256
+ this.unlockScroll();
257
+ document.removeEventListener("keydown", this.handleKeydown);
258
+ if (this.previousFocus?.focus) {
259
+ this.updateComplete.then(() => {
260
+ this.previousFocus?.focus();
261
+ this.previousFocus = null;
262
+ });
263
+ }
264
+ this.dispatchEvent(
265
+ new CustomEvent("rel-open-change", {
266
+ detail: false,
267
+ bubbles: true,
268
+ composed: true,
269
+ })
270
+ );
271
+ }
272
+
273
+ private lockScroll() {
274
+ if (typeof document === "undefined") return;
275
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
276
+ this.originalBodyOverflow = document.body.style.overflow;
277
+ this.originalBodyPaddingRight = document.body.style.paddingRight;
278
+ document.body.style.overflow = "hidden";
279
+ if (scrollbarWidth > 0) {
280
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
281
+ }
282
+ }
283
+
284
+ private unlockScroll() {
285
+ if (typeof document === "undefined") return;
286
+ document.body.style.overflow = this.originalBodyOverflow;
287
+ document.body.style.paddingRight = this.originalBodyPaddingRight;
288
+ }
289
+
290
+ private handleKeydown = (e: KeyboardEvent) => {
291
+ if (!this.open) return;
292
+ if (e.key === "Escape" && this.dismissible) {
293
+ e.preventDefault();
294
+ this.open = false;
295
+ }
296
+ };
297
+
298
+ private handleOverlayClick = (e: MouseEvent) => {
299
+ if (!this.dismissible) return;
300
+ if (e.target === this.overlayElement) {
301
+ this.open = false;
302
+ }
303
+ };
304
+
305
+ private handleCloseButtonClick() {
306
+ this.open = false;
307
+ }
308
+
309
+ render() {
310
+ const hasTitle = !!this.title;
311
+ const hasDescription = !!this.description;
312
+ const showHeader = hasTitle || hasDescription || this.dismissible;
313
+
314
+ return html`
315
+ <div class="modal-container ${this.open ? "open" : ""}">
316
+ <div
317
+ part="overlay"
318
+ class="overlay ${!this.modal ? 'no-modal' : ''}"
319
+ @click="${this.handleOverlayClick}"></div>
320
+
321
+ <div
322
+ part="panel"
323
+ class="panel"
324
+ data-side="${this.side}"
325
+ role="${this.modal ? "dialog" : "region"}"
326
+ aria-modal="${this.modal}"
327
+ tabindex="-1">
328
+
329
+ <div part="header" class="header ${!showHeader ? "empty" : ""}">
330
+ <div class="header-text">
331
+ ${hasTitle ? html`<h2 class="title">${this.title}</h2>` : ""}
332
+ ${hasDescription ? html`<p class="description">${this.description}</p>` : ""}
333
+ </div>
334
+ ${this.dismissible
335
+ ? html`<button
336
+ part="close-button"
337
+ class="close-button"
338
+ @click="${this.handleCloseButtonClick}"
339
+ aria-label="Close">
340
+ <rel-icon name="close" size="sm"></rel-icon>
341
+ </button>`
342
+ : ""}
343
+ </div>
344
+
345
+ <div part="content" class="content">
346
+ <slot></slot>
347
+ </div>
348
+
349
+ <div part="footer" class="footer">
350
+ <slot name="footer"></slot>
351
+ </div>
352
+ </div>
353
+ </div>
354
+ `;
355
+ }
356
+ }
357
+
358
+ declare global {
359
+ interface HTMLElementTagNameMap {
360
+ "rel-modal": RelModal;
361
+ }
362
+ }