@reliability-design/web 1.4.4 → 1.4.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/rel-accordion.ts +13 -1
- package/dist/components/rel-alert/rel-alert.ts +1 -1
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +33 -2
- package/dist/components/rel-badge/rel-badge.ts +6 -1
- package/dist/components/rel-button/rel-button.ts +18 -2
- package/dist/components/rel-calendar/rel-calendar.ts +86 -18
- package/dist/components/rel-carousel/rel-carousel.ts +45 -2
- package/dist/components/rel-checkbox/rel-checkbox.ts +4 -1
- package/dist/components/rel-chip/rel-chip.ts +59 -19
- package/dist/components/rel-date-picker/date-parse.test.ts +116 -0
- package/dist/components/rel-date-picker/date-parse.ts +178 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +675 -93
- package/dist/components/rel-drawer/rel-drawer.ts +28 -61
- package/dist/components/rel-menu/rel-menu.ts +130 -15
- package/dist/components/rel-menubar/rel-menubar.ts +221 -63
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +39 -3
- package/dist/components/rel-otp/rel-otp.ts +14 -4
- package/dist/components/rel-pagination/rel-pagination.ts +13 -0
- package/dist/components/rel-popover/rel-popover.ts +64 -5
- package/dist/components/rel-progress/rel-progress.ts +13 -4
- package/dist/components/rel-radio/README.md +6 -3
- package/dist/components/rel-radio/rel-radio.ts +236 -60
- package/dist/components/rel-range-picker/rel-range-picker.ts +581 -91
- package/dist/components/rel-resizable/rel-resizable-group.ts +20 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +55 -13
- package/dist/components/rel-select/rel-select.ts +203 -24
- package/dist/components/rel-sheet/rel-sheet.ts +184 -18
- package/dist/components/rel-sidebar/rel-sidebar.ts +50 -40
- package/dist/components/rel-skeleton/rel-skeleton.ts +9 -0
- package/dist/components/rel-slider/rel-slider.ts +6 -3
- package/dist/components/rel-snackbar/rel-snackbar.ts +20 -16
- package/dist/components/rel-stepper/rel-stepper.ts +47 -2
- package/dist/components/rel-switch/rel-switch.ts +3 -1
- package/dist/components/rel-tab/rel-tab.ts +32 -7
- package/dist/components/rel-textfield/rel-textfield.ts +263 -75
- package/dist/components/rel-time-picker/rel-time-picker.ts +749 -261
- package/dist/components/rel-time-picker/time-parse.test.ts +67 -0
- package/dist/components/rel-time-picker/time-parse.ts +85 -0
- package/dist/components/rel-toast/rel-toast.ts +4 -2
- package/dist/components/rel-tooltip/README.md +13 -5
- package/dist/components/rel-tooltip/rel-tooltip.ts +33 -0
- package/dist/components/rel-topbar/rel-topbar-actions.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-brand.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-dropdown.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-item.ts +318 -319
- package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar-profile.ts +224 -224
- package/dist/components/rel-topbar/rel-topbar-utilities.ts +1 -1
- package/dist/components/rel-topbar/rel-topbar.ts +440 -440
- package/dist/components/rel-upload/rel-upload.ts +25 -0
- package/dist/index.d.mts +461 -23
- package/dist/index.d.ts +461 -23
- package/dist/index.js +3906 -807
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4143 -1055
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { parseTimeInput } from "./time-parse";
|
|
4
|
+
|
|
5
|
+
const time = (hour: number, minute: number, second = 0) => ({
|
|
6
|
+
kind: "time" as const,
|
|
7
|
+
hour,
|
|
8
|
+
minute,
|
|
9
|
+
second,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("now shortcut: n / now, any case, padded", () => {
|
|
13
|
+
assert.deepEqual(parseTimeInput("n"), { kind: "now" });
|
|
14
|
+
assert.deepEqual(parseTimeInput("N"), { kind: "now" });
|
|
15
|
+
assert.deepEqual(parseTimeInput(" now "), { kind: "now" });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("bare hour", () => {
|
|
19
|
+
assert.deepEqual(parseTimeInput("9"), time(9, 0));
|
|
20
|
+
assert.deepEqual(parseTimeInput("09"), time(9, 0));
|
|
21
|
+
assert.deepEqual(parseTimeInput("17"), time(17, 0));
|
|
22
|
+
assert.deepEqual(parseTimeInput("0"), time(0, 0));
|
|
23
|
+
assert.deepEqual(parseTimeInput("23"), time(23, 0));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("compact digits HMM / HHMM / HHMMSS", () => {
|
|
27
|
+
assert.deepEqual(parseTimeInput("930"), time(9, 30));
|
|
28
|
+
assert.deepEqual(parseTimeInput("0930"), time(9, 30));
|
|
29
|
+
assert.deepEqual(parseTimeInput("1705"), time(17, 5));
|
|
30
|
+
assert.deepEqual(parseTimeInput("093015"), time(9, 30, 15));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("separated: colon, dot, space", () => {
|
|
34
|
+
assert.deepEqual(parseTimeInput("9:30"), time(9, 30));
|
|
35
|
+
assert.deepEqual(parseTimeInput("9.30"), time(9, 30));
|
|
36
|
+
assert.deepEqual(parseTimeInput("9 30"), time(9, 30));
|
|
37
|
+
assert.deepEqual(parseTimeInput("17:05:30"), time(17, 5, 30));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("meridiem suffixes", () => {
|
|
41
|
+
assert.deepEqual(parseTimeInput("9 pm"), time(21, 0));
|
|
42
|
+
assert.deepEqual(parseTimeInput("9:30p"), time(21, 30));
|
|
43
|
+
assert.deepEqual(parseTimeInput("9:30 PM"), time(21, 30));
|
|
44
|
+
assert.deepEqual(parseTimeInput("9a"), time(9, 0));
|
|
45
|
+
assert.deepEqual(parseTimeInput("930pm"), time(21, 30));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("12am → 0h, 12pm → 12h", () => {
|
|
49
|
+
assert.deepEqual(parseTimeInput("12am"), time(0, 0));
|
|
50
|
+
assert.deepEqual(parseTimeInput("12 pm"), time(12, 0));
|
|
51
|
+
assert.deepEqual(parseTimeInput("12:30 am"), time(0, 30));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("invalid input rejected", () => {
|
|
55
|
+
assert.equal(parseTimeInput(""), null);
|
|
56
|
+
assert.equal(parseTimeInput(" "), null);
|
|
57
|
+
assert.equal(parseTimeInput("asdf"), null);
|
|
58
|
+
assert.equal(parseTimeInput("25:00"), null);
|
|
59
|
+
assert.equal(parseTimeInput("9:70"), null);
|
|
60
|
+
assert.equal(parseTimeInput("9:30:70"), null);
|
|
61
|
+
assert.equal(parseTimeInput("24"), null);
|
|
62
|
+
assert.equal(parseTimeInput("13pm"), null); // 12h clock with meridiem
|
|
63
|
+
assert.equal(parseTimeInput("0pm"), null);
|
|
64
|
+
assert.equal(parseTimeInput("12345"), null); // 5 digits is ambiguous
|
|
65
|
+
assert.equal(parseTimeInput("9:30:15:10"), null);
|
|
66
|
+
assert.equal(parseTimeInput("pm"), null);
|
|
67
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Free-form time parsing for rel-time-picker's typed keyboard entry.
|
|
3
|
+
*
|
|
4
|
+
* Pure (no DOM, no Date.now) so it is unit-testable in Node. The component
|
|
5
|
+
* owns range checks (minTime/maxTime, minuteStep snapping) and the "now"
|
|
6
|
+
* clock — this module only turns text into a time or a shortcut intent.
|
|
7
|
+
*
|
|
8
|
+
* Accepted input (case/whitespace-insensitive):
|
|
9
|
+
* "9" → 9:00 "930" → 9:30
|
|
10
|
+
* "0930" → 9:30 "093015" → 9:30:15
|
|
11
|
+
* "9:30" → 9:30 "9.30" → 9:30
|
|
12
|
+
* "9 30" → 9:30 "17:05:30" → 17:05:30
|
|
13
|
+
* "9 pm" → 21:00 "9:30p" → 21:30
|
|
14
|
+
* "12am" → 0:00 "12 pm" → 12:00
|
|
15
|
+
* "n", "now" → the Now shortcut
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type TimeInputResult =
|
|
19
|
+
| { kind: "time"; hour: number; minute: number; second: number }
|
|
20
|
+
| { kind: "now" }
|
|
21
|
+
| null;
|
|
22
|
+
|
|
23
|
+
export function parseTimeInput(text: string): TimeInputResult {
|
|
24
|
+
let input = text.trim().toLowerCase();
|
|
25
|
+
if (!input) return null;
|
|
26
|
+
|
|
27
|
+
if (input === "n" || input === "now") return { kind: "now" };
|
|
28
|
+
|
|
29
|
+
// Meridiem suffix: "am"/"pm"/"a"/"p", optionally space-separated.
|
|
30
|
+
let meridiem: "am" | "pm" | null = null;
|
|
31
|
+
const meridiemMatch = input.match(/\s*(am|pm|a|p)\.?$/);
|
|
32
|
+
if (meridiemMatch) {
|
|
33
|
+
meridiem = meridiemMatch[1].startsWith("a") ? "am" : "pm";
|
|
34
|
+
input = input.slice(0, meridiemMatch.index).trim();
|
|
35
|
+
}
|
|
36
|
+
if (!input) return null;
|
|
37
|
+
|
|
38
|
+
let hour: number;
|
|
39
|
+
let minute = 0;
|
|
40
|
+
let second = 0;
|
|
41
|
+
|
|
42
|
+
if (/^\d+$/.test(input)) {
|
|
43
|
+
// Bare digits: H, HH, HMM, HHMM, HHMMSS.
|
|
44
|
+
switch (input.length) {
|
|
45
|
+
case 1:
|
|
46
|
+
case 2:
|
|
47
|
+
hour = parseInt(input, 10);
|
|
48
|
+
break;
|
|
49
|
+
case 3:
|
|
50
|
+
hour = parseInt(input.slice(0, 1), 10);
|
|
51
|
+
minute = parseInt(input.slice(1), 10);
|
|
52
|
+
break;
|
|
53
|
+
case 4:
|
|
54
|
+
hour = parseInt(input.slice(0, 2), 10);
|
|
55
|
+
minute = parseInt(input.slice(2), 10);
|
|
56
|
+
break;
|
|
57
|
+
case 6:
|
|
58
|
+
hour = parseInt(input.slice(0, 2), 10);
|
|
59
|
+
minute = parseInt(input.slice(2, 4), 10);
|
|
60
|
+
second = parseInt(input.slice(4), 10);
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// Separated parts: "H:MM", "H.MM", "H MM", "HH:MM:SS".
|
|
67
|
+
const parts = input.split(/[:.\s]+/);
|
|
68
|
+
if (parts.length < 2 || parts.length > 3) return null;
|
|
69
|
+
if (!parts.every((p) => /^\d{1,2}$/.test(p))) return null;
|
|
70
|
+
hour = parseInt(parts[0], 10);
|
|
71
|
+
minute = parseInt(parts[1], 10);
|
|
72
|
+
if (parts.length === 3) second = parseInt(parts[2], 10);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (meridiem) {
|
|
76
|
+
// 12-hour clock: hour must be 1–12; 12am → 0, 12pm → 12.
|
|
77
|
+
if (hour < 1 || hour > 12) return null;
|
|
78
|
+
hour = meridiem === "am" ? hour % 12 : (hour % 12) + 12;
|
|
79
|
+
} else if (hour > 23) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
if (minute > 59 || second > 59) return null;
|
|
83
|
+
|
|
84
|
+
return { kind: "time", hour, minute, second };
|
|
85
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LitElement, html, css } from "lit";
|
|
2
2
|
import { customElement, property, state } from "lit/decorators.js";
|
|
3
|
+
import { announce } from "../../utils/announce";
|
|
3
4
|
|
|
4
5
|
export type ToastType = "default" | "error";
|
|
5
6
|
|
|
@@ -35,7 +36,7 @@ export class RelToast extends LitElement {
|
|
|
35
36
|
align-items: center;
|
|
36
37
|
gap: var(--rel-spacing-xs);
|
|
37
38
|
background-color: var(--rel-bg-neutral-inverse-1);
|
|
38
|
-
color: var(--rel-text-inverse);
|
|
39
|
+
color: var(--rel-text-inverse);
|
|
39
40
|
border-radius: var(--rel-border-radius-sm);
|
|
40
41
|
padding: var(--rel-spacing-sm) var(--rel-spacing-md);
|
|
41
42
|
box-shadow: var(--rel-shadows-shadow-300);
|
|
@@ -135,6 +136,7 @@ export class RelToast extends LitElement {
|
|
|
135
136
|
if (!changed.has('show')) return;
|
|
136
137
|
if (this.show) {
|
|
137
138
|
this.visible = true;
|
|
139
|
+
announce(this.message, this.type === "error" ? "assertive" : "polite");
|
|
138
140
|
if (this._dismissTimer) clearTimeout(this._dismissTimer);
|
|
139
141
|
if (this.duration > 0) {
|
|
140
142
|
this._dismissTimer = setTimeout(() => this.close(), this.duration);
|
|
@@ -185,7 +187,7 @@ export class RelToast extends LitElement {
|
|
|
185
187
|
|
|
186
188
|
render() {
|
|
187
189
|
return html`
|
|
188
|
-
<div part="base" class="toast ${this.type} ${this.visible ? "" : "hidden"}" role="status" aria-live="polite">
|
|
190
|
+
<div part="base" class="toast ${this.type} ${this.visible ? "" : "hidden"}" role="status" aria-live="polite" aria-atomic="true">
|
|
189
191
|
<span part="label" class="toast-message">${this.message}</span>
|
|
190
192
|
|
|
191
193
|
${this.actionLabel
|
|
@@ -65,12 +65,19 @@ None exposed.
|
|
|
65
65
|
- `--rel-spacing-sm` - Horizontal padding
|
|
66
66
|
|
|
67
67
|
### Color Tokens
|
|
68
|
-
- `--rel-bg-neutral-inverse-
|
|
69
|
-
- `--rel-
|
|
68
|
+
- `--rel-bg-neutral-inverse-2` - Background color (near-black in light theme, white in dark theme)
|
|
69
|
+
- `--rel-text-inverse` - Text color (white in light theme, near-black in dark theme)
|
|
70
|
+
- `--rel-colors-dark-alpha-3` - Elevation shadow color (Figma `shadow/sm`: 0 2px 4px at 10% black; applied via `drop-shadow` so the tail casts the same shadow as the bubble)
|
|
70
71
|
|
|
71
72
|
### Border Tokens
|
|
72
73
|
- `--rel-border-radius-sm` - Border radius
|
|
73
74
|
|
|
75
|
+
### Theming
|
|
76
|
+
Colors use inverse semantic tokens, so the tooltip adapts automatically when
|
|
77
|
+
`data-theme="dark"` (or `.dark`) is set on an ancestor — no page refresh needed.
|
|
78
|
+
Dark-theme rendering (white bubble, dark text) is unchanged by the elevation
|
|
79
|
+
shadow, which is effectively invisible on dark backgrounds.
|
|
80
|
+
|
|
74
81
|
## Accessibility
|
|
75
82
|
- Uses `role="tooltip"` on the tooltip bubble
|
|
76
83
|
- `aria-hidden="true"` on tooltip (content is supplemental, not critical)
|
|
@@ -78,11 +85,12 @@ None exposed.
|
|
|
78
85
|
- For critical information, consider alternative patterns (e.g., visible labels or expandable sections)
|
|
79
86
|
|
|
80
87
|
### Keyboard Interactions
|
|
81
|
-
-
|
|
82
|
-
-
|
|
88
|
+
- Tooltip shows on `:focus-within` as well as hover, so keyboard users see it when the slotted trigger receives focus (ensure the trigger is focusable)
|
|
89
|
+
- `Escape` dismisses the tooltip (APG); it re-arms once hover/focus leaves and returns
|
|
90
|
+
- Keyboard-triggered tooltips use the same theme-aware styling as hover-triggered ones
|
|
83
91
|
|
|
84
92
|
### Focus Behavior
|
|
85
|
-
- Tooltip
|
|
93
|
+
- Tooltip activates on hover or focus within the trigger
|
|
86
94
|
- Does not interfere with focus on slotted content
|
|
87
95
|
|
|
88
96
|
## Notes / Edge Cases
|
|
@@ -43,6 +43,9 @@ export class RelTooltip extends LitElement {
|
|
|
43
43
|
pointer-events: none;
|
|
44
44
|
transition: opacity 0.2s ease;
|
|
45
45
|
overflow: visible;
|
|
46
|
+
/* Figma shadow/sm; drop-shadow (not box-shadow) so the border-drawn
|
|
47
|
+
tail casts the same shadow as the bubble. */
|
|
48
|
+
filter: drop-shadow(0px 2px 4px var(--rel-colors-dark-alpha-3, rgba(11, 11, 11, 0.1)));
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
/* Figma Type=Multi Line: wrap inside a bounded bubble */
|
|
@@ -57,6 +60,12 @@ export class RelTooltip extends LitElement {
|
|
|
57
60
|
opacity: 1;
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
/* Escape-dismissed (APG): stays hidden until hover/focus leaves and
|
|
64
|
+
returns. Declared after the hover/focus rules so source order wins. */
|
|
65
|
+
:host([data-dismissed]) .tooltip-bubble {
|
|
66
|
+
opacity: 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
/* Positioning */
|
|
61
70
|
.tooltip-bubble.above {
|
|
62
71
|
bottom: calc(100% + 6px);
|
|
@@ -131,6 +140,30 @@ export class RelTooltip extends LitElement {
|
|
|
131
140
|
}
|
|
132
141
|
`;
|
|
133
142
|
|
|
143
|
+
connectedCallback() {
|
|
144
|
+
super.connectedCallback();
|
|
145
|
+
this.addEventListener("keydown", this.onKeydown);
|
|
146
|
+
this.addEventListener("mouseleave", this.clearDismissed);
|
|
147
|
+
this.addEventListener("focusout", this.clearDismissed);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
disconnectedCallback() {
|
|
151
|
+
super.disconnectedCallback();
|
|
152
|
+
this.removeEventListener("keydown", this.onKeydown);
|
|
153
|
+
this.removeEventListener("mouseleave", this.clearDismissed);
|
|
154
|
+
this.removeEventListener("focusout", this.clearDismissed);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** APG: Escape dismisses the tooltip. Not swallowed — an enclosing dialog
|
|
158
|
+
* may legitimately close on the same keystroke. */
|
|
159
|
+
private onKeydown = (e: KeyboardEvent) => {
|
|
160
|
+
if (e.key === "Escape") this.toggleAttribute("data-dismissed", true);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
private clearDismissed = () => {
|
|
164
|
+
this.removeAttribute("data-dismissed");
|
|
165
|
+
};
|
|
166
|
+
|
|
134
167
|
firstUpdated() {
|
|
135
168
|
this.linkTrigger();
|
|
136
169
|
}
|
|
@@ -3,7 +3,7 @@ import { customElement } from "lit/decorators.js";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @deprecated Part of the deprecated rel-topbar family. Use plain
|
|
6
|
-
* `rel-button` elements inside the "
|
|
6
|
+
* `rel-button` elements inside the "Navbar" recipe at /docs/recipes/navbar.
|
|
7
7
|
*/
|
|
8
8
|
@customElement("rel-topbar-actions")
|
|
9
9
|
export class RelTopbarActions extends LitElement {
|
|
@@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators.js";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @deprecated Part of the deprecated rel-topbar family. Use a plain
|
|
6
|
-
* `<a>`/logo markup inside the "
|
|
6
|
+
* `<a>`/logo markup inside the "Navbar" recipe at /docs/recipes/navbar.
|
|
7
7
|
*/
|
|
8
8
|
@customElement("rel-topbar-brand")
|
|
9
9
|
export class RelTopbarBrand extends LitElement {
|
|
@@ -3,7 +3,7 @@ import { customElement, property } from "lit/decorators.js";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @deprecated Part of the deprecated rel-topbar family. Use `rel-menu`'s
|
|
6
|
-
* item slots or `rel-popover` content in the "
|
|
6
|
+
* item slots or `rel-popover` content in the "Navbar" recipe at /docs/recipes/navbar.
|
|
7
7
|
*/
|
|
8
8
|
@customElement("rel-topbar-dropdown-item")
|
|
9
9
|
export class RelTopbarDropdownItem extends LitElement {
|
|
@@ -3,7 +3,7 @@ import { customElement } from "lit/decorators.js";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @deprecated Part of the deprecated rel-topbar family. Use `rel-menu` or
|
|
6
|
-
* `rel-popover` for nav dropdowns in the "
|
|
6
|
+
* `rel-popover` for nav dropdowns in the "Navbar" recipe at /docs/recipes/navbar.
|
|
7
7
|
*/
|
|
8
8
|
@customElement("rel-topbar-dropdown")
|
|
9
9
|
export class RelTopbarDropdown extends LitElement {
|