@reliability-design/web 1.0.3 → 1.1.1

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 (126) hide show
  1. package/dist/components/rel-accordion/README.md +132 -0
  2. package/dist/components/rel-accordion/rel-accordion.ts +47 -16
  3. package/dist/components/rel-alert/README.md +141 -0
  4. package/dist/components/rel-alert/rel-alert.ts +101 -20
  5. package/dist/components/rel-avatar/README.md +119 -0
  6. package/dist/components/rel-avatar/rel-avatar.ts +3 -2
  7. package/dist/components/rel-badge/README.md +86 -0
  8. package/dist/components/rel-badge/rel-badge.ts +11 -7
  9. package/dist/components/rel-breadcrumb/README.md +102 -0
  10. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
  11. package/dist/components/rel-button/README.md +130 -0
  12. package/dist/components/rel-button/rel-button.ts +64 -8
  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 +12 -3
  18. package/dist/components/rel-cardmedia/README.md +127 -0
  19. package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
  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 +468 -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 +51 -12
  28. package/dist/components/rel-chip/README.md +114 -0
  29. package/dist/components/rel-chip/rel-chip.ts +83 -9
  30. package/dist/components/rel-container/README.md +99 -0
  31. package/dist/components/rel-date-picker/README.md +253 -0
  32. package/dist/components/rel-date-picker/index.ts +1 -0
  33. package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
  34. package/dist/components/rel-divider/README.md +100 -0
  35. package/dist/components/rel-divider/rel-divider.ts +9 -0
  36. package/dist/components/rel-drawer/README.md +200 -0
  37. package/dist/components/rel-drawer/index.ts +1 -0
  38. package/dist/components/rel-drawer/rel-drawer-footer.ts +29 -0
  39. package/dist/components/rel-drawer/rel-drawer.ts +612 -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-menu/README.md +151 -0
  44. package/dist/components/rel-menu/rel-menu.ts +64 -24
  45. package/dist/components/rel-menubar/README.md +329 -0
  46. package/dist/components/rel-menubar/index.ts +2 -0
  47. package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
  48. package/dist/components/rel-menubar/rel-menubar.ts +605 -0
  49. package/dist/components/rel-nav-menu/README.md +289 -0
  50. package/dist/components/rel-nav-menu/index.ts +3 -0
  51. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +240 -0
  52. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
  53. package/dist/components/rel-nav-menu/rel-nav-menu.ts +227 -0
  54. package/dist/components/rel-navbar/README.md +126 -0
  55. package/dist/components/rel-navbar/rel-navbar.ts +104 -0
  56. package/dist/components/rel-otp/README.md +440 -0
  57. package/dist/components/rel-otp/index.ts +1 -0
  58. package/dist/components/rel-otp/rel-otp.ts +504 -0
  59. package/dist/components/rel-pagination/README.md +410 -0
  60. package/dist/components/rel-pagination/index.ts +1 -0
  61. package/dist/components/rel-pagination/rel-pagination.ts +381 -0
  62. package/dist/components/rel-popover/README.md +568 -0
  63. package/dist/components/rel-popover/index.ts +2 -0
  64. package/dist/components/rel-popover/rel-popover-content.ts +55 -0
  65. package/dist/components/rel-popover/rel-popover.ts +330 -0
  66. package/dist/components/rel-progress/README.md +110 -0
  67. package/dist/components/rel-progress/rel-progress.ts +32 -5
  68. package/dist/components/rel-radio/README.md +120 -0
  69. package/dist/components/rel-radio/rel-radio.ts +31 -6
  70. package/dist/components/rel-resizable/README.md +140 -0
  71. package/dist/components/rel-resizable/index.ts +3 -0
  72. package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
  73. package/dist/components/rel-resizable/rel-resizable-handle.ts +145 -0
  74. package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
  75. package/dist/components/rel-segmented-button/README.md +141 -0
  76. package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
  77. package/dist/components/rel-select/README.md +107 -0
  78. package/dist/components/rel-select/rel-select.ts +28 -14
  79. package/dist/components/rel-sheet/README.md +224 -0
  80. package/dist/components/rel-sheet/rel-sheet.ts +684 -0
  81. package/dist/components/rel-sidebar/README.md +209 -0
  82. package/dist/components/rel-sidebar/rel-sidebar.ts +1166 -0
  83. package/dist/components/rel-slider/README.md +135 -0
  84. package/dist/components/rel-slider/rel-slider.ts +44 -9
  85. package/dist/components/rel-snackbar/README.md +134 -0
  86. package/dist/components/rel-snackbar/rel-snackbar.ts +109 -14
  87. package/dist/components/rel-stepper/README.md +160 -0
  88. package/dist/components/rel-stepper/rel-stepper.ts +38 -27
  89. package/dist/components/rel-switch/README.md +102 -0
  90. package/dist/components/rel-switch/rel-switch.ts +34 -12
  91. package/dist/components/rel-tab/README.md +163 -0
  92. package/dist/components/rel-tab/rel-tab.ts +79 -23
  93. package/dist/components/rel-table/README.md +178 -0
  94. package/dist/components/rel-table/rel-table-body.ts +21 -0
  95. package/dist/components/rel-table/rel-table-caption.ts +27 -0
  96. package/dist/components/rel-table/rel-table-cell.ts +42 -0
  97. package/dist/components/rel-table/rel-table-footer.ts +23 -0
  98. package/dist/components/rel-table/rel-table-head.ts +45 -0
  99. package/dist/components/rel-table/rel-table-header.ts +21 -0
  100. package/dist/components/rel-table/rel-table-row.ts +36 -0
  101. package/dist/components/rel-table/rel-table.ts +53 -0
  102. package/dist/components/rel-textfield/README.md +90 -0
  103. package/dist/components/rel-textfield/rel-textfield.ts +55 -39
  104. package/dist/components/rel-toast/README.md +148 -0
  105. package/dist/components/rel-toast/rel-toast.ts +63 -24
  106. package/dist/components/rel-tooltip/README.md +103 -0
  107. package/dist/components/rel-tooltip/rel-tooltip.ts +123 -80
  108. package/dist/components/rel-topbar/rel-topbar-actions.ts +23 -0
  109. package/dist/components/rel-topbar/rel-topbar-brand.ts +46 -0
  110. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +33 -0
  111. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +28 -0
  112. package/dist/components/rel-topbar/rel-topbar-item.ts +315 -0
  113. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +23 -0
  114. package/dist/components/rel-topbar/rel-topbar-profile.ts +248 -0
  115. package/dist/components/rel-topbar/rel-topbar-utilities.ts +23 -0
  116. package/dist/components/rel-topbar/rel-topbar.ts +329 -0
  117. package/dist/index.d.mts +831 -7
  118. package/dist/index.d.ts +831 -7
  119. package/dist/index.js +9500 -1564
  120. package/dist/index.js.map +1 -1
  121. package/dist/index.mjs +9240 -1344
  122. package/dist/index.mjs.map +1 -1
  123. package/dist/styles/vars.css +187 -25
  124. package/dist/vars-TBB4BMSD.css +346 -0
  125. package/package.json +2 -1
  126. package/dist/vars-6ZIUPIU4.css +0 -184
@@ -0,0 +1,330 @@
1
+ import { LitElement, html, css } from "lit";
2
+ import { customElement, property, state, query } from "lit/decorators.js";
3
+
4
+ type SideType = "top" | "right" | "bottom" | "left";
5
+ type AlignType = "start" | "center" | "end";
6
+
7
+ @customElement("rel-popover")
8
+ export class RelPopover extends LitElement {
9
+ /* =======================
10
+ * PUBLIC API
11
+ * ======================= */
12
+
13
+ @property({ type: Boolean, reflect: true }) open?: boolean;
14
+ @property({ type: Boolean, attribute: "defaultopen" }) defaultOpen = false;
15
+ @property({ type: Boolean, reflect: true }) disabled = false;
16
+
17
+ @property({ type: String }) side: SideType = "bottom";
18
+ @property({ type: String }) align: AlignType = "center";
19
+ @property({ type: Number, attribute: "sideoffset" }) sideOffset = 8;
20
+ @property({ type: Number, attribute: "alignoffset" }) alignOffset = 0;
21
+
22
+ @property({ type: Boolean, attribute: "trapfocus" }) trapFocus = false;
23
+ @property({ type: Boolean, attribute: "closeonescape" }) closeOnEscape = true;
24
+ @property({ type: Boolean, attribute: "closeonoutsideclick" })
25
+ closeOnOutsideClick = true;
26
+
27
+ /* =======================
28
+ * INTERNAL STATE
29
+ * ======================= */
30
+
31
+ @state() private internalOpen = false;
32
+ @state() private contentStyle = "";
33
+
34
+ @query(".content-wrapper") private contentWrapper!: HTMLDivElement;
35
+
36
+ private triggerEl: HTMLElement | null = null;
37
+ private returnFocusEl: HTMLElement | null = null;
38
+ private uniqueId = `popover-${Math.random().toString(36).slice(2)}`;
39
+
40
+ /* =======================
41
+ * STYLES
42
+ * ======================= */
43
+
44
+ static styles = css`
45
+ :host {
46
+ display: inline-block;
47
+ position: relative;
48
+ }
49
+
50
+ :host([disabled]) {
51
+ pointer-events: none;
52
+ opacity: 0.6;
53
+ }
54
+
55
+ .content-wrapper {
56
+ position: fixed;
57
+ z-index: 1001;
58
+ display: none;
59
+ }
60
+
61
+ .content-wrapper[open] {
62
+ display: block;
63
+ }
64
+ `;
65
+
66
+ /* =======================
67
+ * LIFECYCLE
68
+ * ======================= */
69
+
70
+ connectedCallback() {
71
+ super.connectedCallback();
72
+ this.internalOpen = this.isControlled() ? !!this.open : this.defaultOpen;
73
+
74
+ // ONE interaction entry point — capture phase
75
+ this.addEventListener("pointerdown", this.onHostPointerDown, true);
76
+ }
77
+
78
+ disconnectedCallback() {
79
+ super.disconnectedCallback();
80
+ this.removeEventListener("pointerdown", this.onHostPointerDown, true);
81
+ this.removeGlobalListeners();
82
+ }
83
+
84
+ firstUpdated() {
85
+ // triggerEl is resolved via onTriggerSlotChange; call aria in case slot
86
+ // content was already assigned before firstUpdated
87
+ this.applyTriggerAria();
88
+ }
89
+
90
+ updated(changed: Map<string, unknown>) {
91
+ if (changed.has("open") && this.isControlled()) {
92
+ this.internalOpen = !!this.open;
93
+ }
94
+
95
+ if (changed.has("internalOpen") || changed.has("open")) {
96
+ this.applyTriggerAria();
97
+ this.activeOpen ? this.onOpen() : this.onClose();
98
+ }
99
+ }
100
+
101
+ /* =======================
102
+ * STATE HELPERS
103
+ * ======================= */
104
+
105
+ private isControlled() {
106
+ return this.open !== undefined;
107
+ }
108
+
109
+ private get activeOpen() {
110
+ return this.isControlled() ? !!this.open : this.internalOpen;
111
+ }
112
+
113
+ private setOpen(next: boolean) {
114
+ if (!this.isControlled()) {
115
+ this.internalOpen = next;
116
+ }
117
+
118
+ this.dispatchEvent(
119
+ new CustomEvent("rel-open-change", {
120
+ detail: { open: next },
121
+ bubbles: true,
122
+ composed: true,
123
+ }),
124
+ );
125
+ }
126
+
127
+ /* =======================
128
+ * POINTER HANDLING (THE FIX)
129
+ * ======================= */
130
+
131
+ private onHostPointerDown = (e: PointerEvent) => {
132
+ if (this.disabled) return;
133
+
134
+ const path = e.composedPath();
135
+
136
+ // Toggle ONLY if pointerdown originated from the trigger element
137
+ if (!this.triggerEl || !path.includes(this.triggerEl)) return;
138
+
139
+ // Prevent this interaction from becoming an "outside click"
140
+ e.preventDefault();
141
+ e.stopPropagation();
142
+
143
+ this.setOpen(!this.activeOpen);
144
+ };
145
+
146
+ /* =======================
147
+ * OPEN / CLOSE
148
+ * ======================= */
149
+
150
+ private onOpen() {
151
+ this.returnFocusEl =
152
+ (document.activeElement as HTMLElement) ?? this.triggerEl;
153
+
154
+ this.addGlobalListeners();
155
+
156
+ requestAnimationFrame(() => {
157
+ this.updatePosition();
158
+ this.focusFirst();
159
+ });
160
+ }
161
+
162
+ private onClose() {
163
+ this.removeGlobalListeners();
164
+ this.restoreFocus();
165
+ }
166
+
167
+ /* =======================
168
+ * GLOBAL LISTENERS
169
+ * ======================= */
170
+
171
+ private addGlobalListeners() {
172
+ if (this.closeOnOutsideClick) {
173
+ document.addEventListener("pointerdown", this.onOutsidePointerDown);
174
+ }
175
+
176
+ if (this.closeOnEscape || this.trapFocus) {
177
+ document.addEventListener("keydown", this.onKeyDown);
178
+ }
179
+
180
+ window.addEventListener("resize", this.updatePosition);
181
+ window.addEventListener("scroll", this.updatePosition, true);
182
+ }
183
+
184
+ private removeGlobalListeners() {
185
+ document.removeEventListener("pointerdown", this.onOutsidePointerDown);
186
+ document.removeEventListener("keydown", this.onKeyDown);
187
+ window.removeEventListener("resize", this.updatePosition);
188
+ window.removeEventListener("scroll", this.updatePosition, true);
189
+ }
190
+
191
+ private onOutsidePointerDown = (e: PointerEvent) => {
192
+ if (!this.activeOpen) return;
193
+
194
+ const path = e.composedPath();
195
+
196
+ if (
197
+ path.includes(this) ||
198
+ (this.triggerEl && path.includes(this.triggerEl)) ||
199
+ (this.contentWrapper && path.includes(this.contentWrapper))
200
+ ) {
201
+ return;
202
+ }
203
+
204
+ this.setOpen(false);
205
+ };
206
+
207
+ private onKeyDown = (e: KeyboardEvent) => {
208
+ if (!this.activeOpen) return;
209
+
210
+ if (e.key === "Escape" && this.closeOnEscape) {
211
+ e.preventDefault();
212
+ this.setOpen(false);
213
+ }
214
+
215
+ if (this.trapFocus && e.key === "Tab") {
216
+ this.trapFocusLoop(e);
217
+ }
218
+ };
219
+
220
+ /* =======================
221
+ * FOCUS
222
+ * ======================= */
223
+
224
+ private focusFirst() {
225
+ if (!this.contentWrapper) return;
226
+
227
+ const focusable = this.getFocusable(this.contentWrapper);
228
+ if (focusable[0]) {
229
+ focusable[0].focus();
230
+ } else {
231
+ this.contentWrapper.focus();
232
+ }
233
+ }
234
+
235
+ private restoreFocus() {
236
+ this.triggerEl?.focus();
237
+ }
238
+
239
+ private trapFocusLoop(e: KeyboardEvent) {
240
+ const focusable = this.getFocusable(this.contentWrapper);
241
+ if (focusable.length === 0) return;
242
+
243
+ const first = focusable[0];
244
+ const last = focusable[focusable.length - 1];
245
+
246
+ if (e.shiftKey && document.activeElement === first) {
247
+ e.preventDefault();
248
+ last.focus();
249
+ } else if (!e.shiftKey && document.activeElement === last) {
250
+ e.preventDefault();
251
+ first.focus();
252
+ }
253
+ }
254
+
255
+ private getFocusable(root: HTMLElement): HTMLElement[] {
256
+ return Array.from(
257
+ root.querySelectorAll<HTMLElement>(
258
+ 'a[href],button:not([disabled]),input:not([disabled]),[tabindex]:not([tabindex="-1"])',
259
+ ),
260
+ ).filter((el) => el.offsetWidth || el.offsetHeight);
261
+ }
262
+
263
+ /* =======================
264
+ * POSITIONING
265
+ * ======================= */
266
+
267
+ private updatePosition = () => {
268
+ if (!this.triggerEl || !this.contentWrapper) return;
269
+
270
+ this.contentWrapper.setAttribute("open", "");
271
+
272
+ const t = this.triggerEl.getBoundingClientRect();
273
+ const c = this.contentWrapper.getBoundingClientRect();
274
+
275
+ let top = t.bottom + this.sideOffset;
276
+ let left = t.left + t.width / 2 - c.width / 2;
277
+
278
+ if (this.side === "top") top = t.top - c.height - this.sideOffset;
279
+ if (this.side === "left") left = t.left - c.width - this.sideOffset;
280
+ if (this.side === "right") left = t.right + this.sideOffset;
281
+
282
+ top = Math.max(8, Math.min(top, window.innerHeight - c.height - 8));
283
+ left = Math.max(8, Math.min(left, window.innerWidth - c.width - 8));
284
+
285
+ this.contentStyle = `top:${top}px;left:${left}px;`;
286
+ };
287
+
288
+ /* =======================
289
+ * ARIA
290
+ * ======================= */
291
+
292
+ private onTriggerSlotChange(e: Event) {
293
+ const slot = e.target as HTMLSlotElement;
294
+ const first = slot.assignedElements({ flatten: true })[0] as HTMLElement | undefined;
295
+ this.triggerEl = first ?? null;
296
+ this.applyTriggerAria();
297
+ }
298
+
299
+ private applyTriggerAria() {
300
+ if (!this.triggerEl) return;
301
+ this.triggerEl.setAttribute("aria-haspopup", "dialog");
302
+ this.triggerEl.setAttribute("aria-expanded", String(this.activeOpen));
303
+ this.triggerEl.setAttribute("aria-controls", this.uniqueId);
304
+ }
305
+
306
+ /* =======================
307
+ * RENDER
308
+ * ======================= */
309
+
310
+ render() {
311
+ return html`
312
+ <slot @slotchange=${this.onTriggerSlotChange}></slot>
313
+
314
+ <div
315
+ id="${this.uniqueId}"
316
+ class="content-wrapper"
317
+ style="${this.contentStyle}"
318
+ ?open="${this.activeOpen}"
319
+ >
320
+ <slot name="content"></slot>
321
+ </div>
322
+ `;
323
+ }
324
+ }
325
+
326
+ declare global {
327
+ interface HTMLElementTagNameMap {
328
+ "rel-popover": RelPopover;
329
+ }
330
+ }
@@ -0,0 +1,110 @@
1
+ # rel-progress
2
+
3
+ ## Overview
4
+ A progress indicator component supporting four modes: linear-determinate, linear-indeterminate, circular-determinate, and circular-indeterminate. Features smooth animations, proper ARIA attributes, and respects user motion preferences by disabling animations when requested.
5
+
6
+ ## Usage
7
+ ```html
8
+ <!-- Linear progress (determinate) -->
9
+ <rel-progress mode="linear-determinate" value="45"></rel-progress>
10
+ <rel-progress mode="linear-determinate" value="100"></rel-progress>
11
+
12
+ <!-- Linear progress (indeterminate/loading) -->
13
+ <rel-progress mode="linear-indeterminate"></rel-progress>
14
+
15
+ <!-- Circular progress (determinate) -->
16
+ <rel-progress mode="circular-determinate" value="75"></rel-progress>
17
+
18
+ <!-- Circular progress (indeterminate/spinner) -->
19
+ <rel-progress mode="circular-indeterminate"></rel-progress>
20
+
21
+ <!-- Dynamic progress update -->
22
+ <rel-progress id="uploadProgress" mode="linear-determinate" value="0"></rel-progress>
23
+ <script>
24
+ const progress = document.getElementById('uploadProgress');
25
+ let value = 0;
26
+ const interval = setInterval(() => {
27
+ value += 10;
28
+ progress.value = value;
29
+ if (value >= 100) clearInterval(interval);
30
+ }, 500);
31
+ </script>
32
+ ```
33
+
34
+ ## API
35
+
36
+ ### Attributes/Properties
37
+ | Name | Type | Default | Description |
38
+ |------|------|---------|-------------|
39
+ | `mode` (reflected) | `"linear-determinate" \| "linear-indeterminate" \| "circular-determinate" \| "circular-indeterminate"` | `"linear-determinate"` | Progress indicator type |
40
+ | `value` | `number` | `0` | Progress value (0-100) for determinate modes (ignored in indeterminate modes) |
41
+
42
+ ## Events
43
+ None.
44
+
45
+ ## Slots
46
+ Default slot not used.
47
+
48
+ ## CSS Parts
49
+ None exposed.
50
+
51
+ ## Tokens / CSS Variables
52
+
53
+ ### Semantic Tokens
54
+ - `--rel-fontFamilies-primary` - Font family
55
+
56
+ ### Color Tokens
57
+ - `--rel-colors-gray-border` - Track/background color
58
+ - `--rel-colors-black` - Active progress bar color
59
+
60
+ ### Border Tokens
61
+ - `--rel-borders-radius-border-radius-200` - Linear track border radius
62
+
63
+ ## Accessibility
64
+ - Determinate modes use `role="progressbar"` with `aria-valuenow`, `aria-valuemin="0"`, `aria-valuemax="100"`
65
+ - Indeterminate modes use `role="progressbar"` without value attributes
66
+ - Respects `prefers-reduced-motion: reduce` by disabling animations
67
+
68
+ ### Keyboard Interactions
69
+ - None (not focusable)
70
+
71
+ ### Focus Behavior
72
+ - Not focusable
73
+
74
+ ## Notes / Edge Cases
75
+ - **Linear modes**:
76
+ - Height: `6px`
77
+ - Track uses gray border color
78
+ - Bar uses black color
79
+ - Border radius: `--rel-borders-radius-border-radius-200`
80
+ - **Determinate**: bar width transitions smoothly (0.3s ease)
81
+ - **Indeterminate**: 30% width bar slides left-to-right (1.5s infinite loop)
82
+
83
+ - **Circular modes**:
84
+ - Size: `48px × 48px` (viewBox varies by mode)
85
+ - Stroke width: `3px` (determinate) or `4px` (indeterminate)
86
+ - Stroke linecap: `round`
87
+ - **Determinate**:
88
+ - Uses `stroke-dasharray` and `stroke-dashoffset` to animate progress
89
+ - Circle radius: `15.9155`, circumference: `~100`
90
+ - Rotated -90° to start at top
91
+ - Progress transition: `0.35s`
92
+ - **Indeterminate (spinner)**:
93
+ - Rotates continuously (2s linear infinite)
94
+ - Stroke dash animates (1.5s ease-in-out infinite)
95
+
96
+ - **Value clamping**: Values are automatically clamped to 0-100 range via `safeValue` getter
97
+
98
+ - **Motion preferences**:
99
+ - When `prefers-reduced-motion: reduce`:
100
+ - Linear determinate: no transition
101
+ - Linear indeterminate: no animation, shows full-width bar at 50% opacity
102
+ - Circular determinate: no transition
103
+ - Circular indeterminate: no rotation/dash animation, shows static partial arc
104
+
105
+ - **Display modes**:
106
+ - Linear modes: `display: block` (full width)
107
+ - Circular modes: `display: inline-block` (sized to content)
108
+
109
+ - The component automatically switches render methods based on `mode` property
110
+ - Indeterminate modes ignore the `value` property
@@ -21,7 +21,7 @@ export class RelProgress extends LitElement {
21
21
  static styles = css`
22
22
  :host {
23
23
  display: block;
24
- font-family: var(--rel-fontFamilies-primary);
24
+ font-family: var(--rel-fontFamilies-primary, sans-serif);
25
25
  }
26
26
 
27
27
  :host([mode*="circular"]) {
@@ -30,8 +30,8 @@ export class RelProgress extends LitElement {
30
30
 
31
31
  /* Linear */
32
32
  .linear-track {
33
- background: var(--rel-colors-gray-border);
34
- border-radius: var(--rel-borders-radius-border-radius-200);
33
+ background: var(--rel-colors-gray-border, #ddd);
34
+ border-radius: var(--rel-borders-radius-border-radius-200, 8px);
35
35
  height: 6px;
36
36
  overflow: hidden;
37
37
  width: 100%;
@@ -118,6 +118,32 @@ export class RelProgress extends LitElement {
118
118
  stroke-dashoffset: -124;
119
119
  }
120
120
  }
121
+
122
+ @media (prefers-reduced-motion: reduce) {
123
+ .linear-bar {
124
+ transition: none;
125
+ }
126
+
127
+ .linear-bar.indeterminate {
128
+ animation: none;
129
+ width: 100%;
130
+ opacity: 0.5;
131
+ }
132
+
133
+ .spinner {
134
+ animation: none;
135
+ }
136
+
137
+ .spinner-path {
138
+ animation: none;
139
+ stroke-dasharray: 90, 150;
140
+ stroke-dashoffset: -35;
141
+ }
142
+
143
+ .circular-progress {
144
+ transition: none;
145
+ }
146
+ }
121
147
  `;
122
148
 
123
149
  private get safeValue() {
@@ -126,7 +152,8 @@ export class RelProgress extends LitElement {
126
152
 
127
153
  private renderLinearDeterminate() {
128
154
  return html`
129
- <div class="linear-track">
155
+ <div class="linear-track" role="progressbar"
156
+ aria-valuenow="${this.safeValue}" aria-valuemin="0" aria-valuemax="100">
130
157
  <div class="linear-bar" style="width: ${this.safeValue}%"></div>
131
158
  </div>
132
159
  `;
@@ -134,7 +161,7 @@ export class RelProgress extends LitElement {
134
161
 
135
162
  private renderLinearIndeterminate() {
136
163
  return html`
137
- <div class="linear-track">
164
+ <div class="linear-track" role="progressbar">
138
165
  <div class="linear-bar indeterminate"></div>
139
166
  </div>
140
167
  `;
@@ -0,0 +1,120 @@
1
+ # rel-radio
2
+
3
+ ## Overview
4
+ A radio button group component that allows users to select a single option from a list. Supports individual option states (disabled, error), supporting text, and comprehensive styling with design tokens.
5
+
6
+ ## Usage
7
+ ```html
8
+ <!-- Basic example -->
9
+ <rel-radio id="colorRadio" name="color-group" selectedValue="blue"></rel-radio>
10
+ <script>
11
+ const radio = document.getElementById('colorRadio');
12
+ radio.options = [
13
+ { value: 'red', label: 'Red' },
14
+ { value: 'blue', label: 'Blue' },
15
+ { value: 'green', label: 'Green' }
16
+ ];
17
+ </script>
18
+
19
+ <!-- Example with supporting text and states -->
20
+ <rel-radio id="planRadio" name="plan-group"></rel-radio>
21
+ <script>
22
+ const planRadio = document.getElementById('planRadio');
23
+ planRadio.options = [
24
+ { value: 'basic', label: 'Basic Plan', supportingText: '$9.99/month' },
25
+ { value: 'pro', label: 'Pro Plan', supportingText: '$19.99/month' },
26
+ { value: 'enterprise', label: 'Enterprise', supportingText: 'Contact sales', disabled: true }
27
+ ];
28
+ planRadio.selectedValue = 'basic';
29
+ </script>
30
+
31
+ <!-- Listen for changes -->
32
+ <rel-radio id="preferenceRadio"></rel-radio>
33
+ <script>
34
+ const prefRadio = document.getElementById('preferenceRadio');
35
+ prefRadio.options = [
36
+ { value: 'email', label: 'Email' },
37
+ { value: 'sms', label: 'SMS' }
38
+ ];
39
+ prefRadio.addEventListener('changed', (e) => {
40
+ console.log('Selected:', prefRadio.selectedValue);
41
+ });
42
+ </script>
43
+ ```
44
+
45
+ ## API
46
+
47
+ ### Attributes/Properties
48
+ | Name | Type | Default | Description |
49
+ |------|------|---------|-------------|
50
+ | `options` | `RadioOption[]` | `[]` | Array of radio options (see RadioOption interface below) |
51
+ | `name` | `string` | `"radio-group"` | Name attribute for the radio group (for form grouping) |
52
+ | `selectedValue` | `string \| null` | `null` | Currently selected option value (reflected attribute) |
53
+ | `disabled` | `boolean` | `false` | Disables the entire radio group |
54
+
55
+ ### RadioOption Interface
56
+ ```typescript
57
+ interface RadioOption {
58
+ value: string; // Unique value for this option
59
+ label: string; // Display label
60
+ supportingText?: string; // Optional helper text below the label
61
+ disabled?: boolean; // Disable individual option
62
+ error?: boolean; // Show error state for individual option
63
+ }
64
+ ```
65
+
66
+ ## Events
67
+ | Event | Detail | Description |
68
+ |-------|--------|-------------|
69
+ | `changed` | None (read `selectedValue` property) | Fired when the selected option changes |
70
+
71
+ ## Slots
72
+ Default slot not used.
73
+
74
+ ## CSS Parts
75
+ None exposed.
76
+
77
+ ## Tokens / CSS Variables
78
+
79
+ ### Semantic Tokens
80
+ - `--rel-fontFamilies-primary` - Font family
81
+ - `--rel-fontSizes-sm`, `--rel-fontSizes-xs` - Font sizes for label and supporting text
82
+ - `--rel-fontWeights-medium` - Font weight for labels
83
+
84
+ ### Spacing Tokens
85
+ - `--rel-spacing-space-xs` - Gap between radio options
86
+ - `--rel-spacing-space-3xs` - Gap within each option (label and supporting text)
87
+ - `--rel-spacing-space-2xs` - Gap between radio circle and label
88
+
89
+ ### Color Tokens
90
+ - `--rel-text-heading` - Radio button border and checked dot color
91
+ - `--rel-text-primary` - Label text color
92
+ - `--rel-text-placeholder` - Supporting text color
93
+ - `--rel-border-info` - Focus outline color
94
+ - `--rel-colors-red-secondary` - Error state border and dot color
95
+
96
+ ## Accessibility
97
+ - Uses visually hidden native radio inputs for proper screen reader support
98
+ - Custom visual radio circle with proper focus indicators
99
+ - Focus visible state with outline offset for keyboard navigation
100
+ - Label click targets the radio for larger interaction area
101
+ - Disabled state communicated through reduced opacity and cursor change
102
+ - Proper `name` grouping ensures only one option can be selected
103
+
104
+ ### Keyboard Interactions
105
+ - `Tab` - Move focus to/from radio group
106
+ - `Arrow Up/Down` - Navigate between radio options
107
+ - `Space` - Select focused option
108
+
109
+ ### Focus Behavior
110
+ - Focus outline appears with offset around the custom radio circle
111
+ - Uses info border color for high contrast
112
+
113
+ ## Notes / Edge Cases
114
+ - The `selectedValue` property is reflected as an attribute and can be set programmatically or via HTML
115
+ - When `disabled` is true on the component, all options are disabled regardless of individual option settings
116
+ - Individual options can be disabled via the `disabled` property in the `RadioOption` object
117
+ - The `error` property on `RadioOption` changes the radio button color to red but does not prevent selection
118
+ - Supporting text is optional and appears below the label with left margin alignment
119
+ - Native radio inputs are visually hidden but maintain all accessibility features
120
+ - The `name` attribute groups radios together for form submission and mutual exclusion