@reliability-design/web 1.2.0 → 1.3.0

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.
@@ -1,96 +1,138 @@
1
- import "../../styles/vars-v2.css";
2
- import { LitElement, html, css } from "lit";
3
- import { customElement, property } from "lit/decorators.js";
4
-
5
- @customElement("rel-avatar")
6
- export class RelAvatar extends LitElement {
7
- @property({ type: String, reflect: true })
8
- size: "lg" | "md" | "sm" | "xs" = "md";
9
-
10
- @property({ type: String, reflect: true })
11
- type: "image" | "text" | "icon" = "image";
12
-
13
- @property({ type: String }) src = "";
14
- @property({ type: String }) text = "";
15
- @property({ type: String }) alt = "Avatar";
16
- @property({ type: String }) bg: string = "";
17
-
18
- static styles = css`
19
- :host {
20
- display: inline-flex;
21
- font-family: var(--rel-fontFamilies-primary);
22
- }
23
-
24
- .avatar {
25
- display: inline-flex;
26
- align-items: center;
27
- justify-content: center;
28
- border-radius: 50%;
29
- overflow: hidden;
30
-
31
- background-color: var(--rel-colors-gray-disabled-text);
32
- color: var(--rel-avatar-color, #ffffff);
33
- font-weight: var(--rel-fontWeights-bold);
34
-
35
- user-select: none;
36
- text-transform: uppercase;
37
- }
38
-
39
- /* Sizes */
40
- .avatar-lg {
41
- width: 40px;
42
- height: 40px;
43
- font-size: var(--rel-fontSizes-xl);
44
- }
45
-
46
- .avatar-md {
47
- width: 32px;
48
- height: 32px;
49
- font-size: var(--rel-fontSizes-lg);
50
- }
51
-
52
- .avatar-sm {
53
- width: 24px;
54
- height: 24px;
55
- font-size: var(--rel-fontSizes-md);
56
- }
57
-
58
- .avatar-xs {
59
- width: 18px;
60
- height: 18px;
61
- font-size: var(--rel-fontSizes-xs);
62
- }
63
-
64
- img {
65
- width: 100%;
66
- height: 100%;
67
- object-fit: cover;
68
- }
69
-
70
- .icon-slot {
71
- width: 100%;
72
- height: 100%;
73
- display: inline-flex;
74
- align-items: center;
75
- justify-content: center;
76
- color: #ffffff;
77
- }
78
-
79
- .icon-slot ::slotted(svg) {
80
- width: 50%;
81
- height: 50%;
82
- }
83
- `;
84
-
85
- render() {
86
- return html`
87
- <div class="avatar avatar-${this.size}" style=${this.bg ? `background-color: ${this.bg}` : ""}>
88
- ${this.type === "image" && this.src
89
- ? html`<img src=${this.src} alt=${this.alt} />`
90
- : this.type === "text"
91
- ? html`<span>${this.text}</span>`
92
- : html`<span class="icon-slot"><slot></slot></span>`}
93
- </div>
94
- `;
95
- }
96
- }
1
+ import "../../styles/vars-v2.css";
2
+ import { LitElement, html, css } from "lit";
3
+ import { customElement, property } from "lit/decorators.js";
4
+
5
+ @customElement("rel-avatar")
6
+ export class RelAvatar extends LitElement {
7
+ @property({ type: String, reflect: true })
8
+ size: "lg" | "md" | "sm" | "xs" = "md";
9
+
10
+ @property({ type: String, reflect: true })
11
+ type: "image" | "text" | "icon" = "image";
12
+
13
+ @property({ type: String, reflect: true })
14
+ variant: "fill" | "subtle" | "outline" = "fill";
15
+
16
+ @property({ type: String }) src = "";
17
+ @property({ type: String }) text = "";
18
+ @property({ type: String }) alt = "Avatar";
19
+ @property({ type: String }) bg: string = "";
20
+ @property({ type: String }) color: string = "";
21
+
22
+ static styles = css`
23
+ :host {
24
+ display: inline-flex;
25
+ font-family: var(--rel-fontFamilies-primary);
26
+ }
27
+
28
+ .avatar {
29
+ display: inline-flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ border-radius: 50%;
33
+ overflow: hidden;
34
+ box-sizing: border-box;
35
+ border: var(--rel-borders-width-stroke-100, 1px) solid transparent;
36
+ font-weight: var(--rel-fontWeights-semibold);
37
+ user-select: none;
38
+ text-transform: uppercase;
39
+ }
40
+
41
+ /* ── Variants ─────────────────────────────────────────────── */
42
+
43
+ :host([variant="fill"]) .avatar {
44
+ background-color: var(--rel-avatar-bg-fill, #0b0b0b);
45
+ color: var(--rel-avatar-color-fill, #ffffff);
46
+ }
47
+
48
+ :host([variant="subtle"]) .avatar {
49
+ background-color: var(--rel-avatar-bg-subtle, #f5f5f5);
50
+ color: var(--rel-avatar-color-subtle, #0b0b0b);
51
+ }
52
+
53
+ :host([variant="outline"]) .avatar {
54
+ background-color: transparent;
55
+ border-color: var(--rel-avatar-border-outline, var(--rel-color-border-default));
56
+ color: var(--rel-avatar-color-outline, #0b0b0b);
57
+ }
58
+
59
+ /* ── Sizes ────────────────────────────────────────────────── */
60
+
61
+ .avatar-lg {
62
+ width: 40px;
63
+ height: 40px;
64
+ font-size: var(--rel-fontSizes-lg); /* 20px */
65
+ }
66
+
67
+ .avatar-md {
68
+ width: 32px;
69
+ height: 32px;
70
+ font-size: 16px;
71
+ }
72
+
73
+ .avatar-sm {
74
+ width: 24px;
75
+ height: 24px;
76
+ font-size: var(--rel-fontSizes-sm); /* 12px */
77
+ }
78
+
79
+ .avatar-xs {
80
+ width: 20px;
81
+ height: 20px;
82
+ font-size: var(--rel-fontSizes-xs); /* 10px */
83
+ }
84
+
85
+ /* ── Image ────────────────────────────────────────────────── */
86
+
87
+ img {
88
+ width: 100%;
89
+ height: 100%;
90
+ object-fit: cover;
91
+ }
92
+
93
+ /* ── Icon slot ────────────────────────────────────────────── */
94
+
95
+ .icon-slot {
96
+ width: 100%;
97
+ height: 100%;
98
+ display: inline-flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ color: inherit;
102
+ }
103
+
104
+ .icon-slot ::slotted(svg) {
105
+ width: 50%;
106
+ height: 50%;
107
+ }
108
+
109
+ /* ── Dark mode ────────────────────────────────────────────────
110
+ :host() can never be preceded by a combinator inside a shadow-tree
111
+ stylesheet, so ":host-context(dark) :host([variant=...]) .avatar"
112
+ (the previous approach) is invalid and silently matches nothing.
113
+ Instead, redefine the same custom properties the variant rules
114
+ above already read via var(--token, fallback) — a plain
115
+ :host-context() selector with no compounding, guaranteed valid. ── */
116
+
117
+ :host-context([data-theme="dark"]) {
118
+ --rel-avatar-bg-fill: #ffffff;
119
+ --rel-avatar-color-fill: #0b0b0b;
120
+ --rel-avatar-bg-subtle: #2b2b2b;
121
+ --rel-avatar-color-subtle: #f5f5f5;
122
+ --rel-avatar-border-outline: #525252;
123
+ --rel-avatar-color-outline: #f5f5f5;
124
+ }
125
+ `;
126
+
127
+ render() {
128
+ return html`
129
+ <div class="avatar avatar-${this.size}" style=${[this.bg ? `background-color:${this.bg}` : "", this.color ? `color:${this.color}` : ""].filter(Boolean).join(";")}>
130
+ ${this.type === "image" && this.src
131
+ ? html`<img src=${this.src} alt=${this.alt} />`
132
+ : this.type === "text"
133
+ ? html`<span>${this.text}</span>`
134
+ : html`<span class="icon-slot"><slot></slot></span>`}
135
+ </div>
136
+ `;
137
+ }
138
+ }
@@ -25,6 +25,11 @@ A simple notification badge component displaying numeric counts or dot indicator
25
25
  <rel-badge number="1" size="sm"></rel-badge>
26
26
  <rel-badge number="2" size="md"></rel-badge>
27
27
  <rel-badge number="3" size="lg"></rel-badge>
28
+
29
+ <!-- Custom colors -->
30
+ <rel-badge number="3" color="#7d7d7d"></rel-badge>
31
+ <rel-badge number="3" color="teal" text-color="#1a1a1a"></rel-badge>
32
+ <rel-badge variant="dot" color="lavender"></rel-badge>
28
33
  ```
29
34
 
30
35
  ## API
@@ -35,6 +40,8 @@ A simple notification badge component displaying numeric counts or dot indicator
35
40
  | `number` | `string` | `""` | Text to display in number variant (can be numeric or text like "99+") |
36
41
  | `size` | `"sm" \| "md" \| "lg"` (reflected) | `"md"` | Size variant |
37
42
  | `variant` | `"dot" \| "number"` (reflected) | `"number"` | Display mode (number shows text, dot shows indicator only) |
43
+ | `color` | `string` (reflected) | `""` | Direct CSS color override for the badge background. Accepts any valid CSS color (hex, rgb, hsl, named, `var(...)`). Falls back to the default token when empty |
44
+ | `text-color` | `string` (reflected) | `""` | Direct CSS color override for the badge text/icon color. Accepts any valid CSS color. Falls back to white when empty |
38
45
 
39
46
  ## Events
40
47
  None.
@@ -16,6 +16,24 @@ export class RelBadge extends LitElement {
16
16
  @property({ type: String, reflect: true })
17
17
  variant: "dot" | "number" = "number";
18
18
 
19
+ /**
20
+ * Direct CSS color override for the badge background. Accepts any valid
21
+ * CSS color value: hex, rgb(...), hsl(...), named color, or var(--token).
22
+ *
23
+ * Clear it (set to `''`) to fall back to the default token.
24
+ */
25
+ @property({ type: String, reflect: true })
26
+ color = "";
27
+
28
+ /**
29
+ * Direct CSS color override for the badge text/icon color. Accepts any
30
+ * valid CSS color value: hex, rgb(...), hsl(...), named color, or var(--token).
31
+ *
32
+ * Clear it (set to `''`) to fall back to the default (white).
33
+ */
34
+ @property({ type: String, reflect: true, attribute: "text-color" })
35
+ textColor = "";
36
+
19
37
  static styles = css`
20
38
  :host {
21
39
  display: inline-flex;
@@ -27,8 +45,8 @@ export class RelBadge extends LitElement {
27
45
  justify-content: center;
28
46
  font-family: var(--rel-fontFamilies-primary, sans-serif);
29
47
  font-weight: var(--rel-fontWeights-semibold, 600);
30
- background-color: var(--rel-error-surface-default);
31
- color: #ffffff;
48
+ background-color: var(--rel-badge-bg, var(--rel-error-surface-default));
49
+ color: var(--rel-badge-color, #ffffff);
32
50
  border-radius: 50%;
33
51
  }
34
52
 
@@ -72,6 +90,24 @@ export class RelBadge extends LitElement {
72
90
  return ["badge", `badge-${this.size}`, `badge-${this.variant}`].join(" ");
73
91
  }
74
92
 
93
+ override updated(changedProperties: Map<PropertyKey, unknown>): void {
94
+ if (changedProperties.has("color")) {
95
+ if (this.color) {
96
+ this.style.setProperty("--rel-badge-bg", this.color);
97
+ } else {
98
+ this.style.removeProperty("--rel-badge-bg");
99
+ }
100
+ }
101
+
102
+ if (changedProperties.has("textColor")) {
103
+ if (this.textColor) {
104
+ this.style.setProperty("--rel-badge-color", this.textColor);
105
+ } else {
106
+ this.style.removeProperty("--rel-badge-color");
107
+ }
108
+ }
109
+ }
110
+
75
111
  render() {
76
112
  return html`
77
113
  <span class="${this.classes}" aria-label=${this.ariaLabel || `${this.number} ${this.suffix}`}>
@@ -76,6 +76,10 @@ export class RelButton extends LitElement {
76
76
  /* Disabled */
77
77
  :host([disabled]) {
78
78
  cursor: not-allowed;
79
+ /* Without this, clicks on a disabled button hit-test fall through the
80
+ inner button's pointer-events:none straight onto the host, which still
81
+ dispatches a normal click to any externally-attached listener. */
82
+ pointer-events: none;
79
83
  }
80
84
 
81
85
  :host([disabled]) button {
@@ -245,6 +249,27 @@ export class RelButton extends LitElement {
245
249
  /* btn-link: blue in both modes — no dark override */
246
250
  `;
247
251
 
252
+ connectedCallback() {
253
+ super.connectedCallback();
254
+ // Capture-phase, on the host itself: blocks programmatic el.click() calls
255
+ // and any other path that bypasses the inner button's pointer-events:none
256
+ // (e.g. CSS pointer-events doesn't stop .click(), which targets the host
257
+ // directly regardless of hit-testing).
258
+ this.addEventListener("click", this.handleHostClick, { capture: true });
259
+ }
260
+
261
+ disconnectedCallback() {
262
+ this.removeEventListener("click", this.handleHostClick, { capture: true });
263
+ super.disconnectedCallback();
264
+ }
265
+
266
+ private handleHostClick = (e: MouseEvent) => {
267
+ if (this.disabled) {
268
+ e.preventDefault();
269
+ e.stopImmediatePropagation();
270
+ }
271
+ };
272
+
248
273
  private get classes() {
249
274
  return [
250
275
  `btn-${this.variant}`,
@@ -261,13 +286,10 @@ export class RelButton extends LitElement {
261
286
  return;
262
287
  }
263
288
 
264
- // Re-emit click from host (Angular-friendly)
265
- this.dispatchEvent(
266
- new MouseEvent("click", {
267
- bubbles: true,
268
- composed: true,
269
- })
270
- );
289
+ // Native click events are already composed: true, so this bubbles
290
+ // through the shadow boundary to the host on its own — no re-dispatch
291
+ // needed. (A manual re-dispatch here previously caused every @click
292
+ // listener on <rel-button> to fire twice per real click.)
271
293
  }
272
294
 
273
295
  render() {
@@ -17,6 +17,9 @@ A compact chip component for displaying tags, labels, or selected options. Suppo
17
17
  <!-- Outline variant -->
18
18
  <rel-chip label="Outline Style" tone="blue" outline></rel-chip>
19
19
 
20
+ <!-- Small size variant -->
21
+ <rel-chip label="Small" size="sm" tone="blue"></rel-chip>
22
+
20
23
  <!-- With avatar -->
21
24
  <rel-chip id="avatarChip" label="John Doe" avatar="./user.jpg"></rel-chip>
22
25
 
@@ -43,6 +46,7 @@ A compact chip component for displaying tags, labels, or selected options. Suppo
43
46
  | Name | Type | Default | Description |
44
47
  |------|------|---------|-------------|
45
48
  | `label` | `string` | `"Label"` | Text displayed on the chip |
49
+ | `size` (reflected) | `"sm" \| "md"` | `"md"` | Chip size variant |
46
50
  | `tone` | `"default" \| "blue" \| "yellow" \| "red" \| "green"` | `"default"` | Color tone variant |
47
51
  | `leadingIcon` | `string` | - | SVG or HTML string for leading icon |
48
52
  | `trailingIcon` | `string` | - | SVG or HTML string for trailing icon |
@@ -103,6 +107,9 @@ None exposed.
103
107
  - Remove button focus: 2px white outline with 1px offset
104
108
 
105
109
  ## Notes / Edge Cases
110
+ - **Size variants**:
111
+ - **md** (default): `--rel-fontSizes-md` text, `--rel-spacing-space-2xs`/`--rel-spacing-space-xs` padding, 20px × 20px avatar, 16px × 16px icons
112
+ - **sm**: `--rel-fontSizes-sm` text, `--rel-spacing-space-3xs`/`--rel-spacing-space-2xs` padding, 16px × 16px avatar, 12px × 12px icons
106
113
  - Icons (`leadingIcon`, `trailingIcon`) must be passed as raw HTML/SVG strings via JavaScript (see examples)
107
114
  - Icons are injected using `renderIcon()` which creates a template element and clones the content
108
115
  - Avatar images are 20px × 20px circular (`border-radius: 50%`, `object-fit: cover`)