@reliability-design/web 1.4.3 → 1.4.5

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 (48) hide show
  1. package/dist/components/rel-accordion/rel-accordion.ts +13 -1
  2. package/dist/components/rel-autocomplete/README.md +1 -0
  3. package/dist/components/rel-autocomplete/rel-autocomplete.ts +56 -1
  4. package/dist/components/rel-button/rel-button.ts +18 -2
  5. package/dist/components/rel-calendar/rel-calendar.ts +23 -1
  6. package/dist/components/rel-carousel/rel-carousel.ts +20 -0
  7. package/dist/components/rel-checkbox/README.md +69 -22
  8. package/dist/components/rel-checkbox/rel-checkbox.ts +119 -9
  9. package/dist/components/rel-chip/rel-chip.ts +29 -0
  10. package/dist/components/rel-date-picker/rel-date-picker.ts +156 -13
  11. package/dist/components/rel-drawer/rel-drawer.ts +22 -58
  12. package/dist/components/rel-menu/rel-menu.ts +124 -15
  13. package/dist/components/rel-menubar/rel-menubar.ts +221 -63
  14. package/dist/components/rel-nav-menu/rel-nav-menu.ts +39 -3
  15. package/dist/components/rel-otp/rel-otp.ts +3 -3
  16. package/dist/components/rel-pagination/README.md +14 -0
  17. package/dist/components/rel-pagination/rel-pagination.ts +30 -14
  18. package/dist/components/rel-popover/rel-popover.ts +31 -3
  19. package/dist/components/rel-range-picker/rel-range-picker.ts +251 -3
  20. package/dist/components/rel-resizable/rel-resizable-group.ts +20 -0
  21. package/dist/components/rel-segmented-button/rel-segmented-button.ts +50 -12
  22. package/dist/components/rel-select/README.md +1 -0
  23. package/dist/components/rel-select/rel-select.ts +1385 -1170
  24. package/dist/components/rel-sheet/rel-sheet.ts +22 -1
  25. package/dist/components/rel-sidebar/rel-sidebar.ts +43 -39
  26. package/dist/components/rel-stepper/rel-stepper.ts +44 -1
  27. package/dist/components/rel-tab/rel-tab.ts +32 -7
  28. package/dist/components/rel-time-picker/rel-time-picker.ts +338 -206
  29. package/dist/components/rel-tooltip/rel-tooltip.ts +30 -0
  30. package/dist/components/rel-topbar/rel-topbar-actions.ts +1 -1
  31. package/dist/components/rel-topbar/rel-topbar-brand.ts +1 -1
  32. package/dist/components/rel-topbar/rel-topbar-dropdown-item.ts +1 -1
  33. package/dist/components/rel-topbar/rel-topbar-dropdown.ts +1 -1
  34. package/dist/components/rel-topbar/rel-topbar-item.ts +318 -319
  35. package/dist/components/rel-topbar/rel-topbar-mobile-panel.ts +1 -1
  36. package/dist/components/rel-topbar/rel-topbar-profile.ts +224 -224
  37. package/dist/components/rel-topbar/rel-topbar-utilities.ts +1 -1
  38. package/dist/components/rel-topbar/rel-topbar.ts +440 -440
  39. package/dist/components/rel-upload/UPLOAD-DESIGN-CHANGES.md +134 -0
  40. package/dist/components/rel-upload/rel-upload-actions.ts +163 -0
  41. package/dist/components/rel-upload/rel-upload.ts +943 -0
  42. package/dist/index.d.mts +491 -18
  43. package/dist/index.d.ts +491 -18
  44. package/dist/index.js +2748 -410
  45. package/dist/index.js.map +1 -1
  46. package/dist/index.mjs +2953 -627
  47. package/dist/index.mjs.map +1 -1
  48. package/package.json +1 -1
@@ -270,6 +270,9 @@ export class RelAccordion extends LitElement {
270
270
  }
271
271
  `;
272
272
 
273
+ /** Stable id prefix wiring the header to its panel. */
274
+ private accordionId = `rel-accordion-${Math.random().toString(36).slice(2)}`;
275
+
273
276
  private toggle() {
274
277
  this.open = !this.open;
275
278
  this.dispatchEvent(
@@ -286,9 +289,11 @@ export class RelAccordion extends LitElement {
286
289
  <div part="base" class="accordion">
287
290
  <div
288
291
  part="trigger" class="header"
292
+ id="${this.accordionId}-header"
289
293
  role="button"
290
294
  tabindex="0"
291
295
  aria-expanded="${this.open}"
296
+ aria-controls="${this.accordionId}-body"
292
297
  @click=${this.toggle}
293
298
  @keydown=${(e: KeyboardEvent) => {
294
299
  if (e.key === "Enter" || e.key === " ") {
@@ -315,7 +320,14 @@ export class RelAccordion extends LitElement {
315
320
  </slot>
316
321
  </div>
317
322
  </div>
318
- <div part="content" class="body ${this.open ? "open" : "closed"}">
323
+ <div
324
+ part="content"
325
+ class="body ${this.open ? "open" : "closed"}"
326
+ id="${this.accordionId}-body"
327
+ role="region"
328
+ aria-labelledby="${this.accordionId}-header"
329
+ ?inert=${!this.open}
330
+ >
319
331
  <slot></slot>
320
332
  </div>
321
333
  </div>
@@ -94,3 +94,4 @@ Two data modes:
94
94
  - Form validation is *displayed* (`error`, `error-message`, `required`), not performed — validity logic belongs to the form layer (e.g. Angular validators)
95
95
  - CSS parts: `panel`, `list`, `option`, plus re-exported textfield parts (`label`, `control`, `input`, `helper`, `right-icon`)
96
96
  - Panel styling matches `rel-select`'s listbox; override via `--rel-autocomplete-bg`, `--rel-autocomplete-menu-border`, `--rel-autocomplete-menu-radius`, `--rel-autocomplete-menu-shadow`, `--rel-autocomplete-option-hover`
97
+ - The open panel is promoted to the browser **top layer** (native Popover API, `popover="manual"`), so it renders correctly inside transformed / `overflow: hidden` containers — a centered `rel-modal`, a bottom `rel-drawer`. Engines without the Popover API fall back to `position: fixed` with a measure-and-cancel correction for transformed ancestors
@@ -143,6 +143,15 @@ export class RelAutocomplete extends LitElement {
143
143
  position: fixed;
144
144
  z-index: 1001;
145
145
  display: none;
146
+ /* Reset native [popover] UA styles (the panel is promoted to the
147
+ top layer so transformed ancestors — e.g. a centered rel-modal
148
+ panel — can't hijack its containing block or clip it). */
149
+ inset: auto;
150
+ margin: 0;
151
+ padding: 0;
152
+ width: auto;
153
+ height: auto;
154
+ color: inherit;
146
155
  box-sizing: border-box;
147
156
  background: var(--rel-autocomplete-bg, var(--rel-bg-neutral));
148
157
  border: 1px solid var(--rel-autocomplete-menu-border, var(--rel-border-default));
@@ -222,6 +231,7 @@ export class RelAutocomplete extends LitElement {
222
231
  disconnectedCallback() {
223
232
  super.disconnectedCallback();
224
233
  this.removeGlobalListeners();
234
+ this.hideNativePopover();
225
235
  clearTimeout(this.debounceTimer);
226
236
  }
227
237
 
@@ -309,7 +319,10 @@ export class RelAutocomplete extends LitElement {
309
319
  this.open = true;
310
320
  this.highlightedIndex = -1;
311
321
  this.addGlobalListeners();
312
- requestAnimationFrame(() => this.updatePosition());
322
+ requestAnimationFrame(() => {
323
+ this.showNativePopover();
324
+ this.updatePosition();
325
+ });
313
326
  }
314
327
 
315
328
  private closePanel() {
@@ -317,6 +330,35 @@ export class RelAutocomplete extends LitElement {
317
330
  this.open = false;
318
331
  this.highlightedIndex = -1;
319
332
  this.removeGlobalListeners();
333
+ this.hideNativePopover();
334
+ }
335
+
336
+ /* A transformed ancestor (e.g. rel-modal's centered panel with
337
+ * translate(-50%,-50%)) becomes the containing block for position:fixed
338
+ * descendants, breaking viewport-based coordinates and letting the
339
+ * panel's overflow:hidden clip the suggestions. Promoting the panel to
340
+ * the browser top layer sidesteps both — same fix as rel-popover.
341
+ * `manual` keeps light-dismiss and Escape handling under this
342
+ * component's own control. */
343
+
344
+ private showNativePopover() {
345
+ const p = this.panelEl;
346
+ if (!p || typeof p.showPopover !== "function") return;
347
+ try {
348
+ if (!p.matches(":popover-open")) p.showPopover();
349
+ } catch {
350
+ /* older engines / detached element — fixed-position fallback applies */
351
+ }
352
+ }
353
+
354
+ private hideNativePopover() {
355
+ const p = this.panelEl;
356
+ if (!p || typeof p.hidePopover !== "function") return;
357
+ try {
358
+ if (p.matches(":popover-open")) p.hidePopover();
359
+ } catch {
360
+ /* already hidden or unsupported — nothing to do */
361
+ }
320
362
  }
321
363
 
322
364
  private updatePosition = () => {
@@ -337,6 +379,18 @@ export class RelAutocomplete extends LitElement {
337
379
  left = Math.max(8, window.innerWidth - t.width - 8);
338
380
  }
339
381
 
382
+ // Without top-layer support a transformed ancestor may be the containing
383
+ // block for position:fixed, shifting the panel away from the viewport
384
+ // coordinates computed above. Apply, re-measure, and cancel the offset.
385
+ this.panelEl.style.top = `${top}px`;
386
+ this.panelEl.style.left = `${left}px`;
387
+ this.panelEl.style.width = `${t.width}px`;
388
+ const applied = this.panelEl.getBoundingClientRect();
389
+ top -= applied.top - top;
390
+ left -= applied.left - left;
391
+ this.panelEl.style.top = `${top}px`;
392
+ this.panelEl.style.left = `${left}px`;
393
+
340
394
  this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
341
395
  };
342
396
 
@@ -583,6 +637,7 @@ export class RelAutocomplete extends LitElement {
583
637
  id="${panelId}"
584
638
  part="panel list"
585
639
  class="ac-panel"
640
+ popover="manual"
586
641
  role="listbox"
587
642
  aria-label="${this.accessibleName || "Suggestions"}"
588
643
  style="${this.panelStyle}"
@@ -137,9 +137,12 @@ export class RelButton extends LitElement {
137
137
  }
138
138
  }
139
139
 
140
- /* Sizes */
140
+ /* Sizes. Min-widths come from the Figma Button master (S/M/L =
141
+ 80/100/120) so short labels like "OK" keep a proper pill shape;
142
+ --rel-button-min-width lets a consumer override per instance. */
141
143
  .size-small {
142
144
  height: 32px;
145
+ min-width: var(--rel-button-min-width, 80px);
143
146
  padding: 0 var(--rel-spacing-sm);
144
147
  font-family: var(--rel-action-md-font-family);
145
148
  font-weight: var(--rel-action-md-font-weight);
@@ -150,6 +153,7 @@ export class RelButton extends LitElement {
150
153
 
151
154
  .size-medium {
152
155
  height: 40px;
156
+ min-width: var(--rel-button-min-width, 100px);
153
157
  gap: var(--rel-spacing-xs);
154
158
  padding: 0 var(--rel-spacing-md);
155
159
  font-family: var(--rel-action-lg-font-family);
@@ -161,6 +165,7 @@ export class RelButton extends LitElement {
161
165
 
162
166
  .size-large {
163
167
  height: 48px;
168
+ min-width: var(--rel-button-min-width, 120px);
164
169
  padding: 0 var(--rel-spacing-lg);
165
170
  font-family: var(--rel-action-xl-font-family);
166
171
  font-weight: var(--rel-action-xl-font-weight);
@@ -267,6 +272,11 @@ export class RelButton extends LitElement {
267
272
  color: var(--rel-action-neutral-label-icons-ghost);
268
273
  }
269
274
 
275
+ /* Figma master: ghost S has a narrower 56px min than primary/outline. */
276
+ .btn-text.size-small {
277
+ min-width: var(--rel-button-min-width, 56px);
278
+ }
279
+
270
280
  .btn-text.tone-critical {
271
281
  color: var(--rel-action-danger-label-icons-ghost);
272
282
  }
@@ -275,6 +285,8 @@ export class RelButton extends LitElement {
275
285
  background: transparent;
276
286
  color: var(--rel-action-neutral-label-icons-link);
277
287
  text-decoration: underline;
288
+ /* Links are inline actions — no minimum footprint in the Figma master. */
289
+ min-width: 0;
278
290
  }
279
291
 
280
292
  button.btn-link:hover:not(:disabled) {
@@ -303,6 +315,7 @@ export class RelButton extends LitElement {
303
315
  /* Icon Only as an independent axis: square button of the current style */
304
316
  .btn-icon-only {
305
317
  padding: 0;
318
+ min-width: 0;
306
319
  }
307
320
 
308
321
  .btn-icon-only.size-small {
@@ -321,6 +334,7 @@ export class RelButton extends LitElement {
321
334
  width: 40px;
322
335
  height: 40px;
323
336
  padding: 0;
337
+ min-width: 0;
324
338
  border-radius: 50%;
325
339
  }
326
340
  .btn-icon.tone-default {
@@ -365,7 +379,9 @@ export class RelButton extends LitElement {
365
379
  .btn-text: ghost gets tone-accent hovers below (this generic grey overlay
366
380
  has higher specificity than the per-tone ghost rules and was overriding
367
381
  them, which is why every ghost hover looked grey). */
368
- button:hover:not(:disabled):not(.btn-primary):not(.btn-default):not(.btn-icon):not(.btn-secondary):not(.btn-static):not(.btn-text) {
382
+ /* Link buttons change label color only (rule above) — the overlay fill
383
+ here would make a hovered link read as a ghost button. */
384
+ button:hover:not(:disabled):not(.btn-primary):not(.btn-default):not(.btn-icon):not(.btn-secondary):not(.btn-static):not(.btn-text):not(.btn-link) {
369
385
  background: var(--rel-action-overlay-hover);
370
386
  }
371
387
 
@@ -426,9 +426,31 @@ export class RelCalendar extends LitElement {
426
426
  }
427
427
  }
428
428
 
429
+ /**
430
+ * Index of the single grid cell carrying tabindex="0". The old expression
431
+ * (`isSelected || (index === 0 && !focusedDate)`) left the grid with ZERO
432
+ * tab stops once the user arrowed around without a selection — Tab could
433
+ * never re-enter the calendar.
434
+ */
435
+ private getRovingIndex(days: CalendarDay[]): number {
436
+ const candidates = [
437
+ this.focusedDate
438
+ ? days.findIndex(
439
+ (d) => d.isCurrentMonth && this.isSameDay(d.date, this.focusedDate!)
440
+ )
441
+ : -1,
442
+ days.findIndex((d) => d.isSelected),
443
+ days.findIndex((d) => d.isCurrentMonth && d.isToday),
444
+ days.findIndex((d) => d.isCurrentMonth && !d.isDisabled),
445
+ ];
446
+ const found = candidates.find((i) => i >= 0);
447
+ return found ?? 0;
448
+ }
449
+
429
450
  render() {
430
451
  const days = this.getCalendarDays();
431
452
  const weekdayLabels = this.getWeekdayLabels();
453
+ const rovingIndex = this.getRovingIndex(days);
432
454
 
433
455
  return html`
434
456
  <div role="grid" aria-label="Calendar">
@@ -484,7 +506,7 @@ export class RelCalendar extends LitElement {
484
506
  ?disabled=${day.isDisabled}
485
507
  @click=${() => this.selectDay(day)}
486
508
  @keydown=${(e: KeyboardEvent) => this.handleKeyDown(e, day, index)}
487
- tabindex="${day.isSelected || (index === 0 && !this.focusedDate) ? 0 : -1}"
509
+ tabindex="${index === rovingIndex ? 0 : -1}"
488
510
  >
489
511
  ${day.day}
490
512
  </button>
@@ -251,6 +251,11 @@ export class RelCarousel extends LitElement {
251
251
  "(prefers-reduced-motion: reduce)"
252
252
  ).matches;
253
253
 
254
+ // WCAG 2.2.2: auto-advance must pause while keyboard focus is inside the
255
+ // carousel, not just on mouse hover.
256
+ this.addEventListener("focusin", this.handleFocusIn);
257
+ this.addEventListener("focusout", this.handleFocusOut);
258
+
254
259
  if (this.autoplay) {
255
260
  this.startAutoplay();
256
261
  }
@@ -285,6 +290,8 @@ export class RelCarousel extends LitElement {
285
290
 
286
291
  disconnectedCallback() {
287
292
  super.disconnectedCallback();
293
+ this.removeEventListener("focusin", this.handleFocusIn);
294
+ this.removeEventListener("focusout", this.handleFocusOut);
288
295
  this._ro?.disconnect();
289
296
  this.stopAutoplay();
290
297
  if (this.animationId) {
@@ -556,6 +563,18 @@ export class RelCarousel extends LitElement {
556
563
  }
557
564
  }
558
565
 
566
+ private handleFocusIn = () => {
567
+ if (this.autoplay) this.stopAutoplay();
568
+ };
569
+
570
+ private handleFocusOut = (e: FocusEvent) => {
571
+ if (!this.autoplay) return;
572
+ const next = e.relatedTarget as Node | null;
573
+ // Resume only once focus has fully left the carousel (light or shadow).
574
+ if (next && (this.contains(next) || this.shadowRoot?.contains(next))) return;
575
+ this.startAutoplay();
576
+ };
577
+
559
578
  private handleMouseEnter() {
560
579
  if (this.autoplay && this.pauseOnHover) {
561
580
  this.stopAutoplay();
@@ -667,6 +686,7 @@ export class RelCarousel extends LitElement {
667
686
  class="dot ${i === this.value ? "active" : ""}"
668
687
  part="dot"
669
688
  aria-label="Go to slide ${i + 1}"
689
+ aria-current="${i === this.value ? "true" : nothing}"
670
690
  @click=${() => this.goToSlide(i)}
671
691
  ></button>
672
692
  `)
@@ -1,20 +1,41 @@
1
1
  # rel-checkbox
2
2
 
3
3
  ## Overview
4
- A tri-state checkbox component supporting checked, unchecked, and indeterminate states. Includes label and supporting text with built-in error and disabled states for form validation scenarios.
4
+ A tri-state checkbox component supporting checked, unchecked, and indeterminate states. Includes label, optional supporting text, and optional label prefix icon, with built-in error and disabled states. Supports two sizes, two densities, and leading/trailing box alignment — matching the Figma Checkbox sheet vocabulary (Size, Density, Alignment) verbatim.
5
5
 
6
6
  ## Usage
7
7
  ```html
8
8
  <!-- Basic example -->
9
9
  <rel-checkbox label="Accept terms"></rel-checkbox>
10
10
 
11
- <!-- Example with common options -->
11
+ <!-- Supporting text, compact density (default: text on its own line, indented past the box) -->
12
12
  <rel-checkbox
13
13
  label="Subscribe to newsletter"
14
14
  supportingtext="We'll send you weekly updates"
15
15
  status="unchecked"
16
16
  state="default">
17
17
  </rel-checkbox>
18
+
19
+ <!-- Comfortable density: label + supporting text stack beside the box -->
20
+ <rel-checkbox
21
+ density="comfortable"
22
+ label="Notify the broker"
23
+ supportingtext="Sends an email when the POD is uploaded">
24
+ </rel-checkbox>
25
+
26
+ <!-- Trailing alignment: box at the far edge of the row (settings-row style).
27
+ Trailing always renders comfortable density — compact has no trailing layout. -->
28
+ <rel-checkbox
29
+ alignment="trailing"
30
+ label="Email notifications"
31
+ supportingtext="Weekly summary of load activity">
32
+ </rel-checkbox>
33
+
34
+ <!-- Small size -->
35
+ <rel-checkbox size="small" label="Include archived"></rel-checkbox>
36
+
37
+ <!-- Indeterminate (programmatic-only; user toggle resolves to checked/unchecked) -->
38
+ <rel-checkbox label="Select all" status="indeterminate"></rel-checkbox>
18
39
  ```
19
40
 
20
41
  ## API
@@ -23,42 +44,62 @@ A tri-state checkbox component supporting checked, unchecked, and indeterminate
23
44
  | Name | Type | Default | Description |
24
45
  |------|------|---------|-------------|
25
46
  | `label` | `string` | `"Label"` | Label text displayed next to the checkbox |
26
- | `icon` | `string` | `""` | `rel-icon` name rendered between the checkbox and the label (label prefix icon). Unset renders no icon and reserves no space |
27
- | `supportingtext` | `string` | `""` | Helper text displayed below the checkbox and label |
47
+ | `icon` | `string` | `""` | `rel-icon` name rendered between the checkbox and the label (label prefix icon; scales with `size`). Unset renders no icon and reserves no space |
48
+ | `supportingtext` | `string` | `""` | Helper text; placement depends on `density` |
28
49
  | `state` | `"default" \| "error" \| "disabled"` | `"default"` | Visual and interaction state of the checkbox |
29
- | `status` | `"unchecked" \| "checked" \| "indeterminate"` | `"unchecked"` | Current checked state of the checkbox |
50
+ | `status` | `"unchecked" \| "checked" \| "indeterminate"` | `"unchecked"` | Current check state. Indeterminate is programmatic-only |
51
+ | `checked` | `boolean` (accessor) | `false` | Convenience accessor over `status` (used by the Angular CVA) |
52
+ | `size` | `"small" \| "medium"` | `"medium"` | Control scale: medium = 18px box, body-lg label, body-sm supporting text; small = 14px box, body-md label, body-xs supporting text (reflected) |
53
+ | `density` | `"compact" \| "comfortable"` | `"compact"` | Supporting-text layout: compact puts it on its own line under the row, indented past the box; comfortable stacks label + supporting text beside the box (reflected) |
54
+ | `alignment` | `"leading" \| "trailing"` | `"leading"` | Box position: leading = before the label; trailing = far edge of the row. Trailing always renders comfortable density — compact+trailing is "Not available" in the Figma sheet, and a dev-mode `console.warn` fires if requested (reflected) |
55
+ | `value` | `string` | `""` | Value reported in the `rel-change` event detail |
56
+
57
+ ### Layout matrix (mirrors the Figma sheet)
58
+ | Size | Density | Leading | Trailing |
59
+ |------|---------|---------|----------|
60
+ | M | Comfortable | ✓ | ✓ |
61
+ | M | Compact | ✓ (default) | — renders comfortable |
62
+ | S | Comfortable | ✓ | ✓ |
63
+ | S | Compact | ✓ | — renders comfortable |
30
64
 
31
65
  ## Events
32
66
  | Event | Detail | Description |
33
67
  |-------|--------|-------------|
34
- | `change` | `{ checked: boolean, indeterminate: boolean }` | Fired when checkbox state changes through user interaction |
68
+ | `rel-change` | `{ checked: boolean, value: string }` | Fired on user toggle (bubbles, composed) |
35
69
 
36
70
  ## Slots
37
71
  Default slot not used.
38
72
 
39
73
  ## CSS Parts
40
- None exposed.
74
+ - `base` — outer wrapper
75
+ - `label` — label text span
76
+ - `helper` — supporting text span
41
77
 
42
78
  ## Tokens / CSS Variables
43
79
 
44
80
  ### Semantic Tokens
45
81
  - `--rel-fonts-font-family` - Font family
46
- - `--rel-body-md-regular-font-size`, `--rel-body-sm-regular-font-size` - Font sizes for label and supporting text
47
- - `--rel-fonts-weight-body-medium` - Font weight for label
82
+ - `--rel-body-lg-regular-*` / `--rel-body-sm-regular-*` - Label and supporting text type (medium size)
83
+ - `--rel-body-md-regular-*` / `--rel-body-xs-regular-*` - Label and supporting text type (small size)
84
+
85
+ ### Spacing Tokens
86
+ - `--rel-spacing-xs` - Gap between box, icon, and label
87
+ - `--rel-spacing-3xs` - Gap between label and supporting text (comfortable) / row and supporting text (compact)
48
88
 
49
89
  ### Color Tokens
50
- - `--rel-text-label` - Checkbox border and checked background color
51
- - `--rel-bg-surface` - Checkbox background and checkmark color
52
- - `--rel-text-default` - Label text color
53
- - `--rel-text-placeholder` - Supporting text color
54
- - `--rel-bg-danger` - Error state border and background color
55
- - `--rel-border-info` - Focus outline color
90
+ - `--rel-text-default` - Box border and label text color (rest)
91
+ - `--rel-text-body` - Hover color for box, label, and icon
92
+ - `--rel-bg-neutral-inverse-2` - Checked box fill and indeterminate dash
93
+ - `--rel-text-inverse` - Checkmark color
94
+ - `--rel-text-caption` - Supporting text color
95
+ - `--rel-border-focus` - Focus outline color
96
+ - `--rel-border-danger` / `--rel-action-danger-fill` / `--rel-text-danger` - Error state colors
56
97
 
57
98
  ## Accessibility
58
99
  - Uses visually hidden native checkbox input for screen reader support
59
100
  - Custom visual checkbox with proper focus indicators
60
101
  - Focus visible state with outline offset for keyboard users
61
- - Label click targets the checkbox for larger interaction area
102
+ - Label click targets the checkbox for larger interaction area (in trailing alignment the whole row is clickable)
62
103
  - Disabled state communicated through opacity reduction and cursor change
63
104
 
64
105
  ### Keyboard Interactions
@@ -67,12 +108,18 @@ None exposed.
67
108
 
68
109
  ### Focus Behavior
69
110
  - Focus outline appears with offset around the custom checkbox box
70
- - Uses info border color for high contrast
111
+ - Uses focus border color for high contrast
71
112
 
72
113
  ## Notes / Edge Cases
73
- - Clicking cycles through three states in order: unchecked → checked → indeterminate → unchecked (repeating)
114
+ - User toggling is binary: indeterminate is never entered by interaction — toggling from indeterminate resolves to checked. Set `status="indeterminate"` programmatically (e.g. a Select-All parent driven by app state)
74
115
  - When `state="disabled"`, click events are blocked and opacity is reduced to 0.55
75
- - The indeterminate state displays a horizontal line instead of a checkmark
76
- - In error state, the checkbox border and checked background use error color
77
- - Supporting text is positioned below with left padding to align with checkbox content
78
- - Native checkbox input is visually hidden but maintains accessibility features
116
+ - The host is `display: block`, so a trailing checkbox stretches to its container width; constrain the parent to control row width
117
+ - Comfortable density with no supporting text is visually identical to compact
118
+ - The hidden native input is absolutely positioned against the `.row` containing block; keep `position: relative` on `.row` (Tab-scroll page-jump fix, shared with rel-radio)
119
+
120
+ ## Recipes (intentionally NOT component props)
121
+ - **Selectable list row**: `alignment="trailing"` checkbox inside a padded, hover-highlighted container
122
+ - **Select All / nested group**: parent checkbox with `status="indeterminate"` wired to children in app state
123
+ - **Selectable card**: `rel-card` wrapping a checkbox; toggle a selected border on `rel-change`
124
+
125
+ See the Storybook Checkbox stories for working recipe markup.
@@ -1,9 +1,12 @@
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
3
  import "@reliability-design/icons/rel-icon";
4
4
 
5
5
  type CheckboxState = "default" | "error" | "disabled";
6
6
  type CheckboxStatus = "unchecked" | "checked" | "indeterminate";
7
+ type CheckboxSize = "small" | "medium";
8
+ type CheckboxDensity = "compact" | "comfortable";
9
+ type CheckboxAlignment = "leading" | "trailing";
7
10
 
8
11
  @customElement("rel-checkbox")
9
12
  export class RelCheckbox extends LitElement {
@@ -15,6 +18,21 @@ export class RelCheckbox extends LitElement {
15
18
  @property({ type: String }) status: CheckboxStatus = "unchecked";
16
19
  @property({ type: String }) value = "";
17
20
 
21
+ /** Control scale (Figma Size). `"medium"` (default) = 18px box with body-lg
22
+ * label; `"small"` = 14px box with body-md label. */
23
+ @property({ type: String, reflect: true }) size: CheckboxSize = "medium";
24
+
25
+ /** Supporting-text layout (Figma Density). `"compact"` (default) puts the
26
+ * supporting text on its own line under the row, indented past the box;
27
+ * `"comfortable"` stacks label + supporting text beside the box. */
28
+ @property({ type: String, reflect: true }) density: CheckboxDensity = "compact";
29
+
30
+ /** Box position (Figma Alignment). `"leading"` (default) renders the box
31
+ * before the label; `"trailing"` pushes it to the far edge of the row.
32
+ * Trailing has no compact layout in the spec, so it always renders with
33
+ * comfortable density. */
34
+ @property({ type: String, reflect: true }) alignment: CheckboxAlignment = "leading";
35
+
18
36
  get checked(): boolean {
19
37
  return this.status === "checked";
20
38
  }
@@ -53,6 +71,29 @@ export class RelCheckbox extends LitElement {
53
71
  cursor: pointer;
54
72
  }
55
73
 
74
+ /* Trailing alignment — box moves to the far edge, label stack to the
75
+ start. row-reverse keeps the box first in DOM order (input + .box
76
+ adjacency drives the focus ring) while space-between stretches the
77
+ row to its container. */
78
+ :host([alignment="trailing"]) .row {
79
+ flex-direction: row-reverse;
80
+ justify-content: space-between;
81
+ }
82
+
83
+ /* Comfortable density — label and supporting text stack beside the box. */
84
+ .content {
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: var(--rel-spacing-3xs);
88
+ min-width: 0;
89
+ }
90
+
91
+ .label-line {
92
+ display: flex;
93
+ align-items: center;
94
+ gap: var(--rel-spacing-xs);
95
+ }
96
+
56
97
  input {
57
98
  position: absolute;
58
99
  inset: 0;
@@ -195,7 +236,6 @@ export class RelCheckbox extends LitElement {
195
236
 
196
237
  .text {
197
238
  color: var(--rel-text-caption);
198
- padding-left: 28px;
199
239
 
200
240
  font-family: var(--rel-body-sm-regular-font-family);
201
241
 
@@ -208,8 +248,63 @@ export class RelCheckbox extends LitElement {
208
248
  letter-spacing: var(--rel-body-sm-regular-letter-spacing);
209
249
 
210
250
  }
251
+
252
+ /* Compact density — supporting text sits under the whole row, indented
253
+ past the box (box width + row gap, matching the pre-existing 28px). */
254
+ .text-below {
255
+ padding-left: 28px;
256
+ }
257
+
258
+ /* Size variant (medium is the base scale above). */
259
+ :host([size="small"]) .box {
260
+ width: 14px;
261
+ height: 14px;
262
+ border-radius: 3px;
263
+ }
264
+
265
+ :host([size="small"]) svg {
266
+ width: 10px;
267
+ height: 10px;
268
+ }
269
+
270
+ :host([size="small"]) .label {
271
+ font-family: var(--rel-body-md-regular-font-family);
272
+ font-weight: var(--rel-body-md-regular-font-weight);
273
+ font-size: var(--rel-body-md-regular-font-size);
274
+ line-height: var(--rel-body-md-regular-line-height);
275
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
276
+ }
277
+
278
+ :host([size="small"]) .text {
279
+ font-family: var(--rel-body-xs-regular-font-family);
280
+ font-weight: var(--rel-body-xs-regular-font-weight);
281
+ font-size: var(--rel-body-xs-regular-font-size);
282
+ line-height: var(--rel-body-xs-regular-line-height);
283
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
284
+ }
285
+
286
+ :host([size="small"]) .text-below {
287
+ padding-left: 24px;
288
+ }
211
289
  `;
212
290
 
291
+ protected willUpdate(changed: PropertyValues<this>) {
292
+ if (
293
+ (changed.has("alignment") || changed.has("density")) &&
294
+ this.alignment === "trailing" &&
295
+ this.density === "compact"
296
+ ) {
297
+ console.warn(
298
+ '[rel-checkbox] density="compact" has no trailing layout; rendering comfortable density.'
299
+ );
300
+ }
301
+ }
302
+
303
+ /** Trailing has no compact layout in the spec — trailing wins. */
304
+ private get effectiveDensity(): CheckboxDensity {
305
+ return this.alignment === "trailing" ? "comfortable" : this.density;
306
+ }
307
+
213
308
  private toggle() {
214
309
  if (this.state === "disabled") return;
215
310
 
@@ -239,6 +334,9 @@ export class RelCheckbox extends LitElement {
239
334
  <label class="row">
240
335
  <input
241
336
  type="checkbox"
337
+ .checked=${this.status === "checked"}
338
+ .indeterminate=${this.status === "indeterminate"}
339
+ aria-checked="${this.status === "indeterminate" ? "mixed" : nothing}"
242
340
  @click=${this.toggle}
243
341
  @blur=${() => this._onTouched()}
244
342
  ?disabled=${this.state === "disabled"}
@@ -256,15 +354,27 @@ export class RelCheckbox extends LitElement {
256
354
  </svg>
257
355
  </span>
258
356
 
259
- ${this.icon
260
- ? html`<rel-icon name="${this.icon}" size="md" class="icon-prefix"></rel-icon>`
261
- : null}
262
-
263
- <span part="label" class="label">${this.label}</span>
357
+ <span class="content">
358
+ <span class="label-line">
359
+ ${this.icon
360
+ ? html`<rel-icon
361
+ name="${this.icon}"
362
+ size="${this.size === "small" ? "sm" : "md"}"
363
+ class="icon-prefix"
364
+ ></rel-icon>`
365
+ : null}
366
+
367
+ <span part="label" class="label">${this.label}</span>
368
+ </span>
369
+
370
+ ${this.effectiveDensity === "comfortable" && this.supportingtext
371
+ ? html`<span part="helper" class="text">${this.supportingtext}</span>`
372
+ : null}
373
+ </span>
264
374
  </label>
265
375
 
266
- ${this.supportingtext
267
- ? html`<span part="helper" class="text">${this.supportingtext}</span>`
376
+ ${this.effectiveDensity === "compact" && this.supportingtext
377
+ ? html`<span part="helper" class="text text-below">${this.supportingtext}</span>`
268
378
  : null}
269
379
  </div>
270
380
  `;