@reliability-design/web 1.4.5 → 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.
Files changed (42) hide show
  1. package/dist/components/rel-alert/rel-alert.ts +1 -1
  2. package/dist/components/rel-autocomplete/rel-autocomplete.ts +33 -2
  3. package/dist/components/rel-badge/rel-badge.ts +6 -1
  4. package/dist/components/rel-calendar/rel-calendar.ts +64 -18
  5. package/dist/components/rel-carousel/rel-carousel.ts +25 -2
  6. package/dist/components/rel-chip/rel-chip.ts +31 -20
  7. package/dist/components/rel-date-picker/date-parse.test.ts +116 -0
  8. package/dist/components/rel-date-picker/date-parse.ts +178 -0
  9. package/dist/components/rel-date-picker/rel-date-picker.ts +529 -90
  10. package/dist/components/rel-drawer/rel-drawer.ts +6 -3
  11. package/dist/components/rel-menu/rel-menu.ts +6 -0
  12. package/dist/components/rel-otp/rel-otp.ts +12 -2
  13. package/dist/components/rel-pagination/rel-pagination.ts +13 -0
  14. package/dist/components/rel-popover/rel-popover.ts +33 -2
  15. package/dist/components/rel-progress/rel-progress.ts +13 -4
  16. package/dist/components/rel-radio/README.md +6 -3
  17. package/dist/components/rel-radio/rel-radio.ts +236 -60
  18. package/dist/components/rel-range-picker/rel-range-picker.ts +452 -95
  19. package/dist/components/rel-segmented-button/rel-segmented-button.ts +5 -1
  20. package/dist/components/rel-select/rel-select.ts +44 -13
  21. package/dist/components/rel-sheet/rel-sheet.ts +163 -18
  22. package/dist/components/rel-sidebar/rel-sidebar.ts +7 -1
  23. package/dist/components/rel-skeleton/rel-skeleton.ts +9 -0
  24. package/dist/components/rel-slider/rel-slider.ts +6 -3
  25. package/dist/components/rel-snackbar/rel-snackbar.ts +20 -16
  26. package/dist/components/rel-stepper/rel-stepper.ts +3 -1
  27. package/dist/components/rel-switch/rel-switch.ts +3 -1
  28. package/dist/components/rel-textfield/rel-textfield.ts +263 -75
  29. package/dist/components/rel-time-picker/rel-time-picker.ts +450 -94
  30. package/dist/components/rel-time-picker/time-parse.test.ts +67 -0
  31. package/dist/components/rel-time-picker/time-parse.ts +85 -0
  32. package/dist/components/rel-toast/rel-toast.ts +4 -2
  33. package/dist/components/rel-tooltip/README.md +13 -5
  34. package/dist/components/rel-tooltip/rel-tooltip.ts +3 -0
  35. package/dist/components/rel-upload/rel-upload.ts +25 -0
  36. package/dist/index.d.mts +241 -11
  37. package/dist/index.d.ts +241 -11
  38. package/dist/index.js +2573 -598
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +2614 -640
  41. package/dist/index.mjs.map +1 -1
  42. package/package.json +2 -2
@@ -1,5 +1,6 @@
1
- import { LitElement, html, css } from "lit";
1
+ import { LitElement, html, css, nothing, type PropertyValues } from "lit";
2
2
  import { customElement, property } from "lit/decorators.js";
3
+ import "@reliability-design/icons/rel-icon";
3
4
  import type { Orientation } from "../rel-stepper/rel-stepper.js";
4
5
 
5
6
  export interface RadioOption {
@@ -7,8 +8,17 @@ export interface RadioOption {
7
8
  label: string;
8
9
  supportingText?: string;
9
10
  disabled?: boolean;
11
+ /** rel-icon name rendered before the label (Figma Has Icon/Avatar, Type=Icon). */
12
+ icon?: string;
13
+ /** 1–2 initials rendered as a circular chip before the label (Type=Avatar).
14
+ * Ignored when `icon` is also set. */
15
+ avatar?: string;
10
16
  }
11
17
 
18
+ type RadioSize = "small" | "medium" | "large";
19
+ type RadioDensity = "compact" | "comfortable";
20
+ type RadioAlignment = "leading" | "trailing";
21
+
12
22
  @customElement("rel-radio")
13
23
  export class RelRadio extends LitElement {
14
24
  @property({ type: Array }) options: RadioOption[] = [];
@@ -19,9 +29,23 @@ export class RelRadio extends LitElement {
19
29
  * a column; `"horizontal"` lays them out in a wrapping row. */
20
30
  @property({ type: String, reflect: true }) orientation: Orientation = "vertical";
21
31
 
22
- /** Control scale. `"medium"` (default) = 20px circle with body-lg labels;
23
- * `"small"` = 16px circle with body-md labels. */
24
- @property({ type: String, reflect: true }) size: "small" | "medium" = "medium";
32
+ /** Control scale (Figma Size). `"medium"` (default, Figma M) = 20px control
33
+ * with body-lg labels; `"large"` (Figma L) = 24px control with body-xl
34
+ * labels; `"small"` (Figma S) = 20px control with body-md labels — S and M
35
+ * share the control size in the spec, only typography steps down. */
36
+ @property({ type: String, reflect: true }) size: RadioSize = "medium";
37
+
38
+ /** Supporting-text layout (Figma Density). `"compact"` (default) puts the
39
+ * supporting text on its own line under the row, indented past the control;
40
+ * `"comfortable"` stacks label + supporting text beside the control. */
41
+ @property({ type: String, reflect: true }) density: RadioDensity = "compact";
42
+
43
+ /** Control position (Figma Layout). `"leading"` (default) renders the
44
+ * control before the label; `"trailing"` pushes it to the far edge of the
45
+ * row. Trailing has no compact layout in the spec, so it always renders
46
+ * with comfortable density. Meant for vertical orientation — trailing
47
+ * options stretch to the group's full width. */
48
+ @property({ type: String, reflect: true }) alignment: RadioAlignment = "leading";
25
49
 
26
50
  @property({ type: String, reflect: true })
27
51
  selectedValue: string | null = null;
@@ -33,6 +57,9 @@ export class RelRadio extends LitElement {
33
57
 
34
58
  @property({ type: String }) errorMessage = "";
35
59
 
60
+ /** Accessible name for the radiogroup (it renders no visible group label). */
61
+ @property({ type: String, attribute: "field-aria-label" }) fieldAriaLabel?: string;
62
+
36
63
  @property({ type: String }) value = "";
37
64
 
38
65
  checked = false;
@@ -63,6 +90,12 @@ export class RelRadio extends LitElement {
63
90
  gap: var(--rel-spacing-xs) var(--rel-spacing-xl);
64
91
  }
65
92
 
93
+ /* Trailing alignment — options stretch so the control can sit on the far
94
+ edge of the group's width. */
95
+ :host([alignment="trailing"]) .rel-radio-group {
96
+ align-items: stretch;
97
+ }
98
+
66
99
  .rel-radio-option {
67
100
  display: inline-flex;
68
101
  flex-direction: column;
@@ -71,23 +104,11 @@ export class RelRadio extends LitElement {
71
104
  }
72
105
 
73
106
  .rel-radio-option.disabled {
74
- opacity: 0.6;
75
- color: var(--rel-text-disabled);
107
+ /* Figma State=Disabled: whole option at 40% opacity, colors unchanged. */
108
+ opacity: 0.4;
76
109
  cursor: not-allowed !important;
77
110
  }
78
111
 
79
- .rel-radio-option:not(.disabled):hover .radio-custom {
80
- border-color: var(--rel-text-body);
81
- }
82
-
83
- .rel-radio-option:not(.disabled):hover .radio-label {
84
- color: var(--rel-text-body);
85
- }
86
-
87
- .rel-radio-option:not(.disabled):hover .radio-supporting {
88
- color: var(--rel-text-caption);
89
- }
90
-
91
112
  .rel-radio-option:focus-within:has(input:focus-visible) {
92
113
  outline: 1.5px solid var(--rel-border-focus);
93
114
  outline-offset: 1px;
@@ -104,7 +125,17 @@ export class RelRadio extends LitElement {
104
125
  position: relative;
105
126
  display: flex;
106
127
  align-items: center;
107
- gap: var(--rel-spacing-2xs);
128
+ /* Compact row rhythm (control ↔ label). Comfortable widens to 12px. */
129
+ gap: var(--rel-spacing-xs);
130
+ }
131
+
132
+ .rel-radio-option.comfortable .radio-main {
133
+ gap: var(--rel-spacing-sm);
134
+ }
135
+
136
+ :host([alignment="trailing"]) .radio-main {
137
+ flex-direction: row-reverse;
138
+ justify-content: space-between;
108
139
  }
109
140
 
110
141
  input {
@@ -117,63 +148,163 @@ export class RelRadio extends LitElement {
117
148
  pointer-events: none;
118
149
  }
119
150
 
151
+ /* Control box (Figma .Radio Control) — the drawn ring is inset inside a
152
+ larger hit/spacing box: 16px ring in a 20px box (M/S), 20px ring in a
153
+ 24px box (L). Indent math below uses the BOX size. */
120
154
  .radio-custom {
121
155
  width: 20px;
122
156
  height: 20px;
123
- border: 2px solid var(--rel-text-label);
124
- border-radius: 50%;
157
+ display: inline-flex;
158
+ align-items: center;
159
+ justify-content: center;
125
160
  position: relative;
126
161
  flex-shrink: 0;
127
162
  }
128
163
 
129
- input:checked + .radio-custom::after {
164
+ .radio-custom::before {
165
+ content: "";
166
+ box-sizing: border-box;
167
+ width: 16px;
168
+ height: 16px;
169
+ border: 2px solid var(--rel-text-default);
170
+ border-radius: 50%;
171
+ }
172
+
173
+ .radio-custom::after {
130
174
  content: "";
131
175
  position: absolute;
132
- inset: 4px;
133
- background: var(--rel-text-label);
176
+ width: 8px;
177
+ height: 8px;
134
178
  border-radius: 50%;
179
+ background: var(--rel-text-default);
180
+ display: none;
135
181
  }
136
182
 
137
- .radio-label {
183
+ input:checked + .radio-custom::after {
184
+ display: block;
185
+ }
186
+
187
+ /* Comfortable density — label and supporting text stack beside the control. */
188
+ .stack {
189
+ display: flex;
190
+ flex-direction: column;
191
+ min-width: 0;
192
+ }
193
+
194
+ .label-line {
195
+ display: flex;
196
+ align-items: center;
197
+ gap: var(--rel-spacing-xs);
198
+ min-width: 0;
199
+ }
200
+
201
+ /* Optional label prefix — icon tracks the label's color in every state
202
+ (rel-icon inherits currentColor). */
203
+ .radio-prefix {
204
+ flex-shrink: 0;
138
205
  color: var(--rel-text-default);
206
+ }
139
207
 
140
- font-family: var(--rel-body-lg-regular-font-family);
208
+ .radio-avatar {
209
+ flex-shrink: 0;
210
+ width: 20px;
211
+ height: 20px;
212
+ border-radius: 50%;
213
+ display: inline-flex;
214
+ align-items: center;
215
+ justify-content: center;
216
+ background: var(--rel-bg-neutral-inverse-2);
217
+ color: var(--rel-text-inverse);
218
+ font-family: var(--rel-body-xs-semibold-font-family);
219
+ font-weight: var(--rel-body-xs-semibold-font-weight);
220
+ font-size: var(--rel-body-xs-semibold-font-size);
221
+ line-height: 1;
222
+ letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
223
+ }
141
224
 
225
+ .radio-label {
226
+ color: var(--rel-text-default);
227
+ font-family: var(--rel-body-lg-regular-font-family);
142
228
  font-weight: var(--rel-body-lg-regular-font-weight);
143
-
144
229
  font-size: var(--rel-body-lg-regular-font-size);
145
-
146
230
  line-height: var(--rel-body-lg-regular-line-height);
147
-
148
231
  letter-spacing: var(--rel-body-lg-regular-letter-spacing);
149
-
150
232
  }
151
233
 
152
234
  .radio-supporting {
153
- margin-left: 28px;
154
235
  color: var(--rel-text-caption);
155
-
156
236
  font-family: var(--rel-body-sm-regular-font-family);
157
-
158
237
  font-weight: var(--rel-body-sm-regular-font-weight);
159
-
160
238
  font-size: var(--rel-body-sm-regular-font-size);
161
-
162
239
  line-height: var(--rel-body-sm-regular-line-height);
163
-
164
240
  letter-spacing: var(--rel-body-sm-regular-letter-spacing);
241
+ }
165
242
 
243
+ /* Compact density — supporting text sits under the whole row, indented
244
+ past the control box (box width + row gap). */
245
+ .radio-supporting.indent {
246
+ padding-left: 28px;
166
247
  }
167
248
 
168
- /* Size variant (medium is the base scale above). Supporting-text indent =
169
- circle width + the radio-main gap, so it stays aligned under the label. */
170
- :host([size="small"]) .radio-custom {
171
- width: 16px;
172
- height: 16px;
249
+ /* Hover (Figma State=Hover) — ring, dot, label, and prefix icon shift
250
+ text-default → text-body; supporting text stays caption. Scoped away
251
+ from disabled and error so those keep their own colors. */
252
+ .rel-radio-option:not(.disabled):not(.error-unchecked):not(.error-checked):hover .radio-custom::before {
253
+ border-color: var(--rel-text-body);
173
254
  }
174
255
 
175
- :host([size="small"]) input:checked + .radio-custom::after {
176
- inset: 3px;
256
+ .rel-radio-option:not(.disabled):not(.error-unchecked):not(.error-checked):hover .radio-custom::after {
257
+ background: var(--rel-text-body);
258
+ }
259
+
260
+ .rel-radio-option:not(.disabled):not(.error-unchecked):not(.error-checked):hover .radio-label,
261
+ .rel-radio-option:not(.disabled):not(.error-unchecked):not(.error-checked):hover .radio-prefix {
262
+ color: var(--rel-text-body);
263
+ }
264
+
265
+ /* Size variants (medium is the base scale above). */
266
+ :host([size="large"]) .radio-custom {
267
+ width: 24px;
268
+ height: 24px;
269
+ }
270
+
271
+ :host([size="large"]) .radio-custom::before {
272
+ width: 20px;
273
+ height: 20px;
274
+ }
275
+
276
+ :host([size="large"]) .radio-custom::after {
277
+ width: 10px;
278
+ height: 10px;
279
+ }
280
+
281
+ :host([size="large"]) .radio-label {
282
+ font-family: var(--rel-body-xl-regular-font-family);
283
+ font-weight: var(--rel-body-xl-regular-font-weight);
284
+ font-size: var(--rel-body-xl-regular-font-size);
285
+ line-height: var(--rel-body-xl-regular-line-height);
286
+ letter-spacing: var(--rel-body-xl-regular-letter-spacing);
287
+ }
288
+
289
+ :host([size="large"]) .radio-supporting {
290
+ font-family: var(--rel-body-md-regular-font-family);
291
+ font-weight: var(--rel-body-md-regular-font-weight);
292
+ font-size: var(--rel-body-md-regular-font-size);
293
+ line-height: var(--rel-body-md-regular-line-height);
294
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
295
+ }
296
+
297
+ :host([size="large"]) .radio-supporting.indent {
298
+ padding-left: 32px;
299
+ }
300
+
301
+ :host([size="large"]) .radio-avatar {
302
+ width: 24px;
303
+ height: 24px;
304
+ font-family: var(--rel-body-sm-semibold-font-family);
305
+ font-weight: var(--rel-body-sm-semibold-font-weight);
306
+ font-size: var(--rel-body-sm-semibold-font-size);
307
+ letter-spacing: var(--rel-body-sm-semibold-letter-spacing);
177
308
  }
178
309
 
179
310
  :host([size="small"]) .radio-label {
@@ -185,7 +316,6 @@ export class RelRadio extends LitElement {
185
316
  }
186
317
 
187
318
  :host([size="small"]) .radio-supporting {
188
- margin-left: 24px;
189
319
  font-family: var(--rel-body-xs-regular-font-family);
190
320
  font-weight: var(--rel-body-xs-regular-font-weight);
191
321
  font-size: var(--rel-body-xs-regular-font-size);
@@ -193,21 +323,17 @@ export class RelRadio extends LitElement {
193
323
  letter-spacing: var(--rel-body-xs-regular-letter-spacing);
194
324
  }
195
325
 
196
- /* Error state (Figma State=Error, both Is Checked values) */
197
- .rel-radio-option.error-unchecked .radio-custom,
198
- .rel-radio-option.error-checked .radio-custom {
326
+ /* Error state (Figma State=Error) — ring, dot, and supporting text go
327
+ danger; the label (and prefix) keep their default color. */
328
+ .rel-radio-option.error-unchecked .radio-custom::before,
329
+ .rel-radio-option.error-checked .radio-custom::before {
199
330
  border-color: var(--rel-border-danger);
200
331
  }
201
332
 
202
- .rel-radio-option.error-checked input:checked + .radio-custom::after {
333
+ .rel-radio-option.error-checked .radio-custom::after {
203
334
  background: var(--rel-text-danger);
204
335
  }
205
336
 
206
- .rel-radio-option.error-unchecked .radio-label,
207
- .rel-radio-option.error-checked .radio-label {
208
- color: var(--rel-text-danger);
209
- }
210
-
211
337
  .rel-radio-option.error-unchecked .radio-supporting,
212
338
  .rel-radio-option.error-checked .radio-supporting {
213
339
  color: var(--rel-text-danger);
@@ -229,8 +355,29 @@ export class RelRadio extends LitElement {
229
355
  letter-spacing: var(--rel-body-sm-regular-letter-spacing);
230
356
 
231
357
  }
358
+
359
+ .error-message.empty {
360
+ margin-top: 0;
361
+ }
232
362
  `;
233
363
 
364
+ protected willUpdate(changed: PropertyValues<this>) {
365
+ if (
366
+ (changed.has("alignment") || changed.has("density")) &&
367
+ this.alignment === "trailing" &&
368
+ this.density === "compact"
369
+ ) {
370
+ console.warn(
371
+ '[rel-radio] density="compact" has no trailing layout; rendering comfortable density.'
372
+ );
373
+ }
374
+ }
375
+
376
+ /** Trailing has no compact layout in the spec — trailing wins. */
377
+ private get effectiveDensity(): RadioDensity {
378
+ return this.alignment === "trailing" ? "comfortable" : this.density;
379
+ }
380
+
234
381
  private onChange(value: string) {
235
382
  if (this.disabled) return;
236
383
 
@@ -253,11 +400,27 @@ export class RelRadio extends LitElement {
253
400
  return this.selectedValue !== null && this.selectedValue !== "";
254
401
  }
255
402
 
403
+ private renderPrefix(option: RadioOption) {
404
+ if (option.icon) {
405
+ return html`<rel-icon
406
+ name="${option.icon}"
407
+ size="${this.size === "large" ? "lg" : "md"}"
408
+ class="radio-prefix"
409
+ ></rel-icon>`;
410
+ }
411
+ if (option.avatar) {
412
+ return html`<span class="radio-avatar" aria-hidden="true">${option.avatar}</span>`;
413
+ }
414
+ return null;
415
+ }
416
+
256
417
  private renderOption(option: RadioOption) {
257
418
  const isChecked = this.selectedValue === option.value;
258
419
  const isDisabled = this.disabled || option.disabled;
420
+ const comfortable = this.effectiveDensity === "comfortable";
259
421
  const classes = [
260
422
  "rel-radio-option",
423
+ comfortable ? "comfortable" : "compact",
261
424
  isDisabled ? "disabled" : "",
262
425
  this.error && !this.hasSelection ? "error-unchecked" : "",
263
426
  this.error && isChecked ? "error-checked" : "",
@@ -265,6 +428,19 @@ export class RelRadio extends LitElement {
265
428
  .filter(Boolean)
266
429
  .join(" ");
267
430
 
431
+ const labelLine = html`
432
+ <span class="label-line">
433
+ ${this.renderPrefix(option)}
434
+ <span part="label" class="radio-label">${option.label}</span>
435
+ </span>
436
+ `;
437
+
438
+ const supporting = option.supportingText
439
+ ? html`<div class="radio-supporting ${comfortable ? "" : "indent"}">
440
+ ${option.supportingText}
441
+ </div>`
442
+ : null;
443
+
268
444
  return html`
269
445
  <label class="${classes}">
270
446
  <div class="radio-main">
@@ -277,12 +453,12 @@ export class RelRadio extends LitElement {
277
453
  @blur="${() => this._onTouched()}"
278
454
  />
279
455
  <span class="radio-custom"></span>
280
- <span part="label" class="radio-label">${option.label}</span>
456
+ ${comfortable
457
+ ? html`<span class="stack">${labelLine}${supporting}</span>`
458
+ : labelLine}
281
459
  </div>
282
460
 
283
- ${option.supportingText
284
- ? html`<div class="radio-supporting">${option.supportingText}</div>`
285
- : ""}
461
+ ${comfortable ? null : supporting}
286
462
  </label>
287
463
  `;
288
464
  }
@@ -294,12 +470,12 @@ export class RelRadio extends LitElement {
294
470
  class="rel-radio-group"
295
471
  role="radiogroup"
296
472
  aria-orientation="${this.orientation}"
473
+ aria-label="${this.fieldAriaLabel ?? nothing}"
474
+ aria-describedby="radio-error"
297
475
  >
298
476
  ${this.options.map((o) => this.renderOption(o))}
299
477
  </div>
300
- ${this.error && this.errorMessage
301
- ? html`<div class="error-message">${this.errorMessage}</div>`
302
- : ""}
478
+ <div class="${this.error && this.errorMessage ? "error-message" : "error-message empty"}" id="radio-error" aria-live="polite">${this.error && this.errorMessage ? this.errorMessage : ""}</div>
303
479
  `;
304
480
  }
305
481
  }