@reliability-design/web 1.0.2 → 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.
Files changed (145) hide show
  1. package/dist/components/rel-accordion/README.md +132 -0
  2. package/dist/components/rel-accordion/rel-accordion.ts +61 -29
  3. package/dist/components/rel-alert/README.md +141 -0
  4. package/dist/components/rel-alert/rel-alert.ts +110 -67
  5. package/dist/components/rel-avatar/README.md +119 -0
  6. package/dist/components/rel-avatar/rel-avatar.ts +8 -15
  7. package/dist/components/rel-badge/README.md +86 -0
  8. package/dist/components/rel-badge/rel-badge.ts +28 -21
  9. package/dist/components/rel-breadcrumb/README.md +102 -0
  10. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +24 -18
  11. package/dist/components/rel-button/README.md +130 -0
  12. package/dist/components/rel-button/rel-button.ts +39 -59
  13. package/dist/components/rel-calendar/README.md +109 -0
  14. package/dist/components/rel-calendar/index.ts +1 -0
  15. package/dist/components/rel-calendar/rel-calendar.ts +467 -0
  16. package/dist/components/rel-card/README.md +94 -0
  17. package/dist/components/rel-card/rel-card.ts +21 -11
  18. package/dist/components/rel-cardmedia/README.md +127 -0
  19. package/dist/components/rel-cardmedia/rel-cardmedia.ts +22 -18
  20. package/dist/components/rel-carousel/README.md +166 -0
  21. package/dist/components/rel-carousel/index.ts +1 -0
  22. package/dist/components/rel-carousel/rel-carousel.ts +469 -0
  23. package/dist/components/rel-chart/README.md +286 -0
  24. package/dist/components/rel-chart/index.ts +1 -0
  25. package/dist/components/rel-chart/rel-chart.ts +736 -0
  26. package/dist/components/rel-checkbox/README.md +77 -0
  27. package/dist/components/rel-checkbox/rel-checkbox.ts +36 -45
  28. package/dist/components/rel-chip/README.md +114 -0
  29. package/dist/components/rel-chip/rel-chip.ts +47 -122
  30. package/dist/components/rel-container/README.md +99 -0
  31. package/dist/components/rel-container/rel-container.ts +1 -6
  32. package/dist/components/rel-date-picker/README.md +253 -0
  33. package/dist/components/rel-date-picker/index.ts +1 -0
  34. package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
  35. package/dist/components/rel-divider/README.md +100 -0
  36. package/dist/components/rel-divider/rel-divider.ts +37 -37
  37. package/dist/components/rel-drawer/README.md +200 -0
  38. package/dist/components/rel-drawer/index.ts +1 -0
  39. package/dist/components/rel-drawer/rel-drawer.ts +621 -0
  40. package/dist/components/rel-kbd/README.md +151 -0
  41. package/dist/components/rel-kbd/rel-kbd.ts +118 -0
  42. package/dist/components/rel-layout/README.md +88 -0
  43. package/dist/components/rel-layout/rel-layout.ts +12 -14
  44. package/dist/components/rel-menu/README.md +151 -0
  45. package/dist/components/rel-menu/rel-menu.ts +18 -10
  46. package/dist/components/rel-menubar/README.md +329 -0
  47. package/dist/components/rel-menubar/index.ts +2 -0
  48. package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
  49. package/dist/components/rel-menubar/rel-menubar.ts +576 -0
  50. package/dist/components/rel-nav-menu/README.md +289 -0
  51. package/dist/components/rel-nav-menu/index.ts +3 -0
  52. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +143 -0
  53. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
  54. package/dist/components/rel-nav-menu/rel-nav-menu.ts +331 -0
  55. package/dist/components/rel-navbar/README.md +126 -0
  56. package/dist/components/rel-navbar/rel-navbar.ts +91 -0
  57. package/dist/components/rel-otp/README.md +440 -0
  58. package/dist/components/rel-otp/index.ts +1 -0
  59. package/dist/components/rel-otp/rel-otp.ts +504 -0
  60. package/dist/components/rel-pagination/README.md +410 -0
  61. package/dist/components/rel-pagination/index.ts +1 -0
  62. package/dist/components/rel-pagination/rel-pagination.ts +376 -0
  63. package/dist/components/rel-popover/README.md +568 -0
  64. package/dist/components/rel-popover/index.ts +2 -0
  65. package/dist/components/rel-popover/rel-popover-content.ts +55 -0
  66. package/dist/components/rel-popover/rel-popover.ts +327 -0
  67. package/dist/components/rel-progress/README.md +110 -0
  68. package/dist/components/rel-progress/rel-progress.ts +58 -20
  69. package/dist/components/rel-quote-page/README.md +166 -0
  70. package/dist/components/rel-quote-page/rel-quote-page.ts +816 -0
  71. package/dist/components/rel-radio/README.md +120 -0
  72. package/dist/components/rel-radio/rel-radio.ts +41 -73
  73. package/dist/components/rel-resizable/README.md +140 -0
  74. package/dist/components/rel-resizable/index.ts +3 -0
  75. package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
  76. package/dist/components/rel-resizable/rel-resizable-handle.ts +149 -0
  77. package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
  78. package/dist/components/rel-segmented-button/README.md +141 -0
  79. package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
  80. package/dist/components/rel-select/README.md +107 -0
  81. package/dist/components/rel-select/rel-select.ts +14 -14
  82. package/dist/components/rel-sheet/README.md +224 -0
  83. package/dist/components/rel-sheet/rel-sheet.ts +683 -0
  84. package/dist/components/rel-sidebar/README.md +209 -0
  85. package/dist/components/rel-sidebar/rel-sidebar.ts +1158 -0
  86. package/dist/components/rel-slider/README.md +135 -0
  87. package/dist/components/rel-slider/rel-slider.ts +19 -7
  88. package/dist/components/rel-snackbar/README.md +134 -0
  89. package/dist/components/rel-snackbar/rel-snackbar.ts +73 -2
  90. package/dist/components/rel-stepper/README.md +160 -0
  91. package/dist/components/rel-stepper/rel-stepper.ts +29 -8
  92. package/dist/components/rel-switch/README.md +102 -0
  93. package/dist/components/rel-switch/rel-switch.ts +22 -11
  94. package/dist/components/rel-tab/README.md +163 -0
  95. package/dist/components/rel-tab/rel-tab.ts +46 -22
  96. package/dist/components/rel-table/README.md +178 -0
  97. package/dist/components/rel-table/rel-table-body.ts +16 -0
  98. package/dist/components/rel-table/rel-table-caption.ts +22 -0
  99. package/dist/components/rel-table/rel-table-cell.ts +49 -0
  100. package/dist/components/rel-table/rel-table-footer.ts +18 -0
  101. package/dist/components/rel-table/rel-table-head.ts +33 -0
  102. package/dist/components/rel-table/rel-table-header.ts +16 -0
  103. package/dist/components/rel-table/rel-table-row.ts +32 -0
  104. package/dist/components/rel-table/rel-table.ts +47 -0
  105. package/dist/components/rel-textfield/README.md +90 -0
  106. package/dist/components/rel-textfield/rel-textfield.ts +37 -21
  107. package/dist/components/rel-toast/README.md +148 -0
  108. package/dist/components/rel-toast/rel-toast.ts +32 -14
  109. package/dist/components/rel-tooltip/README.md +103 -0
  110. package/dist/components/rel-tooltip/rel-tooltip.ts +9 -9
  111. package/dist/index.d.mts +846 -7
  112. package/dist/index.d.ts +846 -7
  113. package/dist/index.js +10860 -1803
  114. package/dist/index.js.map +1 -1
  115. package/dist/index.mjs +10820 -1809
  116. package/dist/index.mjs.map +1 -1
  117. package/dist/styles/vars.css +293 -183
  118. package/dist/vars-ITL2K3KO.css +294 -0
  119. package/package.json +2 -2
  120. package/dist/components/rel-accordion/rel-accordion.stories.ts +0 -313
  121. package/dist/components/rel-alert/rel-alert.stories.ts +0 -412
  122. package/dist/components/rel-avatar/rel-avatar.stories.ts +0 -327
  123. package/dist/components/rel-badge/rel-badge.stories.ts +0 -350
  124. package/dist/components/rel-breadcrumb/rel-breadcrumb.stories.ts +0 -306
  125. package/dist/components/rel-button/rel-button.stories.ts +0 -356
  126. package/dist/components/rel-card/rel-card.stories.ts +0 -299
  127. package/dist/components/rel-cardmedia/rel-cardmedia.stories.ts +0 -368
  128. package/dist/components/rel-checkbox/rel-checkbox.stories.ts +0 -414
  129. package/dist/components/rel-chip/rel-chip.stories.ts +0 -552
  130. package/dist/components/rel-container/rel-container.stories.ts +0 -239
  131. package/dist/components/rel-divider/rel-divider.stories.ts +0 -229
  132. package/dist/components/rel-layout/rel-layout.stories.ts +0 -713
  133. package/dist/components/rel-menu/rel-menu.stories.ts +0 -910
  134. package/dist/components/rel-progress/rel-progress.stories.ts +0 -817
  135. package/dist/components/rel-radio/rel-radio.stories.ts +0 -1048
  136. package/dist/components/rel-segmented-button/rel-segmented-button.stories.ts +0 -1079
  137. package/dist/components/rel-select/rel-select.stories.ts +0 -1201
  138. package/dist/components/rel-slider/rel-slider.stories.ts +0 -730
  139. package/dist/components/rel-snackbar/rel-snackbar.stories.ts +0 -808
  140. package/dist/components/rel-stepper/rel-stepper.stories.ts +0 -752
  141. package/dist/components/rel-switch/rel-switch.stories.ts +0 -441
  142. package/dist/components/rel-tab/rel-tab.stories.ts +0 -492
  143. package/dist/components/rel-textfield/rel-textfield.stories.ts +0 -710
  144. package/dist/components/rel-toast/rel-toast.stories.ts +0 -394
  145. package/dist/components/rel-tooltip/rel-tooltip.stories.ts +0 -364
@@ -151,7 +151,7 @@ export class RelStepper extends LitElement {
151
151
  .step-label {
152
152
  white-space: nowrap;
153
153
  font-size: var(--rel-fontSizes-sm);
154
- color: var(--rel-colors-gray-paragraph);
154
+ color: var(--rel-text-primary, #333);
155
155
  }
156
156
 
157
157
  /* Horizontal text-below specific */
@@ -226,7 +226,10 @@ export class RelStepper extends LitElement {
226
226
  display: none;
227
227
  }
228
228
 
229
- .rel-stepper.vertical.text-right .rel-step:last-child .indicator-wrapper .connector {
229
+ .rel-stepper.vertical.text-right
230
+ .rel-step:last-child
231
+ .indicator-wrapper
232
+ .connector {
230
233
  display: none;
231
234
  }
232
235
 
@@ -246,9 +249,9 @@ export class RelStepper extends LitElement {
246
249
  }
247
250
 
248
251
  .rel-step.disabled .indicator {
249
- background: var(--rel-colors-gray-bg-subtle);
250
- border-color: var(--rel-colors-gray-border);
251
- color: var(--rel-colors-gray-disabled);
252
+ background: var(--rel-colors-gray-light, #fafafa);
253
+ border-color: var(--rel-colors-gray-border, #ddd);
254
+ color: var(--rel-text-disabled, #9e9e9e);
252
255
  }
253
256
 
254
257
  .rel-step.done .connector {
@@ -270,7 +273,7 @@ export class RelStepper extends LitElement {
270
273
  }
271
274
 
272
275
  .rel-step.disabled .step-label {
273
- color: var(--rel-colors-gray-disabled);
276
+ color: var(--rel-text-disabled, #9e9e9e);
274
277
  }
275
278
 
276
279
  /********************************************
@@ -331,6 +334,21 @@ export class RelStepper extends LitElement {
331
334
  return classes.join(" ");
332
335
  }
333
336
 
337
+ private onStepClick(step: Step, index: number) {
338
+ if (step.status === "disabled") return;
339
+
340
+ this.dispatchEvent(
341
+ new CustomEvent("step-change", {
342
+ detail: {
343
+ index,
344
+ step,
345
+ },
346
+ bubbles: true,
347
+ composed: true,
348
+ })
349
+ );
350
+ }
351
+
334
352
  private renderIndicator(step: Step, index: number) {
335
353
  if (step.status === "done") {
336
354
  return html`
@@ -344,7 +362,7 @@ export class RelStepper extends LitElement {
344
362
  >
345
363
  <path
346
364
  d="M6.44479 14L0.5 7.36383L1.9862 5.70478L6.44479 10.6819L16.0138 0L17.5 1.65904L6.44479 14Z"
347
- fill="white"
365
+ fill="currentColor"
348
366
  />
349
367
  </svg>
350
368
  `;
@@ -369,7 +387,10 @@ export class RelStepper extends LitElement {
369
387
  <div class="${stepperClasses}">
370
388
  ${this.steps.map(
371
389
  (step, idx) => html`
372
- <div class="${this.getStepClasses(step)}">
390
+ <div
391
+ class="${this.getStepClasses(step)}"
392
+ @click="${() => this.onStepClick(step, idx)}"
393
+ >
373
394
  ${isVertical && isTextRight
374
395
  ? html`
375
396
  <!-- Indicator with connector below -->
@@ -0,0 +1,102 @@
1
+ # rel-switch
2
+
3
+ ## Overview
4
+ A toggle switch component for binary on/off states. Features smooth animations, focus halo effects, pressed states, and respects user motion preferences. Built with custom visual styling while maintaining native checkbox accessibility.
5
+
6
+ ## Usage
7
+ ```html
8
+ <!-- Basic switch -->
9
+ <rel-switch></rel-switch>
10
+
11
+ <!-- Pre-checked switch -->
12
+ <rel-switch checked></rel-switch>
13
+
14
+ <!-- Disabled switch -->
15
+ <rel-switch disabled></rel-switch>
16
+ <rel-switch checked disabled></rel-switch>
17
+
18
+ <!-- With event listener -->
19
+ <rel-switch id="notifications"></rel-switch>
20
+ <script>
21
+ const toggle = document.getElementById('notifications');
22
+ toggle.addEventListener('changed', (e) => {
23
+ console.log('Switch state:', e.detail); // boolean: true/false
24
+ });
25
+ </script>
26
+
27
+ <!-- Programmatically control -->
28
+ <rel-switch id="darkMode"></rel-switch>
29
+ <script>
30
+ const darkModeSwitch = document.getElementById('darkMode');
31
+ darkModeSwitch.checked = true;
32
+ darkModeSwitch.disabled = false;
33
+ </script>
34
+ ```
35
+
36
+ ## API
37
+
38
+ ### Attributes/Properties
39
+ | Name | Type | Default | Description |
40
+ |------|------|---------|-------------|
41
+ | `checked` | `boolean` | `false` | Whether the switch is in the ON position |
42
+ | `disabled` | `boolean` | `false` | Disables the switch and reduces opacity |
43
+
44
+ ## Events
45
+ | Event | Detail | Description |
46
+ |-------|--------|-------------|
47
+ | `changed` | `boolean` (the new checked state) | Fired when the switch is toggled by user interaction |
48
+
49
+ ## Slots
50
+ Default slot not used.
51
+
52
+ ## CSS Parts
53
+ None exposed.
54
+
55
+ ## Tokens / CSS Variables
56
+
57
+ ### Component-Level Custom Properties
58
+ The component defines these CSS custom properties on `:host` which can be overridden:
59
+
60
+ - `--switch-track` - Track color when unchecked (default: `--rel-text-placeholder`)
61
+ - `--switch-knob-on` - Knob color when checked (default: `--rel-colors-black`)
62
+ - `--switch-knob-off` - Knob color when unchecked (default: `--rel-colors-white`)
63
+ - `--switch-halo` - Focus halo color (default: `--rel-border-default`)
64
+ - `--switch-disabled` - Color for disabled state (default: `--rel-text-disabled`)
65
+
66
+ ### Design Token References
67
+ - `--rel-text-placeholder` - Default track color
68
+ - `--rel-text-dark-placeholder` - Hover track color
69
+ - `--rel-colors-black` - ON knob color
70
+ - `--rel-colors-white` - OFF knob color
71
+ - `--rel-border-default` - Focus halo
72
+ - `--rel-text-disabled` - Disabled state color
73
+ - `--rel-surface-surface` - Used in disabled styling
74
+
75
+ ## Accessibility
76
+ - Uses native checkbox input with `role="switch"` and `aria-checked`
77
+ - Visually hidden checkbox maintains keyboard and screen reader support
78
+ - Focus visible state shows prominent halo around knob
79
+ - Disabled state prevents interaction and reduces opacity to 0.5
80
+ - Smooth transitions respect `prefers-reduced-motion` media query
81
+
82
+ ### Keyboard Interactions
83
+ - `Space` - Toggle switch on/off
84
+ - `Tab` - Move focus to/from switch
85
+
86
+ ### Focus Behavior
87
+ - Focus ring appears as a 6px halo around the knob using `--switch-halo` color
88
+ - Halo uses box-shadow for smooth visual feedback
89
+
90
+ ### Motion Support
91
+ - Animations disabled when `prefers-reduced-motion: reduce` is set
92
+ - Transitions apply to track color, knob position, knob color, and box-shadow
93
+
94
+ ## Notes / Edge Cases
95
+ - The switch is 42px wide × 22px tall with a 12px tall track and 18px diameter knob
96
+ - Visual knob animates 22px horizontally when toggling from OFF to ON
97
+ - Hover effect only applies when not disabled (track darkens slightly)
98
+ - Pressed state applies a `scale(0.9)` transform to the knob for tactile feedback
99
+ - The knob includes a subtle drop shadow (`0 1px 3px rgba(0,0,0,0.3)`) for depth
100
+ - When disabled, both track and knob use `--switch-disabled` color
101
+ - The component is `display: inline-block` by default
102
+ - Focus halo appears on keyboard focus only (`:focus-visible`), not on mouse click
@@ -11,11 +11,11 @@ export class RelSwitch extends LitElement {
11
11
 
12
12
  static styles = css`
13
13
  :host {
14
- --switch-track: #7e8186;
15
- --switch-knob-on: #000000;
16
- --switch-knob-off: #ffffff;
17
- --switch-halo: rgba(224, 224, 224, 0.8);
18
- --switch-disabled: #d1d5db;
14
+ --switch-track: var(--rel-text-placeholder);
15
+ --switch-knob-on: var(--rel-colors-black);
16
+ --switch-knob-off: var(--rel-colors-white);
17
+ --switch-halo: var(--rel-border-default);
18
+ --switch-disabled: var(--rel-text-disabled);
19
19
  display: inline-block;
20
20
  }
21
21
 
@@ -77,7 +77,7 @@ export class RelSwitch extends LitElement {
77
77
 
78
78
  /* Hover */
79
79
  .switch-wrapper:hover:not(.switch-disabled) .switch-slider {
80
- background-color: #666;
80
+ background-color: var(--rel-text-dark-placeholder);
81
81
  }
82
82
 
83
83
  /* Focus halo */
@@ -100,14 +100,23 @@ export class RelSwitch extends LitElement {
100
100
  .switch-disabled .switch-slider::before {
101
101
  background-color: var(--switch-disabled);
102
102
  }
103
+
104
+ @media (prefers-reduced-motion: reduce) {
105
+ .switch-slider,
106
+ .switch-slider::before {
107
+ transition: none;
108
+ }
109
+ }
103
110
  `;
104
111
 
105
- private toggle() {
112
+ private toggle(ev: Event) {
106
113
  if (this.disabled) return;
107
- this.checked = !this.checked;
108
- this.requestUpdate();
114
+
115
+ const input = ev.target as HTMLInputElement;
116
+ this.checked = input.checked;
117
+
109
118
  this.dispatchEvent(
110
- new CustomEvent("changed", {
119
+ new CustomEvent<boolean>("changed", {
111
120
  detail: this.checked,
112
121
  bubbles: true,
113
122
  composed: true,
@@ -146,9 +155,11 @@ export class RelSwitch extends LitElement {
146
155
  <label class="${wrapperClasses}">
147
156
  <input
148
157
  type="checkbox"
158
+ role="switch"
159
+ aria-checked="${this.checked}"
149
160
  ?disabled=${this.disabled}
150
161
  .checked=${this.checked}
151
- @change=${this.toggle}
162
+ @change=${(e: Event) => this.toggle(e)}
152
163
  @focus=${this.handleFocus}
153
164
  @blur=${this.handleBlur}
154
165
  @mousedown=${this.handleMouseDown}
@@ -0,0 +1,163 @@
1
+ # rel-tab
2
+
3
+ ## Overview
4
+ A tabbed interface component with automatic underline indicator, keyboard navigation, optional badge counts, and disabled states. Features ARIA-compliant navigation with roving tabindex and arrow key support.
5
+
6
+ ## Usage
7
+ ```html
8
+ <!-- Basic tabs -->
9
+ <rel-tab id="basicTabs"></rel-tab>
10
+ <script>
11
+ const tabs = document.getElementById('basicTabs');
12
+ tabs.tabs = [
13
+ { label: 'Overview' },
14
+ { label: 'Details' },
15
+ { label: 'Settings' }
16
+ ];
17
+ tabs.activeIndex = 0;
18
+ </script>
19
+
20
+ <!-- With badges (notification counts) -->
21
+ <rel-tab id="badgeTabs"></rel-tab>
22
+ <script>
23
+ const badgeTabs = document.getElementById('badgeTabs');
24
+ badgeTabs.tabs = [
25
+ { label: 'Inbox', badge: 5 },
26
+ { label: 'Drafts', badge: 2 },
27
+ { label: 'Sent' }
28
+ ];
29
+ </script>
30
+
31
+ <!-- With disabled tab -->
32
+ <rel-tab id="disabledTabs"></rel-tab>
33
+ <script>
34
+ const disabledTabs = document.getElementById('disabledTabs');
35
+ disabledTabs.tabs = [
36
+ { label: 'Available' },
37
+ { label: 'Coming Soon', disabled: true },
38
+ { label: 'Active' }
39
+ ];
40
+ </script>
41
+
42
+ <!-- Listen for tab changes -->
43
+ <rel-tab id="tabsWithListener"></rel-tab>
44
+ <script>
45
+ const listener = document.getElementById('tabsWithListener');
46
+ listener.tabs = [
47
+ { label: 'Tab 1' },
48
+ { label: 'Tab 2' },
49
+ { label: 'Tab 3' }
50
+ ];
51
+ listener.addEventListener('tab-change', (e) => {
52
+ console.log('Active tab index:', e.detail);
53
+ });
54
+ </script>
55
+
56
+ <!-- Tab content controlled externally -->
57
+ <rel-tab id="contentTabs" activeIndex="0"></rel-tab>
58
+ <div id="tabContent">
59
+ <div class="panel" data-index="0">Content for Tab 1</div>
60
+ <div class="panel" data-index="1" style="display:none;">Content for Tab 2</div>
61
+ <div class="panel" data-index="2" style="display:none;">Content for Tab 3</div>
62
+ </div>
63
+ <script>
64
+ const contentTabs = document.getElementById('contentTabs');
65
+ contentTabs.tabs = [
66
+ { label: 'Tab 1' },
67
+ { label: 'Tab 2' },
68
+ { label: 'Tab 3' }
69
+ ];
70
+ contentTabs.addEventListener('tab-change', (e) => {
71
+ document.querySelectorAll('#tabContent .panel').forEach((panel, i) => {
72
+ panel.style.display = i === e.detail ? 'block' : 'none';
73
+ });
74
+ });
75
+ </script>
76
+ ```
77
+
78
+ ## API
79
+
80
+ ### Attributes/Properties
81
+ | Name | Type | Default | Description |
82
+ |------|------|---------|-------------|
83
+ | `tabs` | `TabItem[]` | `[]` | Array of tab items (see TabItem interface below) |
84
+ | `activeIndex` (reflected) | `number` | `0` | Index of the currently active tab |
85
+
86
+ ### TabItem Interface
87
+ ```typescript
88
+ interface TabItem {
89
+ label: string; // Display text
90
+ badge?: number; // Optional notification count
91
+ disabled?: boolean; // Disables interaction
92
+ }
93
+ ```
94
+
95
+ ## Events
96
+ | Event | Detail | Description |
97
+ |-------|--------|-------------|
98
+ | `tab-change` | `number` (the new active index) | Fired when a different tab is selected |
99
+
100
+ ## Slots
101
+ | Slot | Description |
102
+ |------|-------------|
103
+ | (default) | Tab panel content (rendered in `.tab-content` wrapper below tabs) |
104
+
105
+ ## CSS Parts
106
+ None exposed.
107
+
108
+ ## Tokens / CSS Variables
109
+
110
+ ### Component-Level Custom Properties
111
+ The component defines CSS custom properties on `:host`:
112
+
113
+ - `--tab-active-color` - Active tab underline color (default: `--rel-colors-green-primary`)
114
+ - `--tab-text-default` - Default tab text color (default: `--rel-text-primary`)
115
+ - `--tab-text-disabled` - Disabled tab text color (default: `--rel-text-disabled`)
116
+ - `--tab-text-hover` - Hover tab text color (default: `--rel-text-heading`)
117
+ - `--tab-underline-height` - Height of active underline (default: `3px`)
118
+ - `--tab-hover-color` - Background color on hover (default: `--rel-surface-surface`)
119
+
120
+ ### Design Token References
121
+ - `--rel-fontFamilies-primary` - Font family
122
+ - `--rel-fontSizes-md` - Font size
123
+ - `--rel-fontWeights-medium` - Font weight
124
+ - `--rel-spacing-space-sm`, `--rel-spacing-space-md` - Tab padding
125
+ - `--rel-spacing-space-2xs` - Gap between label and badge
126
+ - `--rel-border-tab` - Bottom border color (default: `#c4c4c4`)
127
+ - `--rel-border-info` - Focus outline color
128
+ - `--rel-borders-radius-border-radius-100` - Focus outline border radius
129
+ - `--rel-colors-red-primary` - Badge background color
130
+ - `--rel-colors-white` - Badge text color
131
+
132
+ ## Accessibility
133
+ - Uses `role="tablist"` on container and `role="tab"` on each tab
134
+ - Tabs have `aria-selected` and `aria-disabled` attributes
135
+ - Implements roving tabindex (only active tab is focusable via Tab key)
136
+ - Arrow key navigation between tabs
137
+ - Focus visible states with blue outline
138
+
139
+ ### Keyboard Interactions
140
+ - `Tab` - Focus the active tab (or first non-disabled tab)
141
+ - `Arrow Right` - Navigate to next tab (wraps to first, skips disabled)
142
+ - `Arrow Left` - Navigate to previous tab (wraps to last, skips disabled)
143
+ - `Enter / Space` - Activate focused tab
144
+
145
+ ### Focus Behavior
146
+ - Focus outline: 2px blue with -2px offset (inside boundary)
147
+ - Border radius applied to focus outline
148
+ - Focus automatically moves when navigating with arrow keys
149
+
150
+ ## Notes / Edge Cases
151
+ - The **active tab** has a colored underline (3px high by default) at the bottom
152
+ - **Hover effect** (non-active, non-disabled): light gray background
153
+ - **Badges** are circular (16px height, min 16px width) with red background and white text
154
+ - Clicking a disabled tab does nothing (no event dispatched, no index change)
155
+ - Arrow key navigation wraps around (after last tab, goes to first, and vice versa)
156
+ - Arrow key navigation skips disabled tabs
157
+ - When a tab is activated via arrow keys, focus is programmatically moved to that tab
158
+ - The component includes a `.tab-content` wrapper (with 1rem vertical padding) that renders slotted content
159
+ - The `.tab-content` is always visible; consumers must manually show/hide panel content based on `activeIndex`
160
+ - Tabs have a bottom border (1px solid) via `border-bottom` on `.tabs-wrapper`
161
+ - Active tab underline (`position: absolute`, `bottom: 0`) overlays the border
162
+ - Transitions: color (0.2s), background-color (0.2s), transform (0.2s) for smooth interactions
163
+ - The `activeIndex` property is reflected in HTML as an attribute
@@ -10,38 +10,44 @@ export interface TabItem {
10
10
  @customElement("rel-tab")
11
11
  export class RelTab extends LitElement {
12
12
  @property({ type: Array }) tabs: TabItem[] = [];
13
- @property({ type: Number }) activeIndex: number = 0;
13
+ @property({ type: Number, reflect: true }) activeIndex = 0;
14
14
 
15
15
  static styles = css`
16
16
  :host {
17
- --tab-active-color: #00f49c;
18
- --tab-text-default: #111;
19
- --tab-text-disabled: #a5a7aa;
20
- --tab-text-hover: #000;
17
+ --tab-active-color: var(--rel-colors-green-primary);
18
+ --tab-text-default: var(--rel-text-primary);
19
+ --tab-text-disabled: var(--rel-text-disabled);
20
+ --tab-text-hover: var(--rel-text-heading);
21
21
  --tab-underline-height: 3px;
22
- --tab-hover-color: #f3f3f3;
22
+ --tab-hover-color: var(--rel-surface-surface);
23
23
  display: block;
24
24
  font-family: var(--rel-fontFamilies-primary);
25
25
  }
26
26
 
27
27
  .tabs-wrapper {
28
28
  display: flex;
29
- border-bottom: 1px solid #e5e5e5;
30
- font-family: var(--rel-fontFamilies-primary);
29
+ border-bottom: 1px solid var(--rel-border-tab, #c4c4c4);
30
+ font-family: var(--rel-fontFamilies-primary, sans-serif);
31
31
  user-select: none;
32
32
  }
33
33
 
34
34
  .tab {
35
35
  position: relative;
36
- padding: 0.75rem 1rem;
36
+ padding: var(--rel-spacing-space-sm, 12px) var(--rel-spacing-space-md, 16px);
37
37
  cursor: pointer;
38
38
  color: var(--tab-text-default);
39
- font-size: 0.9rem;
40
- font-weight: 500;
39
+ font-size: var(--rel-fontSizes-md, 14px);
40
+ font-weight: var(--rel-fontWeights-medium, 500);
41
41
  transition: color 0.2s ease;
42
42
  display: flex;
43
43
  align-items: center;
44
- gap: 6px;
44
+ gap: var(--rel-spacing-space-2xs, 4px);
45
+ }
46
+
47
+ .tab:focus-visible {
48
+ outline: 2px solid var(--rel-border-info, #0091ea);
49
+ outline-offset: -2px;
50
+ border-radius: var(--rel-borders-radius-border-radius-100, 2px);
45
51
  }
46
52
 
47
53
  .tab:hover:not(.disabled) .tab-label {
@@ -77,8 +83,8 @@ export class RelTab extends LitElement {
77
83
  display: inline-flex;
78
84
  justify-content: center;
79
85
  align-items: center;
80
- background: #ff0400;
81
- color: #fff;
86
+ background: var(--rel-colors-red-primary);
87
+ color: var(--rel-colors-white);
82
88
  border-radius: 50%;
83
89
  font-size: 11px;
84
90
  font-weight: 600;
@@ -98,8 +104,8 @@ export class RelTab extends LitElement {
98
104
  if (this.tabs[index]?.disabled) return;
99
105
  this.activeIndex = index;
100
106
  this.dispatchEvent(
101
- new CustomEvent("tab-change", {
102
- detail: { index },
107
+ new CustomEvent<number>("tab-change", {
108
+ detail: index,
103
109
  bubbles: true,
104
110
  composed: true,
105
111
  })
@@ -107,24 +113,42 @@ export class RelTab extends LitElement {
107
113
  }
108
114
 
109
115
  private onKeyDown(event: KeyboardEvent, index: number) {
110
- if (this.tabs[index]?.disabled) return;
111
116
  if (event.key === "Enter" || event.key === " ") {
117
+ if (this.tabs[index]?.disabled) return;
112
118
  event.preventDefault();
113
119
  this.selectTab(index);
120
+ return;
121
+ }
122
+
123
+ if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
124
+ event.preventDefault();
125
+ const dir = event.key === "ArrowRight" ? 1 : -1;
126
+ let next = index;
127
+ for (let i = 0; i < this.tabs.length; i++) {
128
+ next = (next + dir + this.tabs.length) % this.tabs.length;
129
+ if (!this.tabs[next]?.disabled) break;
130
+ }
131
+ this.selectTab(next);
132
+ this.updateComplete.then(() => {
133
+ const tabEls = this.shadowRoot?.querySelectorAll("[role=tab]");
134
+ (tabEls?.[next] as HTMLElement)?.focus();
135
+ });
114
136
  }
115
137
  }
116
138
 
117
139
  render() {
118
140
  return html`
119
- <div class="tabs-wrapper">
141
+ <div class="tabs-wrapper" role="tablist">
120
142
  ${this.tabs.map(
121
143
  (tab, i) => html`
122
144
  <div
123
- class="tab ${i === this.activeIndex ? "active" : ""} ${tab.disabled
124
- ? "disabled"
125
- : ""}"
145
+ class="tab ${i === this.activeIndex
146
+ ? "active"
147
+ : ""} ${tab.disabled ? "disabled" : ""}"
126
148
  role="tab"
127
- tabindex="${tab.disabled ? -1 : 0}"
149
+ aria-selected="${i === this.activeIndex}"
150
+ aria-disabled="${tab.disabled ?? false}"
151
+ tabindex="${tab.disabled ? -1 : i === this.activeIndex ? 0 : -1}"
128
152
  @click="${() => this.selectTab(i)}"
129
153
  @keydown="${(e: KeyboardEvent) => this.onKeyDown(e, i)}"
130
154
  >