@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.
- package/dist/components/rel-accordion/README.md +132 -0
- package/dist/components/rel-accordion/rel-accordion.ts +43 -16
- package/dist/components/rel-alert/README.md +141 -0
- package/dist/components/rel-alert/rel-alert.ts +18 -7
- package/dist/components/rel-avatar/README.md +119 -0
- package/dist/components/rel-badge/README.md +86 -0
- package/dist/components/rel-badge/rel-badge.ts +6 -6
- package/dist/components/rel-breadcrumb/README.md +102 -0
- package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
- package/dist/components/rel-button/README.md +130 -0
- package/dist/components/rel-calendar/README.md +109 -0
- package/dist/components/rel-calendar/index.ts +1 -0
- package/dist/components/rel-calendar/rel-calendar.ts +467 -0
- package/dist/components/rel-card/README.md +94 -0
- package/dist/components/rel-card/rel-card.ts +12 -3
- package/dist/components/rel-cardmedia/README.md +127 -0
- package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
- package/dist/components/rel-carousel/README.md +166 -0
- package/dist/components/rel-carousel/index.ts +1 -0
- package/dist/components/rel-carousel/rel-carousel.ts +469 -0
- package/dist/components/rel-chart/README.md +286 -0
- package/dist/components/rel-chart/index.ts +1 -0
- package/dist/components/rel-chart/rel-chart.ts +736 -0
- package/dist/components/rel-checkbox/README.md +77 -0
- package/dist/components/rel-checkbox/rel-checkbox.ts +18 -10
- package/dist/components/rel-chip/README.md +114 -0
- package/dist/components/rel-chip/rel-chip.ts +20 -4
- package/dist/components/rel-container/README.md +99 -0
- package/dist/components/rel-date-picker/README.md +253 -0
- package/dist/components/rel-date-picker/index.ts +1 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
- package/dist/components/rel-divider/README.md +100 -0
- package/dist/components/rel-drawer/README.md +200 -0
- package/dist/components/rel-drawer/index.ts +1 -0
- package/dist/components/rel-drawer/rel-drawer.ts +621 -0
- package/dist/components/rel-kbd/README.md +151 -0
- package/dist/components/rel-kbd/rel-kbd.ts +118 -0
- package/dist/components/rel-layout/README.md +88 -0
- package/dist/components/rel-menu/README.md +151 -0
- package/dist/components/rel-menu/rel-menu.ts +18 -10
- package/dist/components/rel-menubar/README.md +329 -0
- package/dist/components/rel-menubar/index.ts +2 -0
- package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
- package/dist/components/rel-menubar/rel-menubar.ts +576 -0
- package/dist/components/rel-nav-menu/README.md +289 -0
- package/dist/components/rel-nav-menu/index.ts +3 -0
- package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +143 -0
- package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +331 -0
- package/dist/components/rel-navbar/README.md +126 -0
- package/dist/components/rel-navbar/rel-navbar.ts +91 -0
- package/dist/components/rel-otp/README.md +440 -0
- package/dist/components/rel-otp/index.ts +1 -0
- package/dist/components/rel-otp/rel-otp.ts +504 -0
- package/dist/components/rel-pagination/README.md +410 -0
- package/dist/components/rel-pagination/index.ts +1 -0
- package/dist/components/rel-pagination/rel-pagination.ts +376 -0
- package/dist/components/rel-popover/README.md +568 -0
- package/dist/components/rel-popover/index.ts +2 -0
- package/dist/components/rel-popover/rel-popover-content.ts +55 -0
- package/dist/components/rel-popover/rel-popover.ts +327 -0
- package/dist/components/rel-progress/README.md +110 -0
- package/dist/components/rel-progress/rel-progress.ts +32 -5
- package/dist/components/rel-quote-page/README.md +166 -0
- package/dist/components/rel-quote-page/rel-quote-page.ts +816 -0
- package/dist/components/rel-radio/README.md +120 -0
- package/dist/components/rel-radio/rel-radio.ts +13 -5
- package/dist/components/rel-resizable/README.md +140 -0
- package/dist/components/rel-resizable/index.ts +3 -0
- package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
- package/dist/components/rel-resizable/rel-resizable-handle.ts +149 -0
- package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
- package/dist/components/rel-segmented-button/README.md +141 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
- package/dist/components/rel-select/README.md +107 -0
- package/dist/components/rel-select/rel-select.ts +14 -14
- package/dist/components/rel-sheet/README.md +224 -0
- package/dist/components/rel-sheet/rel-sheet.ts +683 -0
- package/dist/components/rel-sidebar/README.md +209 -0
- package/dist/components/rel-sidebar/rel-sidebar.ts +1158 -0
- package/dist/components/rel-slider/README.md +135 -0
- package/dist/components/rel-slider/rel-slider.ts +19 -7
- package/dist/components/rel-snackbar/README.md +134 -0
- package/dist/components/rel-snackbar/rel-snackbar.ts +67 -2
- package/dist/components/rel-stepper/README.md +160 -0
- package/dist/components/rel-stepper/rel-stepper.ts +6 -6
- package/dist/components/rel-switch/README.md +102 -0
- package/dist/components/rel-switch/rel-switch.ts +15 -6
- package/dist/components/rel-tab/README.md +163 -0
- package/dist/components/rel-tab/rel-tab.ts +36 -14
- package/dist/components/rel-table/README.md +178 -0
- package/dist/components/rel-table/rel-table-body.ts +16 -0
- package/dist/components/rel-table/rel-table-caption.ts +22 -0
- package/dist/components/rel-table/rel-table-cell.ts +49 -0
- package/dist/components/rel-table/rel-table-footer.ts +18 -0
- package/dist/components/rel-table/rel-table-head.ts +33 -0
- package/dist/components/rel-table/rel-table-header.ts +16 -0
- package/dist/components/rel-table/rel-table-row.ts +32 -0
- package/dist/components/rel-table/rel-table.ts +47 -0
- package/dist/components/rel-textfield/README.md +90 -0
- package/dist/components/rel-textfield/rel-textfield.ts +35 -19
- package/dist/components/rel-toast/README.md +148 -0
- package/dist/components/rel-toast/rel-toast.ts +32 -14
- package/dist/components/rel-tooltip/README.md +103 -0
- package/dist/components/rel-tooltip/rel-tooltip.ts +9 -9
- package/dist/index.d.mts +836 -1
- package/dist/index.d.ts +836 -1
- package/dist/index.js +10658 -1493
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10616 -1499
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +293 -183
- package/dist/{vars-6ZIUPIU4.css → vars-ITL2K3KO.css} +293 -183
- 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"`
|