@reliability-design/web 1.0.3 → 1.1.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.
Files changed (126) hide show
  1. package/dist/components/rel-accordion/README.md +132 -0
  2. package/dist/components/rel-accordion/rel-accordion.ts +47 -16
  3. package/dist/components/rel-alert/README.md +141 -0
  4. package/dist/components/rel-alert/rel-alert.ts +101 -20
  5. package/dist/components/rel-avatar/README.md +119 -0
  6. package/dist/components/rel-avatar/rel-avatar.ts +3 -2
  7. package/dist/components/rel-badge/README.md +86 -0
  8. package/dist/components/rel-badge/rel-badge.ts +11 -7
  9. package/dist/components/rel-breadcrumb/README.md +102 -0
  10. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
  11. package/dist/components/rel-button/README.md +130 -0
  12. package/dist/components/rel-button/rel-button.ts +64 -8
  13. package/dist/components/rel-calendar/README.md +109 -0
  14. package/dist/components/rel-calendar/index.ts +1 -0
  15. package/dist/components/rel-calendar/rel-calendar.ts +467 -0
  16. package/dist/components/rel-card/README.md +94 -0
  17. package/dist/components/rel-card/rel-card.ts +12 -3
  18. package/dist/components/rel-cardmedia/README.md +127 -0
  19. package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
  20. package/dist/components/rel-carousel/README.md +166 -0
  21. package/dist/components/rel-carousel/index.ts +1 -0
  22. package/dist/components/rel-carousel/rel-carousel.ts +468 -0
  23. package/dist/components/rel-chart/README.md +286 -0
  24. package/dist/components/rel-chart/index.ts +1 -0
  25. package/dist/components/rel-chart/rel-chart.ts +736 -0
  26. package/dist/components/rel-checkbox/README.md +77 -0
  27. package/dist/components/rel-checkbox/rel-checkbox.ts +51 -12
  28. package/dist/components/rel-chip/README.md +114 -0
  29. package/dist/components/rel-chip/rel-chip.ts +83 -9
  30. package/dist/components/rel-container/README.md +99 -0
  31. package/dist/components/rel-date-picker/README.md +253 -0
  32. package/dist/components/rel-date-picker/index.ts +1 -0
  33. package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
  34. package/dist/components/rel-divider/README.md +100 -0
  35. package/dist/components/rel-divider/rel-divider.ts +9 -0
  36. package/dist/components/rel-drawer/README.md +200 -0
  37. package/dist/components/rel-drawer/index.ts +1 -0
  38. package/dist/components/rel-drawer/rel-drawer-footer.ts +29 -0
  39. package/dist/components/rel-drawer/rel-drawer.ts +612 -0
  40. package/dist/components/rel-kbd/README.md +151 -0
  41. package/dist/components/rel-kbd/rel-kbd.ts +118 -0
  42. package/dist/components/rel-layout/README.md +88 -0
  43. package/dist/components/rel-menu/README.md +151 -0
  44. package/dist/components/rel-menu/rel-menu.ts +64 -24
  45. package/dist/components/rel-menubar/README.md +329 -0
  46. package/dist/components/rel-menubar/index.ts +2 -0
  47. package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
  48. package/dist/components/rel-menubar/rel-menubar.ts +605 -0
  49. package/dist/components/rel-nav-menu/README.md +289 -0
  50. package/dist/components/rel-nav-menu/index.ts +3 -0
  51. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +240 -0
  52. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
  53. package/dist/components/rel-nav-menu/rel-nav-menu.ts +227 -0
  54. package/dist/components/rel-navbar/README.md +126 -0
  55. package/dist/components/rel-navbar/rel-navbar.ts +104 -0
  56. package/dist/components/rel-otp/README.md +440 -0
  57. package/dist/components/rel-otp/index.ts +1 -0
  58. package/dist/components/rel-otp/rel-otp.ts +504 -0
  59. package/dist/components/rel-pagination/README.md +410 -0
  60. package/dist/components/rel-pagination/index.ts +1 -0
  61. package/dist/components/rel-pagination/rel-pagination.ts +381 -0
  62. package/dist/components/rel-popover/README.md +568 -0
  63. package/dist/components/rel-popover/index.ts +2 -0
  64. package/dist/components/rel-popover/rel-popover-content.ts +55 -0
  65. package/dist/components/rel-popover/rel-popover.ts +330 -0
  66. package/dist/components/rel-progress/README.md +110 -0
  67. package/dist/components/rel-progress/rel-progress.ts +32 -5
  68. package/dist/components/rel-radio/README.md +120 -0
  69. package/dist/components/rel-radio/rel-radio.ts +31 -6
  70. package/dist/components/rel-resizable/README.md +140 -0
  71. package/dist/components/rel-resizable/index.ts +3 -0
  72. package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
  73. package/dist/components/rel-resizable/rel-resizable-handle.ts +145 -0
  74. package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
  75. package/dist/components/rel-segmented-button/README.md +141 -0
  76. package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
  77. package/dist/components/rel-select/README.md +107 -0
  78. package/dist/components/rel-select/rel-select.ts +28 -14
  79. package/dist/components/rel-sheet/README.md +224 -0
  80. package/dist/components/rel-sheet/rel-sheet.ts +684 -0
  81. package/dist/components/rel-sidebar/README.md +209 -0
  82. package/dist/components/rel-sidebar/rel-sidebar.ts +1166 -0
  83. package/dist/components/rel-slider/README.md +135 -0
  84. package/dist/components/rel-slider/rel-slider.ts +44 -9
  85. package/dist/components/rel-snackbar/README.md +134 -0
  86. package/dist/components/rel-snackbar/rel-snackbar.ts +109 -14
  87. package/dist/components/rel-stepper/README.md +160 -0
  88. package/dist/components/rel-stepper/rel-stepper.ts +38 -27
  89. package/dist/components/rel-switch/README.md +102 -0
  90. package/dist/components/rel-switch/rel-switch.ts +34 -12
  91. package/dist/components/rel-tab/README.md +163 -0
  92. package/dist/components/rel-tab/rel-tab.ts +79 -23
  93. package/dist/components/rel-table/README.md +178 -0
  94. package/dist/components/rel-table/rel-table-body.ts +21 -0
  95. package/dist/components/rel-table/rel-table-caption.ts +27 -0
  96. package/dist/components/rel-table/rel-table-cell.ts +42 -0
  97. package/dist/components/rel-table/rel-table-footer.ts +23 -0
  98. package/dist/components/rel-table/rel-table-head.ts +45 -0
  99. package/dist/components/rel-table/rel-table-header.ts +21 -0
  100. package/dist/components/rel-table/rel-table-row.ts +36 -0
  101. package/dist/components/rel-table/rel-table.ts +53 -0
  102. package/dist/components/rel-textfield/README.md +90 -0
  103. package/dist/components/rel-textfield/rel-textfield.ts +55 -39
  104. package/dist/components/rel-toast/README.md +148 -0
  105. package/dist/components/rel-toast/rel-toast.ts +63 -24
  106. package/dist/components/rel-tooltip/README.md +103 -0
  107. package/dist/components/rel-tooltip/rel-tooltip.ts +123 -80
  108. package/dist/components/rel-topbar/rel-topbar-actions.ts +23 -0
  109. package/dist/components/rel-topbar/rel-topbar-brand.ts +46 -0
  110. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +33 -0
  111. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +28 -0
  112. package/dist/components/rel-topbar/rel-topbar-item.ts +315 -0
  113. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +23 -0
  114. package/dist/components/rel-topbar/rel-topbar-profile.ts +248 -0
  115. package/dist/components/rel-topbar/rel-topbar-utilities.ts +23 -0
  116. package/dist/components/rel-topbar/rel-topbar.ts +329 -0
  117. package/dist/index.d.mts +831 -7
  118. package/dist/index.d.ts +831 -7
  119. package/dist/index.js +9500 -1564
  120. package/dist/index.js.map +1 -1
  121. package/dist/index.mjs +9240 -1344
  122. package/dist/index.mjs.map +1 -1
  123. package/dist/styles/vars.css +187 -25
  124. package/dist/vars-TBB4BMSD.css +346 -0
  125. package/package.json +2 -1
  126. package/dist/vars-6ZIUPIU4.css +0 -184
@@ -0,0 +1,135 @@
1
+ # rel-slider
2
+
3
+ ## Overview
4
+ A highly customizable range slider component supporting continuous and discrete modes. Features include tick marks, value bubble display, solid/dotted track styles, custom theming via CSS properties, and full accessibility support. Automatically snaps to steps in discrete mode.
5
+
6
+ ## Usage
7
+ ```html
8
+ <!-- Basic continuous slider -->
9
+ <rel-slider value="50" min="0" max="100"></rel-slider>
10
+
11
+ <!-- Discrete slider with ticks -->
12
+ <rel-slider
13
+ type="discrete"
14
+ value="25"
15
+ min="0"
16
+ max="100"
17
+ step="25">
18
+ </rel-slider>
19
+
20
+ <!-- Discrete slider with tick labels -->
21
+ <rel-slider
22
+ type="discrete"
23
+ value="3"
24
+ min="1"
25
+ max="5"
26
+ step="1"
27
+ showTickLabels>
28
+ </rel-slider>
29
+
30
+ <!-- Dotted track without value bubble -->
31
+ <rel-slider
32
+ value="60"
33
+ min="0"
34
+ max="100"
35
+ track="dotted"
36
+ showBubble="false">
37
+ </rel-slider>
38
+
39
+ <!-- Disabled slider -->
40
+ <rel-slider value="30" disabled></rel-slider>
41
+
42
+ <!-- Listen for value changes -->
43
+ <rel-slider id="volumeSlider" value="75" min="0" max="100"></rel-slider>
44
+ <script>
45
+ const slider = document.getElementById('volumeSlider');
46
+ slider.addEventListener('valueChange', (e) => {
47
+ console.log('New value:', e.detail.value);
48
+ });
49
+ </script>
50
+ ```
51
+
52
+ ## API
53
+
54
+ ### Attributes/Properties
55
+ | Name | Type | Default | Description |
56
+ |------|------|---------|-------------|
57
+ | `type` | `"continuous" \| "discrete"` | `"continuous"` | Slider mode: continuous allows any value, discrete snaps to steps |
58
+ | `min` | `number` | `0` | Minimum value |
59
+ | `max` | `number` | `100` | Maximum value |
60
+ | `step` | `number` | `1` | Step increment (used in discrete mode for snapping and ticks) |
61
+ | `value` | `number` | `0` | Current slider value |
62
+ | `disabled` | `boolean` | `false` | Disables the slider |
63
+ | `track` | `"solid" \| "dotted"` | `"solid"` | Visual style of the active track |
64
+ | `showTickLabels` | `boolean` | `false` | Show numeric labels at each tick (discrete mode only) |
65
+ | `showBubble` | `boolean` | `true` | Show value bubble marker above the thumb |
66
+
67
+ ## Events
68
+ | Event | Detail | Description |
69
+ |-------|--------|-------------|
70
+ | `valueChange` | `{ value: number }` | Fired when the slider value changes through user interaction |
71
+
72
+ ## Slots
73
+ Default slot not used.
74
+
75
+ ## CSS Parts
76
+ None exposed.
77
+
78
+ ## Tokens / CSS Variables
79
+
80
+ ### Component-Level Custom Properties
81
+ The component defines these CSS custom properties on `:host` which can be overridden:
82
+
83
+ - `--track-height` - Height of the slider track (default: `5px`)
84
+ - `--thumb-size` - Diameter of the thumb (default: `10px`)
85
+ - `--thumb-stroke` - Thickness of focus/active halo around thumb (default: `7px`)
86
+ - `--thumb-halo-color` - Color of thumb halo (default: `--rel-border-default`)
87
+ - `--tick-size` - Diameter of tick marks (default: `4px`)
88
+ - `--shadow` - Thumb drop shadow (default: `0 2px 4px rgba(0,0,0,0.25)`)
89
+ - `--halo` - Additional halo shadow (default: `0 2px 0 var(--rel-surface-surface), 0 6px 10px rgba(0,0,0,0.07)`)
90
+ - `--marker-width` - Width of value bubble (default: `24px`)
91
+ - `--marker-height` - Height of value bubble (default: `28px`)
92
+
93
+ ### Design Token References
94
+ - `--rel-fontFamilies-primary` - Font family
95
+ - `--rel-colors-gray-border` - Inactive track color
96
+ - `--rel-colors-black` - Active track and thumb color
97
+ - `--rel-colors-white` - Used in dotted track and tick marks
98
+ - `--rel-colors-gray-primary` - Tick mark color (inactive)
99
+ - `--rel-colors-gray-heading` - Tick label color
100
+ - `--rel-colors-gray-placeholder` - Disabled active track and tick colors
101
+ - `--rel-border-default` - Default thumb halo color
102
+ - `--rel-surface-surface` - Disabled track background, used in dotted pattern
103
+ - `--rel-fontSizes-xs` - Tick label font size
104
+
105
+ ## Accessibility
106
+ - Uses native `<input type="range">` for full keyboard and screen reader support
107
+ - Native input is transparent overlay, custom visuals beneath are purely decorative
108
+ - Focus visible state shows halo around custom thumb
109
+ - Disabled state prevents interaction with styled cursor and reduced contrast
110
+ - Respects `prefers-reduced-motion` media query (disables transitions)
111
+
112
+ ### Keyboard Interactions
113
+ - `Arrow Left/Down` - Decrease value
114
+ - `Arrow Right/Up` - Increase value
115
+ - `Home` - Jump to minimum value
116
+ - `End` - Jump to maximum value
117
+ - `Page Up/Down` - Larger increments (browser-dependent)
118
+
119
+ ### Focus Behavior
120
+ - Focus shows halo around custom thumb using `--thumb-halo-color`
121
+ - Active (mouse down) state also shows the halo
122
+
123
+ ## Notes / Edge Cases
124
+ - In **discrete mode**, the slider automatically snaps to the nearest step value as the user drags
125
+ - Tick marks are generated automatically in discrete mode based on `min`, `max`, and `step`
126
+ - If the range doesn't divide evenly by step, a final tick is added at `max`
127
+ - Ticks change color (become white) when the value passes them (active state)
128
+ - **showTickLabels** only has effect in discrete mode
129
+ - **showBubble** displays a pin-drop shaped SVG marker with the current value as text
130
+ - The value bubble uses the "Poppins" font (you may need to load this separately or it will fall back)
131
+ - **Dotted track** uses a `radial-gradient` pattern with white dots over the active color
132
+ - The component has a fixed height of `60px` to accommodate the value bubble
133
+ - When disabled, all interactive colors become muted gray tones
134
+ - Active track width updates dynamically based on the percentage value
135
+ - The component respects `prefers-reduced-motion` and disables all transitions when requested
@@ -26,20 +26,23 @@ export class RelSlider extends LitElement {
26
26
 
27
27
  @state() private ticks: number[] = [];
28
28
 
29
+ private _onChange: (value: number) => void = () => {};
30
+ private _onTouched: () => void = () => {};
31
+
29
32
  static styles = css`
30
33
  :host {
31
34
  --track-height: 5px;
32
35
  --thumb-size: 10px;
33
36
  --thumb-stroke: 7px;
34
- --thumb-halo-color: rgba(224, 222, 222, 0.8);
37
+ --thumb-halo-color: var(--rel-border-default);
35
38
  --tick-size: 4px;
36
39
  --shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
37
- --halo: 0 2px 0 #e9e9e9, 0 6px 10px rgba(0, 0, 0, 0.07);
40
+ --halo: 0 2px 0 var(--rel-surface-surface), 0 6px 10px rgba(0, 0, 0, 0.07);
38
41
  --marker-width: 24px;
39
42
  --marker-height: 28px;
40
43
 
41
44
  display: block;
42
- font-family: var(--rel-fontFamilies-primary);
45
+ font-family: var(--rel-fontFamilies-primary, sans-serif);
43
46
  }
44
47
 
45
48
  .rel-slider {
@@ -112,6 +115,12 @@ export class RelSlider extends LitElement {
112
115
  border: none;
113
116
  }
114
117
 
118
+ .slider-input::slider-track {
119
+ background: transparent;
120
+ height: 100%;
121
+ border: none;
122
+ }
123
+
115
124
  /* Hide native thumb */
116
125
  .slider-input::-webkit-slider-thumb {
117
126
  -webkit-appearance: none;
@@ -131,6 +140,26 @@ export class RelSlider extends LitElement {
131
140
  cursor: pointer;
132
141
  }
133
142
 
143
+ .slider-input::slider-thumb {
144
+ width: 1px;
145
+ height: 1px;
146
+ background: transparent;
147
+ border: none;
148
+ cursor: pointer;
149
+ }
150
+
151
+ .slider-input:focus-visible ~ .thumb-container .custom-thumb {
152
+ box-shadow: 0 0 0 var(--thumb-stroke) var(--thumb-halo-color),
153
+ var(--shadow), var(--halo);
154
+ }
155
+
156
+ @media (prefers-reduced-motion: reduce) {
157
+ .custom-thumb,
158
+ .tick {
159
+ transition: none;
160
+ }
161
+ }
162
+
134
163
  /* Custom Thumb Container */
135
164
  .thumb-container {
136
165
  position: absolute;
@@ -225,7 +254,7 @@ export class RelSlider extends LitElement {
225
254
  .rel-slider.disabled .track-active.dotted {
226
255
  background: radial-gradient(
227
256
  circle,
228
- #eaeaea 0 1px,
257
+ var(--rel-surface-surface) 0 1px,
229
258
  transparent 1px
230
259
  )
231
260
  center/12px 4px repeat-x,
@@ -246,7 +275,7 @@ export class RelSlider extends LitElement {
246
275
 
247
276
  .rel-slider.disabled .custom-thumb {
248
277
  background: var(--rel-colors-gray-placeholder);
249
- box-shadow: 0 0 0 var(--thumb-stroke) #f5f5f5;
278
+ box-shadow: 0 0 0 var(--thumb-stroke) var(--rel-surface-surface);
250
279
  }
251
280
 
252
281
  .rel-slider.disabled .value-marker svg path {
@@ -318,14 +347,19 @@ export class RelSlider extends LitElement {
318
347
  this.value = next;
319
348
  this.updateActiveWidth();
320
349
  this.dispatchEvent(
321
- new CustomEvent("valueChange", {
350
+ new CustomEvent("rel-change", {
322
351
  detail: { value: this.value },
323
352
  bubbles: true,
324
353
  composed: true,
325
354
  })
326
355
  );
356
+ this._onChange(this.value);
327
357
  }
328
358
 
359
+ registerOnChange(fn: (value: number) => void): void { this._onChange = fn; }
360
+ registerOnTouched(fn: () => void): void { this._onTouched = fn; }
361
+ writeValue(value: number): void { this.value = value; }
362
+
329
363
  private updateActiveWidth() {
330
364
  const activeEl = this.shadowRoot?.querySelector(".track-active") as HTMLDivElement;
331
365
  if (activeEl) {
@@ -347,12 +381,12 @@ export class RelSlider extends LitElement {
347
381
  <svg viewBox="0 0 24 32" xmlns="http://www.w3.org/2000/svg">
348
382
  <path
349
383
  d="M12 0C18.6274 0 24 5.37258 24 12C24 21 12 32 12 32C12 32 0 21 0 12C0 5.37258 5.37258 0 12 0Z"
350
- fill="black" />
384
+ fill="var(--rel-colors-black)" />
351
385
  <text
352
386
  x="12"
353
387
  y="16"
354
388
  text-anchor="middle"
355
- fill="white"
389
+ fill="var(--rel-colors-white)"
356
390
  font-size="12"
357
391
  font-family="Poppins"
358
392
  font-weight="600">
@@ -429,7 +463,8 @@ export class RelSlider extends LitElement {
429
463
  .step="${this.type === 'discrete' ? String(this.step) : '1'}"
430
464
  .value="${String(this.value)}"
431
465
  ?disabled="${this.disabled}"
432
- @input="${this.onInput}" />
466
+ @input="${this.onInput}"
467
+ @blur="${() => this._onTouched()}" />
433
468
 
434
469
  ${this.renderThumbContainer()}
435
470
  ${this.renderTicks()}
@@ -0,0 +1,134 @@
1
+ # rel-snackbar
2
+
3
+ ## Overview
4
+ A bottom-anchored notification component for brief messages with optional action buttons. Supports single/double line layouts, configurable auto-dismiss duration, longer action mode (2x duration), and optional close icon. Features slide-up animation respecting user motion preferences.
5
+
6
+ ## Usage
7
+ ```html
8
+ <!-- Basic snackbar -->
9
+ <rel-snackbar message="File uploaded successfully"></rel-snackbar>
10
+
11
+ <!-- With action button -->
12
+ <rel-snackbar
13
+ id="undoSnackbar"
14
+ message="Item deleted"
15
+ action="UNDO">
16
+ </rel-snackbar>
17
+ <script>
18
+ const snackbar = document.getElementById('undoSnackbar');
19
+ snackbar.addEventListener('actionClick', () => {
20
+ console.log('Undo action clicked');
21
+ // Perform undo logic
22
+ });
23
+ </script>
24
+
25
+ <!-- Two-line layout -->
26
+ <rel-snackbar
27
+ message="Your message is too long to fit on a single line, so we'll use two."
28
+ lines="double"
29
+ action="DISMISS">
30
+ </rel-snackbar>
31
+
32
+ <!-- Longer action (2x duration) -->
33
+ <rel-snackbar
34
+ message="Changes saved"
35
+ action="VIEW"
36
+ longerAction>
37
+ </rel-snackbar>
38
+
39
+ <!-- Without close icon -->
40
+ <rel-snackbar
41
+ message="Processing..."
42
+ iconClose="false"
43
+ duration="0">
44
+ </rel-snackbar>
45
+
46
+ <!-- Custom duration (10 seconds) -->
47
+ <rel-snackbar
48
+ message="Important notification"
49
+ duration="10000"
50
+ action="GOT IT">
51
+ </rel-snackbar>
52
+
53
+ <!-- Listen for close event -->
54
+ <rel-snackbar id="notif" message="Update available"></rel-snackbar>
55
+ <script>
56
+ const notif = document.getElementById('notif');
57
+ notif.addEventListener('close', () => {
58
+ console.log('Snackbar closed');
59
+ });
60
+ </script>
61
+ ```
62
+
63
+ ## API
64
+
65
+ ### Attributes/Properties
66
+ | Name | Type | Default | Description |
67
+ |------|------|---------|-------------|
68
+ | `message` | `string` | `"This is a snackbar message"` | Primary message text |
69
+ | `action` | `string` | - | Optional action button label (e.g., "UNDO", "RETRY") |
70
+ | `longerAction` | `boolean` | `false` | If true, doubles the auto-dismiss duration |
71
+ | `iconClose` | `boolean` | `true` | Whether to show the close (✖) icon button |
72
+ | `lines` | `"single" \| "double"` | `"single"` | Layout mode (single-line flex-row, double-line flex-column) |
73
+ | `duration` | `number` | `4000` | Auto-dismiss duration in milliseconds (0 = no auto-dismiss) |
74
+
75
+ ## Events
76
+ | Event | Detail | Description |
77
+ |-------|--------|-------------|
78
+ | `actionClick` | None | Fired when the action button is clicked |
79
+ | `close` | None | Fired when snackbar is closed (via action, close button, or auto-dismiss) |
80
+
81
+ ## Slots
82
+ Default slot not used.
83
+
84
+ ## CSS Parts
85
+ None exposed.
86
+
87
+ ## Tokens / CSS Variables
88
+
89
+ ### Semantic Tokens
90
+ - `--rel-fontFamilies-primary` - Font family
91
+ - `--rel-fontSizes-sm` - Font size
92
+ - `--rel-fontWeights-semibold` - Action button font weight
93
+ - `--rel-lineHeights-lineheight-200` - Message line height
94
+
95
+ ### Spacing Tokens
96
+ - `--rel-spacing-space-2xs` - Action button gap, close button padding
97
+
98
+ ### Color Tokens
99
+ - `--rel-colors-black` - Background color
100
+ - `--rel-colors-white` - Text and focus outline color
101
+ - `--rel-colors-green-secondary` - Action button text color
102
+
103
+ ### Shadow Tokens
104
+ - `--rel-shadows-shadow-300` - Drop shadow
105
+
106
+ ### Border Tokens
107
+ - `--rel-borders-radius-border-radius-200` - Border radius
108
+
109
+ ## Accessibility
110
+ - Snackbars are informational and do not interrupt workflows
111
+ - Close and action buttons have proper focus styles
112
+ - Animation respects `prefers-reduced-motion` media query
113
+
114
+ ### Keyboard Interactions
115
+ - `Tab` - Focus action button or close button
116
+ - `Space/Enter` - Activate focused button
117
+
118
+ ### Focus Behavior
119
+ - Buttons show 2px white outline with 2px offset when focused
120
+ - Border radius applied to maintain visual consistency
121
+
122
+ ## Notes / Edge Cases
123
+ - Snackbar has a minimum width of `280px` and maximum width of `500px`
124
+ - **Single-line mode** (`lines="single"`): message and actions are flex-row with `justify-content: space-between`
125
+ - **Double-line mode** (`lines="double"`): layout switches to flex-column with actions aligned to the right
126
+ - Auto-dismiss timer starts in `connectedCallback()` and resets when `duration` or `longerAction` changes
127
+ - If `longerAction` is true, the effective duration is `duration * 2`
128
+ - Setting `duration="0"` disables auto-dismiss
129
+ - Clicking the action button automatically closes the snackbar after dispatching `actionClick`
130
+ - Clicking the close icon dispatches `close` and hides the snackbar (`visible = false`)
131
+ - When `visible` is false, `hidden` class applies `display: none`
132
+ - The component includes a `slide-up` animation (50px translateY → 0) on appearance
133
+ - Animation disabled when `prefers-reduced-motion: reduce` is set
134
+ - Snackbar is `display: block` by default (should be positioned by parent or global styles)
@@ -2,6 +2,7 @@ import { LitElement, html, css } from "lit";
2
2
  import { customElement, property, state } from "lit/decorators.js";
3
3
 
4
4
  export type SnackbarLines = "single" | "double";
5
+ export type SnackbarType = "default" | "success" | "error" | "warning";
5
6
 
6
7
  @customElement("rel-snackbar")
7
8
  export class RelSnackbar extends LitElement {
@@ -17,9 +18,12 @@ export class RelSnackbar extends LitElement {
17
18
 
18
19
  @property({ type: Number }) duration: number = 4000;
19
20
 
20
- @state() private visible: boolean = true;
21
+ @property({ type: String }) type: SnackbarType = "default";
22
+ @property({ type: Boolean, reflect: true }) show = false;
21
23
 
22
- private timeout: number | null = null;
24
+ @state() private visible: boolean = false;
25
+
26
+ private _dismissTimer: ReturnType<typeof setTimeout> | null = null;
23
27
 
24
28
  static styles = css`
25
29
  :host {
@@ -41,6 +45,7 @@ export class RelSnackbar extends LitElement {
41
45
  font-family: var(--rel-fontFamilies-primary);
42
46
  box-shadow: var(--rel-shadows-shadow-300);
43
47
  animation: slide-up 0.3s ease-out;
48
+ border-left: 4px solid transparent;
44
49
  }
45
50
 
46
51
  .snackbar.two-line {
@@ -52,6 +57,47 @@ export class RelSnackbar extends LitElement {
52
57
  display: none;
53
58
  }
54
59
 
60
+ /* Type variants — border accent using semantic DS tokens */
61
+ .snackbar.type-success {
62
+ border-left-color: var(--rel-colors-green-600, #16a34a);
63
+ }
64
+
65
+ .snackbar.type-error {
66
+ border-left-color: var(--rel-colors-red-600, #dc2626);
67
+ }
68
+
69
+ .snackbar.type-warning {
70
+ border-left-color: var(--rel-colors-amber-600, #d97706);
71
+ }
72
+
73
+ /* Icon color matches type accent */
74
+ .snackbar.type-success .snackbar-icon {
75
+ color: var(--rel-colors-green-400, #4ade80);
76
+ }
77
+
78
+ .snackbar.type-error .snackbar-icon {
79
+ color: var(--rel-colors-red-400, #f87171);
80
+ }
81
+
82
+ .snackbar.type-warning .snackbar-icon {
83
+ color: var(--rel-colors-amber-400, #fbbf24);
84
+ }
85
+
86
+ .snackbar-icon {
87
+ display: flex;
88
+ align-items: center;
89
+ margin-right: 10px;
90
+ flex-shrink: 0;
91
+ font-size: 14px;
92
+ }
93
+
94
+ .snackbar-body {
95
+ display: flex;
96
+ align-items: center;
97
+ flex: 1;
98
+ min-width: 0;
99
+ }
100
+
55
101
  .snackbar-message {
56
102
  font-size: var(--rel-fontSizes-sm);
57
103
  line-height: var(--rel-lineHeights-lineheight-200);
@@ -80,7 +126,7 @@ export class RelSnackbar extends LitElement {
80
126
  .snackbar-action {
81
127
  background: none;
82
128
  border: none;
83
- color: var(--rel-colors-green-secondary);
129
+ color: var(--rel-colors-green-400, #4ade80);
84
130
  font-weight: var(--rel-fontWeights-semibold);
85
131
  font-size: var(--rel-fontSizes-sm);
86
132
  font-family: var(--rel-fontFamilies-primary);
@@ -97,10 +143,16 @@ export class RelSnackbar extends LitElement {
97
143
  opacity: 0.6;
98
144
  }
99
145
 
146
+ .snackbar-action:focus-visible {
147
+ outline: 2px solid var(--rel-colors-white, #fff);
148
+ outline-offset: 2px;
149
+ border-radius: 2px;
150
+ }
151
+
100
152
  .snackbar-close {
101
153
  background: none;
102
154
  border: none;
103
- color: var(--rel-colors-white);
155
+ color: var(--rel-colors-white, #fff);
104
156
  cursor: pointer;
105
157
  font-size: 16px;
106
158
  padding: 0;
@@ -120,6 +172,23 @@ export class RelSnackbar extends LitElement {
120
172
  opacity: 0.6;
121
173
  }
122
174
 
175
+ .snackbar-close:focus-visible {
176
+ outline: 2px solid var(--rel-colors-white, #fff);
177
+ outline-offset: 2px;
178
+ border-radius: 2px;
179
+ }
180
+
181
+ /* Prevent any inherited transform from firing on button click */
182
+ .snackbar button:active {
183
+ transform: none;
184
+ }
185
+
186
+ @media (prefers-reduced-motion: reduce) {
187
+ .snackbar {
188
+ animation: none;
189
+ }
190
+ }
191
+
123
192
  @keyframes slide-up {
124
193
  from {
125
194
  transform: translateY(50px);
@@ -134,32 +203,42 @@ export class RelSnackbar extends LitElement {
134
203
 
135
204
  connectedCallback() {
136
205
  super.connectedCallback();
137
- this.setAutoDismiss();
206
+ }
207
+
208
+ willUpdate(changed: Map<string, unknown>) {
209
+ if (!changed.has('show')) return;
210
+ if (this.show) {
211
+ this.visible = true;
212
+ this.setAutoDismiss();
213
+ } else {
214
+ this.visible = false;
215
+ this.clearDismissTimer();
216
+ }
138
217
  }
139
218
 
140
219
  updated(changed: Map<string, unknown>) {
141
- if (changed.has("duration") || changed.has("longerAction")) {
220
+ if ((changed.has("duration") || changed.has("longerAction")) && this.show) {
142
221
  this.setAutoDismiss();
143
222
  }
144
223
  }
145
224
 
146
225
  disconnectedCallback() {
147
226
  super.disconnectedCallback();
148
- this.clearTimeout();
227
+ this.clearDismissTimer();
149
228
  }
150
229
 
151
230
  private setAutoDismiss() {
152
231
  if (this.duration > 0) {
153
- this.clearTimeout();
232
+ this.clearDismissTimer();
154
233
  const delay = this.longerAction ? this.duration * 2 : this.duration;
155
- this.timeout = window.setTimeout(() => this.dismiss(), delay);
234
+ this._dismissTimer = setTimeout(() => this.dismiss(), delay);
156
235
  }
157
236
  }
158
237
 
159
- private clearTimeout() {
160
- if (this.timeout !== null) {
161
- window.clearTimeout(this.timeout);
162
- this.timeout = null;
238
+ private clearDismissTimer() {
239
+ if (this._dismissTimer !== null) {
240
+ clearTimeout(this._dismissTimer);
241
+ this._dismissTimer = null;
163
242
  }
164
243
  }
165
244
 
@@ -174,6 +253,7 @@ export class RelSnackbar extends LitElement {
174
253
  }
175
254
 
176
255
  private dismiss() {
256
+ this.show = false;
177
257
  this.visible = false;
178
258
  this.dispatchEvent(
179
259
  new CustomEvent("close", {
@@ -183,18 +263,33 @@ export class RelSnackbar extends LitElement {
183
263
  );
184
264
  }
185
265
 
266
+ private getIcon(): string {
267
+ switch (this.type) {
268
+ case "success": return "✓";
269
+ case "error": return "✕";
270
+ case "warning": return "⚠";
271
+ default: return "";
272
+ }
273
+ }
274
+
186
275
  render() {
187
276
  const snackbarClasses = [
188
277
  "snackbar",
189
278
  this.lines === "double" ? "two-line" : "",
190
279
  !this.visible ? "hidden" : "",
280
+ this.type !== "default" ? `type-${this.type}` : "",
191
281
  ]
192
282
  .filter(Boolean)
193
283
  .join(" ");
194
284
 
285
+ const icon = this.getIcon();
286
+
195
287
  return html`
196
288
  <div class="${snackbarClasses}">
197
- <div class="snackbar-message">${this.message}</div>
289
+ <div class="snackbar-body">
290
+ ${icon ? html`<span class="snackbar-icon">${icon}</span>` : ""}
291
+ <div class="snackbar-message">${this.message}</div>
292
+ </div>
198
293
 
199
294
  <div class="snackbar-actions">
200
295
  ${this.action