@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
@@ -0,0 +1,327 @@
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
+
39
+ /* =======================
40
+ * STYLES
41
+ * ======================= */
42
+
43
+ static styles = css`
44
+ :host {
45
+ display: inline-block;
46
+ position: relative;
47
+ }
48
+
49
+ :host([disabled]) {
50
+ pointer-events: none;
51
+ opacity: 0.6;
52
+ }
53
+
54
+ .content-wrapper {
55
+ position: fixed;
56
+ z-index: 1001;
57
+ display: none;
58
+ }
59
+
60
+ .content-wrapper[open] {
61
+ display: block;
62
+ }
63
+ `;
64
+
65
+ /* =======================
66
+ * LIFECYCLE
67
+ * ======================= */
68
+
69
+ connectedCallback() {
70
+ super.connectedCallback();
71
+ this.internalOpen = this.isControlled() ? !!this.open : this.defaultOpen;
72
+
73
+ // ONE interaction entry point — capture phase
74
+ this.addEventListener("pointerdown", this.onHostPointerDown, true);
75
+ }
76
+
77
+ disconnectedCallback() {
78
+ super.disconnectedCallback();
79
+ this.removeEventListener("pointerdown", this.onHostPointerDown, true);
80
+ this.removeGlobalListeners();
81
+ }
82
+
83
+ firstUpdated() {
84
+ // Resolve trigger once from light DOM
85
+ this.triggerEl = this.querySelector(
86
+ '[slot="trigger"]',
87
+ ) as HTMLElement | null;
88
+ this.applyTriggerAria();
89
+ }
90
+
91
+ updated(changed: Map<string, unknown>) {
92
+ if (changed.has("open") && this.isControlled()) {
93
+ this.internalOpen = !!this.open;
94
+ }
95
+
96
+ if (changed.has("internalOpen") || changed.has("open")) {
97
+ this.applyTriggerAria();
98
+ this.activeOpen ? this.onOpen() : this.onClose();
99
+ }
100
+ }
101
+
102
+ /* =======================
103
+ * STATE HELPERS
104
+ * ======================= */
105
+
106
+ private isControlled() {
107
+ return this.open !== undefined;
108
+ }
109
+
110
+ private get activeOpen() {
111
+ return this.isControlled() ? !!this.open : this.internalOpen;
112
+ }
113
+
114
+ private setOpen(next: boolean) {
115
+ if (!this.isControlled()) {
116
+ this.internalOpen = next;
117
+ }
118
+
119
+ this.dispatchEvent(
120
+ new CustomEvent("rel-open-change", {
121
+ detail: { open: next },
122
+ bubbles: true,
123
+ composed: true,
124
+ }),
125
+ );
126
+ }
127
+
128
+ /* =======================
129
+ * POINTER HANDLING (THE FIX)
130
+ * ======================= */
131
+
132
+ private onHostPointerDown = (e: PointerEvent) => {
133
+ if (this.disabled) return;
134
+
135
+ const path = e.composedPath();
136
+
137
+ // Toggle ONLY if pointerdown originated from trigger
138
+ const triggerNode = path.find(
139
+ (n) => n instanceof HTMLElement && n.getAttribute?.("slot") === "trigger",
140
+ ) as HTMLElement | undefined;
141
+
142
+ if (!triggerNode) return;
143
+
144
+ // Prevent this interaction from becoming an "outside click"
145
+ e.preventDefault();
146
+ e.stopPropagation();
147
+
148
+ this.triggerEl = triggerNode;
149
+ this.setOpen(!this.activeOpen);
150
+ };
151
+
152
+ /* =======================
153
+ * OPEN / CLOSE
154
+ * ======================= */
155
+
156
+ private onOpen() {
157
+ this.returnFocusEl =
158
+ (document.activeElement as HTMLElement) ?? this.triggerEl;
159
+
160
+ this.addGlobalListeners();
161
+
162
+ requestAnimationFrame(() => {
163
+ this.updatePosition();
164
+ this.focusFirst();
165
+ });
166
+ }
167
+
168
+ private onClose() {
169
+ this.removeGlobalListeners();
170
+ this.restoreFocus();
171
+ }
172
+
173
+ /* =======================
174
+ * GLOBAL LISTENERS
175
+ * ======================= */
176
+
177
+ private addGlobalListeners() {
178
+ if (this.closeOnOutsideClick) {
179
+ document.addEventListener("pointerdown", this.onOutsidePointerDown);
180
+ }
181
+
182
+ if (this.closeOnEscape || this.trapFocus) {
183
+ document.addEventListener("keydown", this.onKeyDown);
184
+ }
185
+
186
+ window.addEventListener("resize", this.updatePosition);
187
+ window.addEventListener("scroll", this.updatePosition, true);
188
+ }
189
+
190
+ private removeGlobalListeners() {
191
+ document.removeEventListener("pointerdown", this.onOutsidePointerDown);
192
+ document.removeEventListener("keydown", this.onKeyDown);
193
+ window.removeEventListener("resize", this.updatePosition);
194
+ window.removeEventListener("scroll", this.updatePosition, true);
195
+ }
196
+
197
+ private onOutsidePointerDown = (e: PointerEvent) => {
198
+ if (!this.activeOpen) return;
199
+
200
+ const path = e.composedPath();
201
+
202
+ if (
203
+ path.includes(this) ||
204
+ (this.triggerEl && path.includes(this.triggerEl)) ||
205
+ (this.contentWrapper && path.includes(this.contentWrapper))
206
+ ) {
207
+ return;
208
+ }
209
+
210
+ this.setOpen(false);
211
+ };
212
+
213
+ private onKeyDown = (e: KeyboardEvent) => {
214
+ if (!this.activeOpen) return;
215
+
216
+ if (e.key === "Escape" && this.closeOnEscape) {
217
+ e.preventDefault();
218
+ this.setOpen(false);
219
+ }
220
+
221
+ if (this.trapFocus && e.key === "Tab") {
222
+ this.trapFocusLoop(e);
223
+ }
224
+ };
225
+
226
+ /* =======================
227
+ * FOCUS
228
+ * ======================= */
229
+
230
+ private focusFirst() {
231
+ if (!this.contentWrapper) return;
232
+
233
+ const focusable = this.getFocusable(this.contentWrapper);
234
+ if (focusable[0]) {
235
+ focusable[0].focus();
236
+ } else {
237
+ this.contentWrapper.focus();
238
+ }
239
+ }
240
+
241
+ private restoreFocus() {
242
+ this.triggerEl?.focus();
243
+ }
244
+
245
+ private trapFocusLoop(e: KeyboardEvent) {
246
+ const focusable = this.getFocusable(this.contentWrapper);
247
+ if (focusable.length === 0) return;
248
+
249
+ const first = focusable[0];
250
+ const last = focusable[focusable.length - 1];
251
+
252
+ if (e.shiftKey && document.activeElement === first) {
253
+ e.preventDefault();
254
+ last.focus();
255
+ } else if (!e.shiftKey && document.activeElement === last) {
256
+ e.preventDefault();
257
+ first.focus();
258
+ }
259
+ }
260
+
261
+ private getFocusable(root: HTMLElement): HTMLElement[] {
262
+ return Array.from(
263
+ root.querySelectorAll<HTMLElement>(
264
+ 'a[href],button:not([disabled]),input:not([disabled]),[tabindex]:not([tabindex="-1"])',
265
+ ),
266
+ ).filter((el) => el.offsetWidth || el.offsetHeight);
267
+ }
268
+
269
+ /* =======================
270
+ * POSITIONING
271
+ * ======================= */
272
+
273
+ private updatePosition = () => {
274
+ if (!this.triggerEl || !this.contentWrapper) return;
275
+
276
+ this.contentWrapper.setAttribute("open", "");
277
+
278
+ const t = this.triggerEl.getBoundingClientRect();
279
+ const c = this.contentWrapper.getBoundingClientRect();
280
+
281
+ let top = t.bottom + this.sideOffset;
282
+ let left = t.left + t.width / 2 - c.width / 2;
283
+
284
+ if (this.side === "top") top = t.top - c.height - this.sideOffset;
285
+ if (this.side === "left") left = t.left - c.width - this.sideOffset;
286
+ if (this.side === "right") left = t.right + this.sideOffset;
287
+
288
+ top = Math.max(8, Math.min(top, window.innerHeight - c.height - 8));
289
+ left = Math.max(8, Math.min(left, window.innerWidth - c.width - 8));
290
+
291
+ this.contentStyle = `top:${top}px;left:${left}px;`;
292
+ };
293
+
294
+ /* =======================
295
+ * ARIA
296
+ * ======================= */
297
+
298
+ private applyTriggerAria() {
299
+ if (!this.triggerEl) return;
300
+ this.triggerEl.setAttribute("aria-haspopup", "dialog");
301
+ this.triggerEl.setAttribute("aria-expanded", String(this.activeOpen));
302
+ }
303
+
304
+ /* =======================
305
+ * RENDER
306
+ * ======================= */
307
+
308
+ render() {
309
+ return html`
310
+ <slot name="trigger"></slot>
311
+
312
+ <div
313
+ class="content-wrapper"
314
+ style="${this.contentStyle}"
315
+ ?open="${this.activeOpen}"
316
+ >
317
+ <slot></slot>
318
+ </div>
319
+ `;
320
+ }
321
+ }
322
+
323
+ declare global {
324
+ interface HTMLElementTagNameMap {
325
+ "rel-popover": RelPopover;
326
+ }
327
+ }
@@ -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
@@ -9,9 +9,11 @@ export type ProgressMode =
9
9
 
10
10
  @customElement("rel-progress")
11
11
  export class RelProgress extends LitElement {
12
- @property({ type: String }) mode: ProgressMode = "linear-determinate";
12
+ @property({ type: String, reflect: true })
13
+ mode: ProgressMode = "linear-determinate";
13
14
 
14
- @property({ type: Number }) value: number = 0;
15
+ @property({ type: Number })
16
+ value = 0;
15
17
 
16
18
  private radius = 15.9155;
17
19
  private circumference = 2 * Math.PI * this.radius;
@@ -19,17 +21,17 @@ export class RelProgress extends LitElement {
19
21
  static styles = css`
20
22
  :host {
21
23
  display: block;
22
- font-family: var(--rel-fontFamilies-primary);
24
+ font-family: var(--rel-fontFamilies-primary, sans-serif);
23
25
  }
24
26
 
25
27
  :host([mode*="circular"]) {
26
28
  display: inline-block;
27
29
  }
28
30
 
29
- /* Linear Progress */
31
+ /* Linear */
30
32
  .linear-track {
31
- background: var(--rel-colors-gray-border);
32
- 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);
33
35
  height: 6px;
34
36
  overflow: hidden;
35
37
  width: 100%;
@@ -58,7 +60,7 @@ export class RelProgress extends LitElement {
58
60
  }
59
61
  }
60
62
 
61
- /* Circular Progress */
63
+ /* Circular */
62
64
  .circular {
63
65
  display: inline-block;
64
66
  width: 48px;
@@ -83,11 +85,9 @@ export class RelProgress extends LitElement {
83
85
  stroke-width: 3;
84
86
  stroke-linecap: round;
85
87
  transition: stroke-dashoffset 0.35s;
86
- transform: rotate(0deg);
87
- transform-origin: center;
88
88
  }
89
89
 
90
- /* Circular Indeterminate Spinner */
90
+ /* Spinner */
91
91
  .spinner {
92
92
  animation: rotate 2s linear infinite;
93
93
  }
@@ -118,38 +118,71 @@ 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
 
149
+ private get safeValue() {
150
+ return Math.min(100, Math.max(0, this.value));
151
+ }
152
+
123
153
  private renderLinearDeterminate() {
124
154
  return html`
125
- <div class="linear-track">
126
- <div class="linear-bar" style="width: ${this.value}%"></div>
155
+ <div class="linear-track" role="progressbar"
156
+ aria-valuenow="${this.safeValue}" aria-valuemin="0" aria-valuemax="100">
157
+ <div class="linear-bar" style="width: ${this.safeValue}%"></div>
127
158
  </div>
128
159
  `;
129
160
  }
130
161
 
131
162
  private renderLinearIndeterminate() {
132
163
  return html`
133
- <div class="linear-track">
164
+ <div class="linear-track" role="progressbar">
134
165
  <div class="linear-bar indeterminate"></div>
135
166
  </div>
136
167
  `;
137
168
  }
138
169
 
139
170
  private renderCircularDeterminate() {
140
- const offset = this.circumference - (this.value / 100) * this.circumference;
171
+ const offset =
172
+ this.circumference - (this.safeValue / 100) * this.circumference;
141
173
 
142
174
  return html`
143
175
  <div class="circular">
144
176
  <svg viewBox="0 0 36 36" class="circular-svg">
145
- <circle class="circular-bg" cx="18" cy="18" r="15.9155" />
177
+ <circle class="circular-bg" cx="18" cy="18" r="${this.radius}" />
146
178
  <circle
147
179
  class="circular-progress"
148
180
  cx="18"
149
181
  cy="18"
150
- r="15.9155"
182
+ r="${this.radius}"
151
183
  stroke-dasharray="${this.circumference}"
152
- stroke-dashoffset="${offset}" />
184
+ stroke-dashoffset="${offset}"
185
+ />
153
186
  </svg>
154
187
  </div>
155
188
  `;
@@ -159,7 +192,14 @@ export class RelProgress extends LitElement {
159
192
  return html`
160
193
  <div class="circular">
161
194
  <svg viewBox="0 0 50 50" class="spinner">
162
- <circle class="spinner-path" cx="25" cy="25" r="20" fill="none" stroke-width="4" />
195
+ <circle
196
+ class="spinner-path"
197
+ cx="25"
198
+ cy="25"
199
+ r="20"
200
+ fill="none"
201
+ stroke-width="4"
202
+ />
163
203
  </svg>
164
204
  </div>
165
205
  `;
@@ -167,8 +207,6 @@ export class RelProgress extends LitElement {
167
207
 
168
208
  render() {
169
209
  switch (this.mode) {
170
- case "linear-determinate":
171
- return this.renderLinearDeterminate();
172
210
  case "linear-indeterminate":
173
211
  return this.renderLinearIndeterminate();
174
212
  case "circular-determinate":