@reliability-design/web 1.0.3 → 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 (114) hide show
  1. package/dist/components/rel-accordion/README.md +132 -0
  2. package/dist/components/rel-accordion/rel-accordion.ts +43 -16
  3. package/dist/components/rel-alert/README.md +141 -0
  4. package/dist/components/rel-alert/rel-alert.ts +18 -7
  5. package/dist/components/rel-avatar/README.md +119 -0
  6. package/dist/components/rel-badge/README.md +86 -0
  7. package/dist/components/rel-badge/rel-badge.ts +6 -6
  8. package/dist/components/rel-breadcrumb/README.md +102 -0
  9. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
  10. package/dist/components/rel-button/README.md +130 -0
  11. package/dist/components/rel-calendar/README.md +109 -0
  12. package/dist/components/rel-calendar/index.ts +1 -0
  13. package/dist/components/rel-calendar/rel-calendar.ts +467 -0
  14. package/dist/components/rel-card/README.md +94 -0
  15. package/dist/components/rel-card/rel-card.ts +12 -3
  16. package/dist/components/rel-cardmedia/README.md +127 -0
  17. package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
  18. package/dist/components/rel-carousel/README.md +166 -0
  19. package/dist/components/rel-carousel/index.ts +1 -0
  20. package/dist/components/rel-carousel/rel-carousel.ts +469 -0
  21. package/dist/components/rel-chart/README.md +286 -0
  22. package/dist/components/rel-chart/index.ts +1 -0
  23. package/dist/components/rel-chart/rel-chart.ts +736 -0
  24. package/dist/components/rel-checkbox/README.md +77 -0
  25. package/dist/components/rel-checkbox/rel-checkbox.ts +18 -10
  26. package/dist/components/rel-chip/README.md +114 -0
  27. package/dist/components/rel-chip/rel-chip.ts +20 -4
  28. package/dist/components/rel-container/README.md +99 -0
  29. package/dist/components/rel-date-picker/README.md +253 -0
  30. package/dist/components/rel-date-picker/index.ts +1 -0
  31. package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
  32. package/dist/components/rel-divider/README.md +100 -0
  33. package/dist/components/rel-drawer/README.md +200 -0
  34. package/dist/components/rel-drawer/index.ts +1 -0
  35. package/dist/components/rel-drawer/rel-drawer.ts +621 -0
  36. package/dist/components/rel-kbd/README.md +151 -0
  37. package/dist/components/rel-kbd/rel-kbd.ts +118 -0
  38. package/dist/components/rel-layout/README.md +88 -0
  39. package/dist/components/rel-menu/README.md +151 -0
  40. package/dist/components/rel-menu/rel-menu.ts +18 -10
  41. package/dist/components/rel-menubar/README.md +329 -0
  42. package/dist/components/rel-menubar/index.ts +2 -0
  43. package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
  44. package/dist/components/rel-menubar/rel-menubar.ts +576 -0
  45. package/dist/components/rel-nav-menu/README.md +289 -0
  46. package/dist/components/rel-nav-menu/index.ts +3 -0
  47. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +143 -0
  48. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
  49. package/dist/components/rel-nav-menu/rel-nav-menu.ts +331 -0
  50. package/dist/components/rel-navbar/README.md +126 -0
  51. package/dist/components/rel-navbar/rel-navbar.ts +91 -0
  52. package/dist/components/rel-otp/README.md +440 -0
  53. package/dist/components/rel-otp/index.ts +1 -0
  54. package/dist/components/rel-otp/rel-otp.ts +504 -0
  55. package/dist/components/rel-pagination/README.md +410 -0
  56. package/dist/components/rel-pagination/index.ts +1 -0
  57. package/dist/components/rel-pagination/rel-pagination.ts +376 -0
  58. package/dist/components/rel-popover/README.md +568 -0
  59. package/dist/components/rel-popover/index.ts +2 -0
  60. package/dist/components/rel-popover/rel-popover-content.ts +55 -0
  61. package/dist/components/rel-popover/rel-popover.ts +327 -0
  62. package/dist/components/rel-progress/README.md +110 -0
  63. package/dist/components/rel-progress/rel-progress.ts +32 -5
  64. package/dist/components/rel-quote-page/README.md +166 -0
  65. package/dist/components/rel-quote-page/rel-quote-page.ts +816 -0
  66. package/dist/components/rel-radio/README.md +120 -0
  67. package/dist/components/rel-radio/rel-radio.ts +13 -5
  68. package/dist/components/rel-resizable/README.md +140 -0
  69. package/dist/components/rel-resizable/index.ts +3 -0
  70. package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
  71. package/dist/components/rel-resizable/rel-resizable-handle.ts +149 -0
  72. package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
  73. package/dist/components/rel-segmented-button/README.md +141 -0
  74. package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
  75. package/dist/components/rel-select/README.md +107 -0
  76. package/dist/components/rel-select/rel-select.ts +14 -14
  77. package/dist/components/rel-sheet/README.md +224 -0
  78. package/dist/components/rel-sheet/rel-sheet.ts +683 -0
  79. package/dist/components/rel-sidebar/README.md +209 -0
  80. package/dist/components/rel-sidebar/rel-sidebar.ts +1158 -0
  81. package/dist/components/rel-slider/README.md +135 -0
  82. package/dist/components/rel-slider/rel-slider.ts +19 -7
  83. package/dist/components/rel-snackbar/README.md +134 -0
  84. package/dist/components/rel-snackbar/rel-snackbar.ts +67 -2
  85. package/dist/components/rel-stepper/README.md +160 -0
  86. package/dist/components/rel-stepper/rel-stepper.ts +6 -6
  87. package/dist/components/rel-switch/README.md +102 -0
  88. package/dist/components/rel-switch/rel-switch.ts +15 -6
  89. package/dist/components/rel-tab/README.md +163 -0
  90. package/dist/components/rel-tab/rel-tab.ts +36 -14
  91. package/dist/components/rel-table/README.md +178 -0
  92. package/dist/components/rel-table/rel-table-body.ts +16 -0
  93. package/dist/components/rel-table/rel-table-caption.ts +22 -0
  94. package/dist/components/rel-table/rel-table-cell.ts +49 -0
  95. package/dist/components/rel-table/rel-table-footer.ts +18 -0
  96. package/dist/components/rel-table/rel-table-head.ts +33 -0
  97. package/dist/components/rel-table/rel-table-header.ts +16 -0
  98. package/dist/components/rel-table/rel-table-row.ts +32 -0
  99. package/dist/components/rel-table/rel-table.ts +47 -0
  100. package/dist/components/rel-textfield/README.md +90 -0
  101. package/dist/components/rel-textfield/rel-textfield.ts +35 -19
  102. package/dist/components/rel-toast/README.md +148 -0
  103. package/dist/components/rel-toast/rel-toast.ts +32 -14
  104. package/dist/components/rel-tooltip/README.md +103 -0
  105. package/dist/components/rel-tooltip/rel-tooltip.ts +9 -9
  106. package/dist/index.d.mts +836 -1
  107. package/dist/index.d.ts +836 -1
  108. package/dist/index.js +10658 -1493
  109. package/dist/index.js.map +1 -1
  110. package/dist/index.mjs +10616 -1499
  111. package/dist/index.mjs.map +1 -1
  112. package/dist/styles/vars.css +293 -183
  113. package/dist/{vars-6ZIUPIU4.css → vars-ITL2K3KO.css} +293 -183
  114. package/package.json +1 -1
@@ -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
@@ -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,166 @@
1
+ # rel-quote-page
2
+
3
+ ## Overview
4
+ A comprehensive freight shipping quote form component with all necessary fields for LTL (Less Than Truckload) shipping quotes. Features two main sections (Shipping Details and Commodity Details) with 24+ form fields including location search, service level tabs, radio groups, dropdowns, dimension inputs, and date picker. Automatically dispatches `form-change` events for external form handling.
5
+
6
+ ## Usage
7
+ ```html
8
+ <!-- Basic usage -->
9
+ <rel-quote-page></rel-quote-page>
10
+
11
+ <!-- With form change listener -->
12
+ <rel-quote-page id="quoteForm"></rel-quote-page>
13
+ <script>
14
+ const form = document.getElementById('quoteForm');
15
+ form.addEventListener('form-change', (e) => {
16
+ console.log('Form data:', e.detail);
17
+ // e.detail contains the complete QuoteFormData object
18
+ });
19
+ </script>
20
+
21
+ <!-- Programmatically access form data -->
22
+ <rel-quote-page id="quote"></rel-quote-page>
23
+ <script>
24
+ const quote = document.getElementById('quote');
25
+ // Access form data via the formData property
26
+ console.log(quote.formData.pickupLocation);
27
+ console.log(quote.formData.serviceLevel);
28
+ console.log(quote.formData.totalWeight);
29
+ </script>
30
+ ```
31
+
32
+ ## API
33
+
34
+ ### Properties
35
+ All form data is managed internally via a reactive `formData` state property (not directly settable). Access it via the `formData` property or the `form-change` event.
36
+
37
+ ## Events
38
+ | Event | Detail | Description |
39
+ |-------|--------|-------------|
40
+ | `form-change` | `QuoteFormData` | Fired whenever any form field changes |
41
+
42
+ ## QuoteFormData Interface
43
+ ```typescript
44
+ interface QuoteFormData {
45
+ // Shipping Details
46
+ pickupLocation: string;
47
+ dropoffLocation: string;
48
+ serviceLevel: "standard" | "guaranteed";
49
+ originType: "commercial" | "residential";
50
+ destinationType: "commercial" | "residential";
51
+ loadingRequirements: string;
52
+ deliveryRequirements: string;
53
+ loadingLimitedAccessType: string;
54
+ deliveryLimitedAccessType: string;
55
+ pickupDate: string; // Date in YYYY-MM-DD format
56
+
57
+ // Commodity Details
58
+ isHazmat: boolean;
59
+ commodity: string;
60
+ commodityDescription: string;
61
+ handlingUnits: number;
62
+ handlingType: string;
63
+ length: number; // inches
64
+ width: number; // inches
65
+ height: number; // inches
66
+ totalWeight: number; // lbs
67
+ class: string; // freight class
68
+ insuredValue: number;
69
+ nmfcNumber: string;
70
+ }
71
+ ```
72
+
73
+ ## Slots
74
+ None. This component provides a complete, self-contained form UI.
75
+
76
+ ## CSS Parts
77
+ None exposed.
78
+
79
+ ## Tokens / CSS Variables
80
+
81
+ ### Semantic Tokens
82
+ - `--rel-fontFamilies-primary` - Font family
83
+
84
+ ### Font Tokens
85
+ - `--rel-fontSizes-xl` - Section titles (24px)
86
+ - `--rel-fontSizes-lg` - Large text
87
+ - `--rel-fontSizes-md` - Subsection titles (16px)
88
+ - `--rel-fontSizes-sm` - Field labels, inputs (14px)
89
+ - `--rel-fontSizes-xs` - Helper text, dimension labels (12px)
90
+ - `--rel-fontWeights-bold` - Section titles
91
+ - `--rel-fontWeights-semibold` - Subsection titles
92
+ - `--rel-fontWeights-medium` - Field labels, radio labels
93
+
94
+ ### Color Tokens
95
+ - `--rel-colors-white` - Component background, input backgrounds, tab background
96
+ - `--rel-colors-black` - Section title, active tab background, radio selected, input focus border
97
+ - `--rel-colors-green-primary` - Subsection title bullet
98
+ - `--rel-colors-red-primary` - Required field asterisks
99
+ - `--rel-colors-gray-heading` - Field labels, subsection titles
100
+ - `--rel-colors-gray-primary` - Input text, radio labels
101
+ - `--rel-colors-gray-placeholder` - Helper text
102
+ - `--rel-colors-gray-border` - Input borders, tab borders
103
+
104
+ ### Border Tokens
105
+ - `--rel-borders-radius-border-radius-300` - Component and input border radius (16px/similar)
106
+
107
+ ### Spacing Tokens
108
+ - `--rel-spacing-space-xl` - Component padding
109
+ - `--rel-spacing-space-2xl` - Section bottom margin
110
+ - `--rel-spacing-space-lg` - Section row gap, field bottom margin
111
+ - `--rel-spacing-space-md` - Subsection margins, field group gap, service level tab bottom margin
112
+ - `--rel-spacing-space-sm` - Input padding, radio group gap
113
+ - `--rel-spacing-space-xs` - Tab padding, dimension input padding
114
+ - `--rel-spacing-space-2xs` - Field label-input gap
115
+ - `--rel-spacing-space-3xs` - Info icon gap, dimension label gap
116
+
117
+ ## Accessibility
118
+ - All required fields are marked with a red asterisk and `required` class
119
+ - Input fields have associated `<label>` elements
120
+ - Radio groups use custom styled divs (not native radios) but support click interaction
121
+ - Helper text provides additional context for complex fields
122
+ - Info icons (`ⓘ`) use `cursor: help` to indicate additional information
123
+
124
+ ### Keyboard Interactions
125
+ - Standard form navigation (Tab, Shift+Tab)
126
+ - Native input keyboard support (text, number, date, select)
127
+ - Service level tabs are `<button>` elements and keyboard accessible
128
+
129
+ ### Focus Behavior
130
+ - Inputs show focus border via `:focus-visible` (black border)
131
+ - Standard browser focus behavior for all form controls
132
+
133
+ ## Notes / Edge Cases
134
+ - **Form sections**:
135
+ 1. **Shipping Details**: Pickup/dropoff locations, service level, origin/destination types, loading/delivery requirements, limited access types, pickup date
136
+ 2. **Commodity Details**: Hazmat status, commodity info, handling units/type, dimensions (L/W/H), total weight, class, insured value, NMFC number
137
+
138
+ - **Service Level**: Toggle between "Standard" and "Guaranteed" using tab buttons (black background when active)
139
+
140
+ - **Radio groups**: Custom styled (not native `<input type="radio">`) using divs with visual indicators
141
+
142
+ - **Dimension inputs**: Grid layout with 4 columns (Length, Width, Height, Total Weight)
143
+ - Width and Height show "96\" max" helper text
144
+ - Inputs show unit labels (inches, lbs)
145
+
146
+ - **Responsive design**: Two-column grid on desktop (`grid-template-columns: 1fr 1fr`), single column on mobile (≤768px)
147
+
148
+ - **Location inputs**: Include search icon emoji (🔍) on the right side
149
+
150
+ - **Helper text**: Loading Requirements field includes detailed explanation about inside pickup/delivery and liftgate requirements
151
+
152
+ - **Info icons**: Small circular icons with `ⓘ` character, `cursor: help`
153
+
154
+ - **Subsection titles**: Include a green circular bullet (`::before` pseudo-element)
155
+
156
+ - **Dropdowns**: Custom styled `<select>` with SVG dropdown arrow (appearance: none)
157
+
158
+ - The component has max-width of 900px and is centered with `margin: 0 auto`
159
+
160
+ - Box shadow and border radius give the component a card-like appearance
161
+
162
+ - All form updates trigger the `form-change` event with the complete `formData` object
163
+
164
+ - Number inputs use `parseInt()` or `parseFloat()` with fallback to 0 for invalid values
165
+
166
+ - Date input is native HTML5 `type="date"`