@reliability-design/web 1.3.0 → 1.3.1

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.
@@ -28,7 +28,7 @@ export class RelAlert extends LitElement {
28
28
 
29
29
  .rel-alert {
30
30
  display: flex;
31
- align-items: center;
31
+ align-items: flex-start;
32
32
  gap: var(--rel-spacing-space-xs);
33
33
  padding: var(--rel-spacing-space-sm) var(--rel-spacing-space-md);
34
34
  border-radius: var(--rel-borders-radius-border-radius-200);
@@ -54,9 +54,15 @@ export class RelAlert extends LitElement {
54
54
  opacity: 0.9;
55
55
  }
56
56
 
57
+ .rel-icon {
58
+ margin-top: 2px;
59
+ flex-shrink: 0;
60
+ }
61
+
57
62
  .rel-icon svg {
58
63
  width: 16px;
59
64
  height: 16px;
65
+ display: block;
60
66
  }
61
67
 
62
68
  .rel-link {
@@ -69,6 +75,7 @@ export class RelAlert extends LitElement {
69
75
 
70
76
  .rel-close {
71
77
  margin-left: auto;
78
+ align-self: flex-start;
72
79
  background: none;
73
80
  border: none;
74
81
  cursor: pointer;
@@ -43,46 +43,63 @@ export class RelBadge extends LitElement {
43
43
  display: inline-flex;
44
44
  align-items: center;
45
45
  justify-content: center;
46
+ box-sizing: border-box;
46
47
  font-family: var(--rel-fontFamilies-primary, sans-serif);
47
48
  font-weight: var(--rel-fontWeights-semibold, 600);
48
49
  background-color: var(--rel-badge-bg, var(--rel-error-surface-default));
49
50
  color: var(--rel-badge-color, #ffffff);
50
- border-radius: 50%;
51
+ /* Large fixed radius (not 50%) — stays a perfect circle for 1-2 digits,
52
+ but becomes a pill instead of clipping when content like "999+"
53
+ is wider than the minimum circle. */
54
+ border-radius: 999px;
55
+ white-space: nowrap;
51
56
  }
52
57
 
53
- /* NUMBER BADGES */
58
+ /* NUMBER BADGES — min-width (not width) lets multi-character content
59
+ like "999+" grow the badge into a pill instead of overflowing a fixed circle. */
54
60
  .badge-sm {
55
- width: 20px;
61
+ min-width: 20px;
56
62
  height: 20px;
63
+ padding: 0 4px;
57
64
  font-size: var(--rel-fontSizes-xs, 12px);
58
65
  }
59
66
 
60
67
  .badge-md {
61
- width: 28px;
68
+ min-width: 28px;
62
69
  height: 28px;
70
+ padding: 0 6px;
63
71
  font-size: var(--rel-fontSizes-sm, 14px);
64
72
  }
65
73
 
66
74
  .badge-lg {
67
- width: 40px;
75
+ min-width: 40px;
68
76
  height: 40px;
77
+ padding: 0 8px;
69
78
  font-size: var(--rel-fontSizes-lg, 20px);
70
79
  }
71
80
 
72
- /* DOT BADGES */
81
+ /* DOT BADGES — explicitly reset min-width/padding inherited from the
82
+ number-badge size rules above; otherwise min-width wins over this
83
+ smaller width and the dot gets stretched into a squashed pill. */
73
84
  .badge-dot.badge-sm {
74
85
  width: 8px;
75
86
  height: 8px;
87
+ min-width: 0;
88
+ padding: 0;
76
89
  }
77
90
 
78
91
  .badge-dot.badge-md {
79
92
  width: 10px;
80
93
  height: 10px;
94
+ min-width: 0;
95
+ padding: 0;
81
96
  }
82
97
 
83
98
  .badge-dot.badge-lg {
84
99
  width: 12px;
85
100
  height: 12px;
101
+ min-width: 0;
102
+ padding: 0;
86
103
  }
87
104
  `;
88
105
 
@@ -1,4 +1,5 @@
1
1
  import "../../styles/vars-v2.css";
2
+ import "../rel-badge/rel-badge";
2
3
  import { LitElement, html, css } from "lit";
3
4
  import { customElement, property } from "lit/decorators.js";
4
5
  import { ifDefined } from "lit/directives/if-defined.js";
@@ -23,9 +24,33 @@ export class RelButton extends LitElement {
23
24
 
24
25
  @property({ type: String, attribute: "aria-label" }) ariaLabel: string | null = null;
25
26
 
27
+ /** Numeric/overflow text shown in a corner badge (e.g. "3", "999+"). Ignored when badgeDot is set. */
28
+ @property({ type: String }) badge = "";
29
+
30
+ /** Shows a small corner dot instead of a numbered badge — takes precedence over `badge`. */
31
+ @property({ type: Boolean, attribute: "badge-dot" }) badgeDot = false;
32
+
26
33
  static styles = css`
27
34
  :host {
28
35
  display: inline-flex;
36
+ position: relative;
37
+ }
38
+
39
+ .btn-badge {
40
+ position: absolute;
41
+ top: -6px;
42
+ right: -6px;
43
+ pointer-events: none;
44
+ }
45
+
46
+ /* Icon buttons are a 40px circle (radius 20). An 8px dot's center needs to
47
+ land ON that circle's edge at the 45° corner point — which works out to
48
+ roughly 2px inward from each edge (R - R/√2 - dot-radius ≈ 1.9px), not
49
+ outside it like the numbered badge. Negative offsets here would float
50
+ the dot in the empty bounding-box corner outside the visible circle. */
51
+ .btn-badge[variant="dot"] {
52
+ top: 2px;
53
+ right: 2px;
29
54
  }
30
55
 
31
56
  button {
@@ -293,6 +318,8 @@ export class RelButton extends LitElement {
293
318
  }
294
319
 
295
320
  render() {
321
+ const showBadge = this.badgeDot || !!this.badge;
322
+
296
323
  return html`
297
324
  <button
298
325
  class="${this.classes}"
@@ -303,6 +330,14 @@ export class RelButton extends LitElement {
303
330
  >
304
331
  <slot></slot>
305
332
  </button>
333
+ ${showBadge
334
+ ? html`<rel-badge
335
+ class="btn-badge"
336
+ variant=${this.badgeDot ? "dot" : "number"}
337
+ number=${this.badge}
338
+ size="sm"
339
+ ></rel-badge>`
340
+ : null}
306
341
  `;
307
342
  }
308
343
  }
@@ -17,17 +17,43 @@ export class RelDatePicker extends LitElement {
17
17
  @property({ type: String }) label: string | null = null;
18
18
  @property({ type: Boolean, reflect: true }) disabled = false;
19
19
  @property({ type: Boolean }) required = false;
20
+ @property({ type: Boolean, reflect: true }) readonly = false;
21
+
22
+ // Date bounds. minDate/maxDate are the standard names; min/max are kept as
23
+ // deprecated aliases (minDate/maxDate win when both are set).
24
+ @property({ type: String, attribute: "min-date" }) minDate: string | null = null;
25
+ @property({ type: String, attribute: "max-date" }) maxDate: string | null = null;
26
+ /** @deprecated use minDate */
20
27
  @property({ type: String }) min: string | null = null;
28
+ /** @deprecated use maxDate */
21
29
  @property({ type: String }) max: string | null = null;
30
+
22
31
  @property({ type: String, attribute: "week-start" }) weekStart: "sun" | "mon" = "sun";
23
32
  @property({ type: String }) locale = "en-US";
24
33
  @property({ type: Boolean, reflect: true }) open = false;
25
34
  @property({ type: Boolean, attribute: "close-on-select" }) closeOnSelect = true;
26
35
  @property({ type: Boolean }) clearable = false;
27
36
 
37
+ // ── Standardized input API (mirrors rel-textfield) ──
38
+ @property({ type: Boolean }) error = false;
39
+ @property({ type: String, attribute: "error-message" }) errorMessage?: string;
40
+ @property({ type: String, attribute: "helper-text" }) helperText?: string;
41
+ @property({ type: String }) name?: string;
42
+ @property({ type: String }) size: "small" | "medium" | "large" = "medium";
43
+ @property({ type: String, reflect: true }) variant: "outlined" = "outlined";
44
+ @property({ type: Boolean, reflect: true }) fullWidth = false;
45
+
28
46
  // Custom disabled predicate (not settable via attribute)
29
47
  public isDateDisabled?: (date: Date) => boolean;
30
48
 
49
+ // minDate/maxDate supersede the deprecated min/max aliases
50
+ private get minBound(): string | null {
51
+ return this.minDate ?? this.min;
52
+ }
53
+ private get maxBound(): string | null {
54
+ return this.maxDate ?? this.max;
55
+ }
56
+
31
57
  @state() private viewYear: number = new Date().getFullYear();
32
58
  @state() private viewMonth: number = new Date().getMonth();
33
59
  @state() private focusedDate: Date | null = null;
@@ -40,11 +66,17 @@ export class RelDatePicker extends LitElement {
40
66
 
41
67
  static styles = css`
42
68
  :host {
43
- display: inline-block;
69
+ display: block;
44
70
  font-family: var(--rel-fontFamilies-primary);
45
71
  position: relative;
46
72
  }
47
73
 
74
+ /* Intrinsic width by default; set fullWidth to fill the parent. */
75
+ :host(:not([fullwidth])) {
76
+ display: inline-block;
77
+ width: auto;
78
+ }
79
+
48
80
  :host([disabled]) {
49
81
  opacity: 0.6;
50
82
  cursor: not-allowed;
@@ -54,6 +86,10 @@ export class RelDatePicker extends LitElement {
54
86
  pointer-events: none;
55
87
  }
56
88
 
89
+ :host([readonly]) .trigger-button {
90
+ cursor: default;
91
+ }
92
+
57
93
  .date-picker-wrapper {
58
94
  display: flex;
59
95
  flex-direction: column;
@@ -66,6 +102,21 @@ export class RelDatePicker extends LitElement {
66
102
  color: var(--rel-text-heading);
67
103
  }
68
104
 
105
+ .required-mark {
106
+ color: var(--rel-error-surface-default, #cc0000);
107
+ margin-left: 2px;
108
+ }
109
+
110
+ /* Support text (helperText / errorMessage) */
111
+ .date-picker-support {
112
+ font-size: var(--rel-fontSizes-xs);
113
+ color: var(--rel-color-text-placeholder);
114
+ }
115
+
116
+ .date-picker-support.error {
117
+ color: var(--rel-error-surface-default, #cc0000);
118
+ }
119
+
69
120
  .trigger-button {
70
121
  width: 100%;
71
122
  min-width: 200px;
@@ -100,6 +151,28 @@ export class RelDatePicker extends LitElement {
100
151
  background: var(--rel-surface-secondary);
101
152
  }
102
153
 
154
+ /* Error state */
155
+ .trigger-button.error,
156
+ .trigger-button.error:hover:not(:disabled) {
157
+ border-color: var(--rel-error-surface-default, #cc0000);
158
+ }
159
+
160
+ .trigger-button.error:focus-visible {
161
+ outline-color: var(--rel-error-surface-default, #cc0000);
162
+ border-color: var(--rel-error-surface-default, #cc0000);
163
+ }
164
+
165
+ /* Size variants (medium is the base .trigger-button style) */
166
+ .date-picker-wrapper.small .trigger-button {
167
+ padding: var(--rel-spacing-space-xs) var(--rel-spacing-space-sm);
168
+ font-size: var(--rel-fontSizes-sm);
169
+ }
170
+
171
+ .date-picker-wrapper.large .trigger-button {
172
+ padding: var(--rel-spacing-space-md) var(--rel-spacing-space-md);
173
+ font-size: var(--rel-fontSizes-lg);
174
+ }
175
+
103
176
  .trigger-content {
104
177
  flex: 1;
105
178
  color: var(--rel-text-primary);
@@ -428,13 +501,13 @@ export class RelDatePicker extends LitElement {
428
501
  private isDateDisabledInternal(date: Date): boolean {
429
502
  if (this.disabled) return true;
430
503
 
431
- if (this.min) {
432
- const minDate = this.parseISODate(this.min);
504
+ if (this.minBound) {
505
+ const minDate = this.parseISODate(this.minBound);
433
506
  if (minDate && date < minDate) return true;
434
507
  }
435
508
 
436
- if (this.max) {
437
- const maxDate = this.parseISODate(this.max);
509
+ if (this.maxBound) {
510
+ const maxDate = this.parseISODate(this.maxBound);
438
511
  if (maxDate && date > maxDate) return true;
439
512
  }
440
513
 
@@ -535,13 +608,13 @@ export class RelDatePicker extends LitElement {
535
608
  let startYear = currentYear - 100;
536
609
  let endYear = currentYear + 10;
537
610
 
538
- if (this.min) {
539
- const minDate = this.parseISODate(this.min);
611
+ if (this.minBound) {
612
+ const minDate = this.parseISODate(this.minBound);
540
613
  if (minDate) startYear = Math.max(startYear, minDate.getFullYear() - 5);
541
614
  }
542
615
 
543
- if (this.max) {
544
- const maxDate = this.parseISODate(this.max);
616
+ if (this.maxBound) {
617
+ const maxDate = this.parseISODate(this.maxBound);
545
618
  if (maxDate) endYear = Math.min(endYear, maxDate.getFullYear() + 5);
546
619
  }
547
620
 
@@ -553,7 +626,7 @@ export class RelDatePicker extends LitElement {
553
626
  }
554
627
 
555
628
  private toggleCalendarPopover() {
556
- if (this.disabled) return;
629
+ if (this.disabled || this.readonly) return;
557
630
  this.open = !this.open;
558
631
  this.dispatchEvent(
559
632
  new CustomEvent("rel-open-change", {
@@ -597,7 +670,7 @@ export class RelDatePicker extends LitElement {
597
670
  }
598
671
 
599
672
  private selectDay(day: CalendarDay) {
600
- if (day.isDisabled || this.disabled) return;
673
+ if (day.isDisabled || this.disabled || this.readonly) return;
601
674
 
602
675
  const newValue = this.formatISODate(day.date);
603
676
  this.value = newValue;
@@ -746,14 +819,34 @@ export class RelDatePicker extends LitElement {
746
819
  const months = this.getMonthsList();
747
820
  const years = this.getYearsList();
748
821
 
822
+ const isError = this.error;
823
+ const belowText = isError && this.errorMessage ? this.errorMessage : this.helperText;
824
+ const describedById = belowText ? "date-picker-desc" : undefined;
825
+ const wrapperClasses = [
826
+ "date-picker-wrapper",
827
+ this.size,
828
+ isError ? "error" : "",
829
+ this.readonly ? "readonly" : "",
830
+ ]
831
+ .filter(Boolean)
832
+ .join(" ");
833
+
749
834
  return html`
750
- <div class="date-picker-wrapper">
835
+ <div class="${wrapperClasses}">
751
836
  ${this.label
752
- ? html`<label class="label" id="date-picker-label">${this.label}</label>`
837
+ ? html`<label class="label" id="date-picker-label"
838
+ >${this.label}${this.required
839
+ ? html`<span class="required-mark" aria-hidden="true">*</span>`
840
+ : nothing}</label
841
+ >`
842
+ : nothing}
843
+
844
+ ${this.name
845
+ ? html`<input type="hidden" name=${this.name} .value=${this.value ?? ""} />`
753
846
  : nothing}
754
847
 
755
848
  <button
756
- class="trigger-button"
849
+ class="trigger-button ${isError ? "error" : ""}"
757
850
  type="button"
758
851
  ?disabled=${this.disabled}
759
852
  @click=${this.toggleCalendarPopover}
@@ -762,6 +855,9 @@ export class RelDatePicker extends LitElement {
762
855
  aria-haspopup="dialog"
763
856
  aria-expanded=${this.open}
764
857
  aria-required=${this.required}
858
+ aria-invalid=${isError}
859
+ aria-readonly=${this.readonly}
860
+ aria-describedby=${describedById ?? nothing}
765
861
  aria-labelledby=${this.label ? "date-picker-label" : nothing}
766
862
  >
767
863
  <span class="trigger-content ${this.value ? "" : "placeholder"}">
@@ -914,6 +1010,12 @@ export class RelDatePicker extends LitElement {
914
1010
  )}
915
1011
  </div>
916
1012
  </div>
1013
+
1014
+ ${belowText
1015
+ ? html`<div class="date-picker-support ${isError ? "error" : ""}" id=${describedById}>
1016
+ ${belowText}
1017
+ </div>`
1018
+ : nothing}
917
1019
  </div>
918
1020
  `;
919
1021
  }
@@ -6,6 +6,7 @@ import "../rel-badge/rel-badge";
6
6
  export interface TabItem {
7
7
  label: string;
8
8
  badge?: number;
9
+ badgeDot?: boolean;
9
10
  disabled?: boolean;
10
11
  }
11
12
 
@@ -14,6 +15,8 @@ export class RelTabItem extends LitElement {
14
15
  @property({ type: String }) label: string = "";
15
16
  @property({ type: String }) value: string = "";
16
17
  @property({ type: Number }) badge: number = 0;
18
+ /** Shows a small dot instead of a numbered badge — takes precedence over `badge`. */
19
+ @property({ type: Boolean, attribute: "badge-dot" }) badgeDot: boolean = false;
17
20
  @property({ type: Boolean }) disabled: boolean = false;
18
21
 
19
22
  render() {
@@ -106,11 +109,12 @@ export class RelTab extends LitElement {
106
109
  .filter((el): el is RelTabItem => el instanceof RelTabItem);
107
110
  }
108
111
 
109
- private get _effectiveTabs(): Array<{ label: string; badge?: number; disabled?: boolean }> {
112
+ private get _effectiveTabs(): Array<{ label: string; badge?: number; badgeDot?: boolean; disabled?: boolean }> {
110
113
  if (this._tabItems.length > 0) {
111
114
  return this._tabItems.map((item) => ({
112
115
  label: item.label,
113
116
  badge: item.badge || 0,
117
+ badgeDot: item.badgeDot,
114
118
  disabled: item.disabled,
115
119
  }));
116
120
  }
@@ -173,9 +177,11 @@ export class RelTab extends LitElement {
173
177
  @keydown="${(e: KeyboardEvent) => this.onKeyDown(e, i)}"
174
178
  >
175
179
  <span class="tab-label">${tab.label}</span>
176
- ${tab.badge
177
- ? html`<rel-badge number="${tab.badge}" variant="number" size="sm"></rel-badge>`
178
- : ""}
180
+ ${tab.badgeDot
181
+ ? html`<rel-badge variant="dot" size="sm"></rel-badge>`
182
+ : tab.badge
183
+ ? html`<rel-badge number="${tab.badge}" variant="number" size="sm"></rel-badge>`
184
+ : ""}
179
185
  <span class="tab-underline"></span>
180
186
  </div>
181
187
  `