@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
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ var __decorateClass = (decorators, target, key, kind) => {
29
29
  // src/index.ts
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
+ FocusTrap: () => FocusTrap,
32
33
  RelAccordion: () => RelAccordion,
33
34
  RelAlert: () => RelAlert,
34
35
  RelAutocomplete: () => RelAutocomplete,
@@ -87,7 +88,18 @@ __export(index_exports, {
87
88
  RelTopbarItem: () => RelTopbarItem,
88
89
  RelTopbarMobilePanel: () => RelTopbarMobilePanel,
89
90
  RelTopbarProfile: () => RelTopbarProfile,
90
- RelTopbarUtilities: () => RelTopbarUtilities
91
+ RelTopbarUtilities: () => RelTopbarUtilities,
92
+ RelUpload: () => RelUpload,
93
+ RelUploadActions: () => RelUploadActions,
94
+ bindShortcut: () => bindShortcut,
95
+ formatShortcutParts: () => formatShortcutParts,
96
+ getDeepActiveElement: () => getDeepActiveElement,
97
+ getFocusableElements: () => getFocusableElements,
98
+ isApplePlatform: () => isApplePlatform,
99
+ isEditableTarget: () => isEditableTarget,
100
+ matchesShortcut: () => matchesShortcut,
101
+ parseShortcut: () => parseShortcut,
102
+ trapTabKey: () => trapTabKey
91
103
  });
92
104
  module.exports = __toCommonJS(index_exports);
93
105
  var import_vars = require("./vars-XNMTSRVN.css");
@@ -104,6 +116,8 @@ var RelAccordion = class extends import_lit.LitElement {
104
116
  this.size = "md";
105
117
  this.type = "default";
106
118
  this.appearance = "shadow";
119
+ /** Stable id prefix wiring the header to its panel. */
120
+ this.accordionId = `rel-accordion-${Math.random().toString(36).slice(2)}`;
107
121
  }
108
122
  toggle() {
109
123
  this.open = !this.open;
@@ -120,9 +134,11 @@ var RelAccordion = class extends import_lit.LitElement {
120
134
  <div part="base" class="accordion">
121
135
  <div
122
136
  part="trigger" class="header"
137
+ id="${this.accordionId}-header"
123
138
  role="button"
124
139
  tabindex="0"
125
140
  aria-expanded="${this.open}"
141
+ aria-controls="${this.accordionId}-body"
126
142
  @click=${this.toggle}
127
143
  @keydown=${(e) => {
128
144
  if (e.key === "Enter" || e.key === " ") {
@@ -145,7 +161,14 @@ var RelAccordion = class extends import_lit.LitElement {
145
161
  </slot>
146
162
  </div>
147
163
  </div>
148
- <div part="content" class="body ${this.open ? "open" : "closed"}">
164
+ <div
165
+ part="content"
166
+ class="body ${this.open ? "open" : "closed"}"
167
+ id="${this.accordionId}-body"
168
+ role="region"
169
+ aria-labelledby="${this.accordionId}-header"
170
+ ?inert=${!this.open}
171
+ >
149
172
  <slot></slot>
150
173
  </div>
151
174
  </div>
@@ -1648,6 +1671,14 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1648
1671
  if (left + t.width > window.innerWidth - 8) {
1649
1672
  left = Math.max(8, window.innerWidth - t.width - 8);
1650
1673
  }
1674
+ this.panelEl.style.top = `${top}px`;
1675
+ this.panelEl.style.left = `${left}px`;
1676
+ this.panelEl.style.width = `${t.width}px`;
1677
+ const applied = this.panelEl.getBoundingClientRect();
1678
+ top -= applied.top - top;
1679
+ left -= applied.left - left;
1680
+ this.panelEl.style.top = `${top}px`;
1681
+ this.panelEl.style.left = `${left}px`;
1651
1682
  this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
1652
1683
  };
1653
1684
  }
@@ -1659,6 +1690,7 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1659
1690
  disconnectedCallback() {
1660
1691
  super.disconnectedCallback();
1661
1692
  this.removeGlobalListeners();
1693
+ this.hideNativePopover();
1662
1694
  clearTimeout(this.debounceTimer);
1663
1695
  }
1664
1696
  willUpdate(changed) {
@@ -1718,13 +1750,40 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1718
1750
  this.open = true;
1719
1751
  this.highlightedIndex = -1;
1720
1752
  this.addGlobalListeners();
1721
- requestAnimationFrame(() => this.updatePosition());
1753
+ requestAnimationFrame(() => {
1754
+ this.showNativePopover();
1755
+ this.updatePosition();
1756
+ });
1722
1757
  }
1723
1758
  closePanel() {
1724
1759
  if (!this.open) return;
1725
1760
  this.open = false;
1726
1761
  this.highlightedIndex = -1;
1727
1762
  this.removeGlobalListeners();
1763
+ this.hideNativePopover();
1764
+ }
1765
+ /* A transformed ancestor (e.g. rel-modal's centered panel with
1766
+ * translate(-50%,-50%)) becomes the containing block for position:fixed
1767
+ * descendants, breaking viewport-based coordinates and letting the
1768
+ * panel's overflow:hidden clip the suggestions. Promoting the panel to
1769
+ * the browser top layer sidesteps both — same fix as rel-popover.
1770
+ * `manual` keeps light-dismiss and Escape handling under this
1771
+ * component's own control. */
1772
+ showNativePopover() {
1773
+ const p = this.panelEl;
1774
+ if (!p || typeof p.showPopover !== "function") return;
1775
+ try {
1776
+ if (!p.matches(":popover-open")) p.showPopover();
1777
+ } catch {
1778
+ }
1779
+ }
1780
+ hideNativePopover() {
1781
+ const p = this.panelEl;
1782
+ if (!p || typeof p.hidePopover !== "function") return;
1783
+ try {
1784
+ if (p.matches(":popover-open")) p.hidePopover();
1785
+ } catch {
1786
+ }
1728
1787
  }
1729
1788
  /** Re-evaluates whether the panel should be showing for the current state. */
1730
1789
  syncPanel() {
@@ -1760,17 +1819,17 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1760
1819
  this.syncPanel();
1761
1820
  }
1762
1821
  }
1763
- async runSearch(query11) {
1822
+ async runSearch(query12) {
1764
1823
  const seq = ++this.searchSeq;
1765
1824
  try {
1766
- const results = await this.searchFn(query11);
1825
+ const results = await this.searchFn(query12);
1767
1826
  if (seq !== this.searchSeq) return;
1768
1827
  this.remoteItems = results ?? [];
1769
1828
  } catch (err) {
1770
1829
  if (seq !== this.searchSeq) return;
1771
1830
  this.remoteItems = [];
1772
1831
  this.dispatchEvent(
1773
- new CustomEvent("rel-search-error", { detail: { query: query11, error: err }, bubbles: true, composed: true })
1832
+ new CustomEvent("rel-search-error", { detail: { query: query12, error: err }, bubbles: true, composed: true })
1774
1833
  );
1775
1834
  } finally {
1776
1835
  if (seq === this.searchSeq) {
@@ -1940,6 +1999,7 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1940
1999
  id="${panelId}"
1941
2000
  part="panel list"
1942
2001
  class="ac-panel"
2002
+ popover="manual"
1943
2003
  role="listbox"
1944
2004
  aria-label="${this.accessibleName || "Suggestions"}"
1945
2005
  style="${this.panelStyle}"
@@ -1994,6 +2054,15 @@ RelAutocomplete.styles = import_lit4.css`
1994
2054
  position: fixed;
1995
2055
  z-index: 1001;
1996
2056
  display: none;
2057
+ /* Reset native [popover] UA styles (the panel is promoted to the
2058
+ top layer so transformed ancestors — e.g. a centered rel-modal
2059
+ panel — can't hijack its containing block or clip it). */
2060
+ inset: auto;
2061
+ margin: 0;
2062
+ padding: 0;
2063
+ width: auto;
2064
+ height: auto;
2065
+ color: inherit;
1997
2066
  box-sizing: border-box;
1998
2067
  background: var(--rel-autocomplete-bg, var(--rel-bg-neutral));
1999
2068
  border: 1px solid var(--rel-autocomplete-menu-border, var(--rel-border-default));
@@ -2789,9 +2858,12 @@ RelButton.styles = import_lit8.css`
2789
2858
  }
2790
2859
  }
2791
2860
 
2792
- /* Sizes */
2861
+ /* Sizes. Min-widths come from the Figma Button master (S/M/L =
2862
+ 80/100/120) so short labels like "OK" keep a proper pill shape;
2863
+ --rel-button-min-width lets a consumer override per instance. */
2793
2864
  .size-small {
2794
2865
  height: 32px;
2866
+ min-width: var(--rel-button-min-width, 80px);
2795
2867
  padding: 0 var(--rel-spacing-sm);
2796
2868
  font-family: var(--rel-action-md-font-family);
2797
2869
  font-weight: var(--rel-action-md-font-weight);
@@ -2802,6 +2874,7 @@ RelButton.styles = import_lit8.css`
2802
2874
 
2803
2875
  .size-medium {
2804
2876
  height: 40px;
2877
+ min-width: var(--rel-button-min-width, 100px);
2805
2878
  gap: var(--rel-spacing-xs);
2806
2879
  padding: 0 var(--rel-spacing-md);
2807
2880
  font-family: var(--rel-action-lg-font-family);
@@ -2813,6 +2886,7 @@ RelButton.styles = import_lit8.css`
2813
2886
 
2814
2887
  .size-large {
2815
2888
  height: 48px;
2889
+ min-width: var(--rel-button-min-width, 120px);
2816
2890
  padding: 0 var(--rel-spacing-lg);
2817
2891
  font-family: var(--rel-action-xl-font-family);
2818
2892
  font-weight: var(--rel-action-xl-font-weight);
@@ -2919,6 +2993,11 @@ RelButton.styles = import_lit8.css`
2919
2993
  color: var(--rel-action-neutral-label-icons-ghost);
2920
2994
  }
2921
2995
 
2996
+ /* Figma master: ghost S has a narrower 56px min than primary/outline. */
2997
+ .btn-text.size-small {
2998
+ min-width: var(--rel-button-min-width, 56px);
2999
+ }
3000
+
2922
3001
  .btn-text.tone-critical {
2923
3002
  color: var(--rel-action-danger-label-icons-ghost);
2924
3003
  }
@@ -2927,6 +3006,8 @@ RelButton.styles = import_lit8.css`
2927
3006
  background: transparent;
2928
3007
  color: var(--rel-action-neutral-label-icons-link);
2929
3008
  text-decoration: underline;
3009
+ /* Links are inline actions — no minimum footprint in the Figma master. */
3010
+ min-width: 0;
2930
3011
  }
2931
3012
 
2932
3013
  button.btn-link:hover:not(:disabled) {
@@ -2955,6 +3036,7 @@ RelButton.styles = import_lit8.css`
2955
3036
  /* Icon Only as an independent axis: square button of the current style */
2956
3037
  .btn-icon-only {
2957
3038
  padding: 0;
3039
+ min-width: 0;
2958
3040
  }
2959
3041
 
2960
3042
  .btn-icon-only.size-small {
@@ -2973,6 +3055,7 @@ RelButton.styles = import_lit8.css`
2973
3055
  width: 40px;
2974
3056
  height: 40px;
2975
3057
  padding: 0;
3058
+ min-width: 0;
2976
3059
  border-radius: 50%;
2977
3060
  }
2978
3061
  .btn-icon.tone-default {
@@ -3017,7 +3100,9 @@ RelButton.styles = import_lit8.css`
3017
3100
  .btn-text: ghost gets tone-accent hovers below (this generic grey overlay
3018
3101
  has higher specificity than the per-tone ghost rules and was overriding
3019
3102
  them, which is why every ghost hover looked grey). */
3020
- button:hover:not(:disabled):not(.btn-primary):not(.btn-default):not(.btn-icon):not(.btn-secondary):not(.btn-static):not(.btn-text) {
3103
+ /* Link buttons change label color only (rule above) — the overlay fill
3104
+ here would make a hovered link read as a ghost button. */
3105
+ 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) {
3021
3106
  background: var(--rel-action-overlay-hover);
3022
3107
  }
3023
3108
 
@@ -3369,9 +3454,28 @@ var RelCalendar = class extends import_lit9.LitElement {
3369
3454
  });
3370
3455
  }
3371
3456
  }
3457
+ /**
3458
+ * Index of the single grid cell carrying tabindex="0". The old expression
3459
+ * (`isSelected || (index === 0 && !focusedDate)`) left the grid with ZERO
3460
+ * tab stops once the user arrowed around without a selection — Tab could
3461
+ * never re-enter the calendar.
3462
+ */
3463
+ getRovingIndex(days) {
3464
+ const candidates = [
3465
+ this.focusedDate ? days.findIndex(
3466
+ (d) => d.isCurrentMonth && this.isSameDay(d.date, this.focusedDate)
3467
+ ) : -1,
3468
+ days.findIndex((d) => d.isSelected),
3469
+ days.findIndex((d) => d.isCurrentMonth && d.isToday),
3470
+ days.findIndex((d) => d.isCurrentMonth && !d.isDisabled)
3471
+ ];
3472
+ const found = candidates.find((i) => i >= 0);
3473
+ return found ?? 0;
3474
+ }
3372
3475
  render() {
3373
3476
  const days = this.getCalendarDays();
3374
3477
  const weekdayLabels = this.getWeekdayLabels();
3478
+ const rovingIndex = this.getRovingIndex(days);
3375
3479
  return import_lit9.html`
3376
3480
  <div role="grid" aria-label="Calendar">
3377
3481
  <div part="header" class="header">
@@ -3424,7 +3528,7 @@ var RelCalendar = class extends import_lit9.LitElement {
3424
3528
  ?disabled=${day.isDisabled}
3425
3529
  @click=${() => this.selectDay(day)}
3426
3530
  @keydown=${(e) => this.handleKeyDown(e, day, index)}
3427
- tabindex="${day.isSelected || index === 0 && !this.focusedDate ? 0 : -1}"
3531
+ tabindex="${index === rovingIndex ? 0 : -1}"
3428
3532
  >
3429
3533
  ${day.day}
3430
3534
  </button>
@@ -3724,12 +3828,23 @@ var RelCarousel = class extends import_lit11.LitElement {
3724
3828
  // Number of clone slides prepended before the real slides
3725
3829
  this.clonesBefore = 0;
3726
3830
  this._silentJumping = false;
3831
+ this.handleFocusIn = () => {
3832
+ if (this.autoplay) this.stopAutoplay();
3833
+ };
3834
+ this.handleFocusOut = (e) => {
3835
+ if (!this.autoplay) return;
3836
+ const next = e.relatedTarget;
3837
+ if (next && (this.contains(next) || this.shadowRoot?.contains(next))) return;
3838
+ this.startAutoplay();
3839
+ };
3727
3840
  }
3728
3841
  connectedCallback() {
3729
3842
  super.connectedCallback();
3730
3843
  this.prefersReducedMotion = window.matchMedia(
3731
3844
  "(prefers-reduced-motion: reduce)"
3732
3845
  ).matches;
3846
+ this.addEventListener("focusin", this.handleFocusIn);
3847
+ this.addEventListener("focusout", this.handleFocusOut);
3733
3848
  if (this.autoplay) {
3734
3849
  this.startAutoplay();
3735
3850
  }
@@ -3761,6 +3876,8 @@ var RelCarousel = class extends import_lit11.LitElement {
3761
3876
  }
3762
3877
  disconnectedCallback() {
3763
3878
  super.disconnectedCallback();
3879
+ this.removeEventListener("focusin", this.handleFocusIn);
3880
+ this.removeEventListener("focusout", this.handleFocusOut);
3764
3881
  this._ro?.disconnect();
3765
3882
  this.stopAutoplay();
3766
3883
  if (this.animationId) {
@@ -4075,6 +4192,7 @@ var RelCarousel = class extends import_lit11.LitElement {
4075
4192
  class="dot ${i === this.value ? "active" : ""}"
4076
4193
  part="dot"
4077
4194
  aria-label="Go to slide ${i + 1}"
4195
+ aria-current="${i === this.value ? "true" : import_lit11.nothing}"
4078
4196
  @click=${() => this.goToSlide(i)}
4079
4197
  ></button>
4080
4198
  `) : import_lit11.nothing}
@@ -4467,6 +4585,9 @@ var RelCheckbox = class extends import_lit13.LitElement {
4467
4585
  this.state = "default";
4468
4586
  this.status = "unchecked";
4469
4587
  this.value = "";
4588
+ this.size = "medium";
4589
+ this.density = "compact";
4590
+ this.alignment = "leading";
4470
4591
  this._onChange = () => {
4471
4592
  };
4472
4593
  this._onTouched = () => {
@@ -4478,6 +4599,17 @@ var RelCheckbox = class extends import_lit13.LitElement {
4478
4599
  set checked(val) {
4479
4600
  this.status = val ? "checked" : "unchecked";
4480
4601
  }
4602
+ willUpdate(changed) {
4603
+ if ((changed.has("alignment") || changed.has("density")) && this.alignment === "trailing" && this.density === "compact") {
4604
+ console.warn(
4605
+ '[rel-checkbox] density="compact" has no trailing layout; rendering comfortable density.'
4606
+ );
4607
+ }
4608
+ }
4609
+ /** Trailing has no compact layout in the spec — trailing wins. */
4610
+ get effectiveDensity() {
4611
+ return this.alignment === "trailing" ? "comfortable" : this.density;
4612
+ }
4481
4613
  toggle() {
4482
4614
  if (this.state === "disabled") return;
4483
4615
  this.status = this.status === "checked" ? "unchecked" : "checked";
@@ -4508,6 +4640,9 @@ var RelCheckbox = class extends import_lit13.LitElement {
4508
4640
  <label class="row">
4509
4641
  <input
4510
4642
  type="checkbox"
4643
+ .checked=${this.status === "checked"}
4644
+ .indeterminate=${this.status === "indeterminate"}
4645
+ aria-checked="${this.status === "indeterminate" ? "mixed" : import_lit13.nothing}"
4511
4646
  @click=${this.toggle}
4512
4647
  @blur=${() => this._onTouched()}
4513
4648
  ?disabled=${this.state === "disabled"}
@@ -4525,12 +4660,22 @@ var RelCheckbox = class extends import_lit13.LitElement {
4525
4660
  </svg>
4526
4661
  </span>
4527
4662
 
4528
- ${this.icon ? import_lit13.html`<rel-icon name="${this.icon}" size="md" class="icon-prefix"></rel-icon>` : null}
4663
+ <span class="content">
4664
+ <span class="label-line">
4665
+ ${this.icon ? import_lit13.html`<rel-icon
4666
+ name="${this.icon}"
4667
+ size="${this.size === "small" ? "sm" : "md"}"
4668
+ class="icon-prefix"
4669
+ ></rel-icon>` : null}
4670
+
4671
+ <span part="label" class="label">${this.label}</span>
4672
+ </span>
4529
4673
 
4530
- <span part="label" class="label">${this.label}</span>
4674
+ ${this.effectiveDensity === "comfortable" && this.supportingtext ? import_lit13.html`<span part="helper" class="text">${this.supportingtext}</span>` : null}
4675
+ </span>
4531
4676
  </label>
4532
4677
 
4533
- ${this.supportingtext ? import_lit13.html`<span part="helper" class="text">${this.supportingtext}</span>` : null}
4678
+ ${this.effectiveDensity === "compact" && this.supportingtext ? import_lit13.html`<span part="helper" class="text text-below">${this.supportingtext}</span>` : null}
4534
4679
  </div>
4535
4680
  `;
4536
4681
  }
@@ -4563,6 +4708,29 @@ RelCheckbox.styles = import_lit13.css`
4563
4708
  cursor: pointer;
4564
4709
  }
4565
4710
 
4711
+ /* Trailing alignment — box moves to the far edge, label stack to the
4712
+ start. row-reverse keeps the box first in DOM order (input + .box
4713
+ adjacency drives the focus ring) while space-between stretches the
4714
+ row to its container. */
4715
+ :host([alignment="trailing"]) .row {
4716
+ flex-direction: row-reverse;
4717
+ justify-content: space-between;
4718
+ }
4719
+
4720
+ /* Comfortable density — label and supporting text stack beside the box. */
4721
+ .content {
4722
+ display: flex;
4723
+ flex-direction: column;
4724
+ gap: var(--rel-spacing-3xs);
4725
+ min-width: 0;
4726
+ }
4727
+
4728
+ .label-line {
4729
+ display: flex;
4730
+ align-items: center;
4731
+ gap: var(--rel-spacing-xs);
4732
+ }
4733
+
4566
4734
  input {
4567
4735
  position: absolute;
4568
4736
  inset: 0;
@@ -4705,7 +4873,6 @@ RelCheckbox.styles = import_lit13.css`
4705
4873
 
4706
4874
  .text {
4707
4875
  color: var(--rel-text-caption);
4708
- padding-left: 28px;
4709
4876
 
4710
4877
  font-family: var(--rel-body-sm-regular-font-family);
4711
4878
 
@@ -4718,6 +4885,44 @@ RelCheckbox.styles = import_lit13.css`
4718
4885
  letter-spacing: var(--rel-body-sm-regular-letter-spacing);
4719
4886
 
4720
4887
  }
4888
+
4889
+ /* Compact density — supporting text sits under the whole row, indented
4890
+ past the box (box width + row gap, matching the pre-existing 28px). */
4891
+ .text-below {
4892
+ padding-left: 28px;
4893
+ }
4894
+
4895
+ /* Size variant (medium is the base scale above). */
4896
+ :host([size="small"]) .box {
4897
+ width: 14px;
4898
+ height: 14px;
4899
+ border-radius: 3px;
4900
+ }
4901
+
4902
+ :host([size="small"]) svg {
4903
+ width: 10px;
4904
+ height: 10px;
4905
+ }
4906
+
4907
+ :host([size="small"]) .label {
4908
+ font-family: var(--rel-body-md-regular-font-family);
4909
+ font-weight: var(--rel-body-md-regular-font-weight);
4910
+ font-size: var(--rel-body-md-regular-font-size);
4911
+ line-height: var(--rel-body-md-regular-line-height);
4912
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
4913
+ }
4914
+
4915
+ :host([size="small"]) .text {
4916
+ font-family: var(--rel-body-xs-regular-font-family);
4917
+ font-weight: var(--rel-body-xs-regular-font-weight);
4918
+ font-size: var(--rel-body-xs-regular-font-size);
4919
+ line-height: var(--rel-body-xs-regular-line-height);
4920
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
4921
+ }
4922
+
4923
+ :host([size="small"]) .text-below {
4924
+ padding-left: 24px;
4925
+ }
4721
4926
  `;
4722
4927
  __decorateClass([
4723
4928
  (0, import_decorators13.property)({ type: String })
@@ -4737,6 +4942,15 @@ __decorateClass([
4737
4942
  __decorateClass([
4738
4943
  (0, import_decorators13.property)({ type: String })
4739
4944
  ], RelCheckbox.prototype, "value", 2);
4945
+ __decorateClass([
4946
+ (0, import_decorators13.property)({ type: String, reflect: true })
4947
+ ], RelCheckbox.prototype, "size", 2);
4948
+ __decorateClass([
4949
+ (0, import_decorators13.property)({ type: String, reflect: true })
4950
+ ], RelCheckbox.prototype, "density", 2);
4951
+ __decorateClass([
4952
+ (0, import_decorators13.property)({ type: String, reflect: true })
4953
+ ], RelCheckbox.prototype, "alignment", 2);
4740
4954
  RelCheckbox = __decorateClass([
4741
4955
  (0, import_decorators13.customElement)("rel-checkbox")
4742
4956
  ], RelCheckbox);
@@ -4767,6 +4981,109 @@ function formatDatePattern(date, pattern, locale = "en-US") {
4767
4981
  );
4768
4982
  }
4769
4983
 
4984
+ // src/utils/focus.ts
4985
+ var FOCUSABLE_SELECTOR = [
4986
+ 'a[href]:not([tabindex="-1"])',
4987
+ 'button:not([disabled]):not([tabindex="-1"])',
4988
+ 'input:not([disabled]):not([tabindex="-1"])',
4989
+ 'select:not([disabled]):not([tabindex="-1"])',
4990
+ 'textarea:not([disabled]):not([tabindex="-1"])',
4991
+ 'audio[controls]:not([tabindex="-1"])',
4992
+ 'video[controls]:not([tabindex="-1"])',
4993
+ '[contenteditable]:not([contenteditable="false"]):not([tabindex="-1"])',
4994
+ '[tabindex]:not([tabindex="-1"])'
4995
+ ].join(",");
4996
+ function isVisible(el) {
4997
+ return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
4998
+ }
4999
+ function getFocusableElements(root) {
5000
+ const out = [];
5001
+ const visit = (el) => {
5002
+ if (el instanceof HTMLElement && el.matches(FOCUSABLE_SELECTOR) && isVisible(el)) {
5003
+ out.push(el);
5004
+ }
5005
+ if (el instanceof HTMLSlotElement) {
5006
+ el.assignedElements({ flatten: true }).forEach(visit);
5007
+ } else if (el.shadowRoot) {
5008
+ Array.from(el.shadowRoot.children).forEach(visit);
5009
+ } else {
5010
+ Array.from(el.children).forEach(visit);
5011
+ }
5012
+ };
5013
+ const start = root instanceof ShadowRoot ? Array.from(root.children) : root.shadowRoot ? Array.from(root.shadowRoot.children) : Array.from(root.children);
5014
+ start.forEach(visit);
5015
+ return out;
5016
+ }
5017
+ function getDeepActiveElement() {
5018
+ let active = document.activeElement;
5019
+ while (active?.shadowRoot?.activeElement) {
5020
+ active = active.shadowRoot.activeElement;
5021
+ }
5022
+ return active instanceof HTMLElement ? active : null;
5023
+ }
5024
+ function trapTabKey(e, container) {
5025
+ if (e.key !== "Tab") return;
5026
+ const focusable = getFocusableElements(container);
5027
+ if (focusable.length === 0) {
5028
+ e.preventDefault();
5029
+ return;
5030
+ }
5031
+ const first = focusable[0];
5032
+ const last = focusable[focusable.length - 1];
5033
+ const active = getDeepActiveElement();
5034
+ const inside = active ? focusable.includes(active) : false;
5035
+ if (e.shiftKey) {
5036
+ if (!inside || active === first) {
5037
+ e.preventDefault();
5038
+ last.focus();
5039
+ }
5040
+ } else {
5041
+ if (!inside || active === last) {
5042
+ e.preventDefault();
5043
+ first.focus();
5044
+ }
5045
+ }
5046
+ }
5047
+ var FocusTrap = class {
5048
+ constructor(getContainer) {
5049
+ this.getContainer = getContainer;
5050
+ this.previousFocus = null;
5051
+ this.active = false;
5052
+ this.onKeyDown = (e) => {
5053
+ const container = this.getContainer();
5054
+ if (container) trapTabKey(e, container);
5055
+ };
5056
+ }
5057
+ activate(options) {
5058
+ if (this.active) return;
5059
+ this.active = true;
5060
+ this.previousFocus = getDeepActiveElement();
5061
+ document.addEventListener("keydown", this.onKeyDown, true);
5062
+ requestAnimationFrame(() => {
5063
+ if (!this.active) return;
5064
+ const container = this.getContainer();
5065
+ if (!container) return;
5066
+ const target = options?.initialFocus ?? getFocusableElements(container)[0] ?? null;
5067
+ if (target) {
5068
+ target.focus();
5069
+ } else if (container instanceof HTMLElement) {
5070
+ container.tabIndex = -1;
5071
+ container.focus();
5072
+ }
5073
+ });
5074
+ }
5075
+ deactivate(options) {
5076
+ if (!this.active) return;
5077
+ this.active = false;
5078
+ document.removeEventListener("keydown", this.onKeyDown, true);
5079
+ const previous = this.previousFocus;
5080
+ this.previousFocus = null;
5081
+ if (options?.restoreFocus !== false && previous?.isConnected) {
5082
+ requestAnimationFrame(() => previous.focus());
5083
+ }
5084
+ }
5085
+ };
5086
+
4770
5087
  // src/components/rel-date-picker/rel-date-picker.ts
4771
5088
  var RelDatePicker = class extends import_lit14.LitElement {
4772
5089
  constructor() {
@@ -4804,9 +5121,58 @@ var RelDatePicker = class extends import_lit14.LitElement {
4804
5121
  }
4805
5122
  };
4806
5123
  this.handleEscapeKey = (e) => {
4807
- if (e.key === "Escape" && this.open) {
5124
+ if (!this.open) return;
5125
+ if (e.key === "Escape") {
4808
5126
  this.closeCalendarPopover();
5127
+ return;
5128
+ }
5129
+ if (e.key === "Tab") {
5130
+ const popover = this.renderRoot.querySelector(".popover");
5131
+ if (popover) trapTabKey(e, popover);
5132
+ }
5133
+ };
5134
+ this.handleDropdownKeydown = (e) => {
5135
+ const menu = e.currentTarget;
5136
+ const items = Array.from(
5137
+ menu.querySelectorAll(".dropdown-item")
5138
+ );
5139
+ if (items.length === 0) return;
5140
+ if (e.key === "Escape") {
5141
+ e.preventDefault();
5142
+ e.stopPropagation();
5143
+ const trigger = menu.closest(".dropdown-container")?.querySelector(".dropdown-trigger");
5144
+ this.monthDropdownOpen = false;
5145
+ this.yearDropdownOpen = false;
5146
+ trigger?.focus();
5147
+ return;
5148
+ }
5149
+ if (e.key === "Tab") {
5150
+ this.monthDropdownOpen = false;
5151
+ this.yearDropdownOpen = false;
5152
+ return;
4809
5153
  }
5154
+ const idx = items.indexOf(getDeepActiveElement());
5155
+ let target;
5156
+ switch (e.key) {
5157
+ case "ArrowDown":
5158
+ target = Math.min(items.length - 1, idx + 1);
5159
+ break;
5160
+ case "ArrowUp":
5161
+ target = Math.max(0, idx - 1);
5162
+ break;
5163
+ case "Home":
5164
+ target = 0;
5165
+ break;
5166
+ case "End":
5167
+ target = items.length - 1;
5168
+ break;
5169
+ default:
5170
+ return;
5171
+ }
5172
+ e.preventDefault();
5173
+ e.stopPropagation();
5174
+ items[target]?.focus();
5175
+ items[target]?.scrollIntoView({ block: "nearest" });
4810
5176
  };
4811
5177
  }
4812
5178
  // minDate/maxDate supersede the deprecated min/max aliases
@@ -4843,10 +5209,12 @@ var RelDatePicker = class extends import_lit14.LitElement {
4843
5209
  }
4844
5210
  if (changedProperties.has("open") && this.open) {
4845
5211
  setTimeout(() => {
4846
- const firstDay = this.calendarGrid?.querySelector(
5212
+ const target = this.calendarGrid?.querySelector(
5213
+ '.day-button[tabindex="0"]:not(:disabled)'
5214
+ ) ?? this.calendarGrid?.querySelector(
4847
5215
  ".day-button:not(:disabled)"
4848
5216
  );
4849
- firstDay?.focus();
5217
+ target?.focus();
4850
5218
  }, 100);
4851
5219
  }
4852
5220
  }
@@ -5144,27 +5512,67 @@ var RelDatePicker = class extends import_lit14.LitElement {
5144
5512
  e.stopPropagation();
5145
5513
  this.monthDropdownOpen = !this.monthDropdownOpen;
5146
5514
  this.yearDropdownOpen = false;
5515
+ if (this.monthDropdownOpen) this.focusOpenDropdownSelection();
5147
5516
  }
5148
5517
  toggleYearDropdown(e) {
5149
5518
  e.stopPropagation();
5150
5519
  this.yearDropdownOpen = !this.yearDropdownOpen;
5151
5520
  this.monthDropdownOpen = false;
5521
+ if (this.yearDropdownOpen) this.focusOpenDropdownSelection();
5522
+ }
5523
+ handleDropdownTriggerKeydown(e, kind) {
5524
+ if (e.key !== "ArrowDown") return;
5525
+ e.preventDefault();
5526
+ e.stopPropagation();
5527
+ this.monthDropdownOpen = kind === "month";
5528
+ this.yearDropdownOpen = kind === "year";
5529
+ this.focusOpenDropdownSelection();
5530
+ }
5531
+ async focusOpenDropdownSelection() {
5532
+ await this.updateComplete;
5533
+ const menu = this.renderRoot.querySelector(".dropdown-menu.open");
5534
+ if (!menu) return;
5535
+ const target = menu.querySelector(".dropdown-item.selected") ?? menu.querySelector(".dropdown-item");
5536
+ target?.focus();
5537
+ target?.scrollIntoView({ block: "nearest" });
5152
5538
  }
5153
5539
  selectMonth(month, e) {
5154
5540
  e.stopPropagation();
5541
+ const trigger = e.target.closest(".dropdown-container")?.querySelector(".dropdown-trigger");
5155
5542
  this.viewMonth = month;
5156
5543
  this.monthDropdownOpen = false;
5544
+ this.updateComplete.then(() => trigger?.focus());
5157
5545
  }
5158
5546
  selectYear(year, e) {
5159
5547
  e.stopPropagation();
5548
+ const trigger = e.target.closest(".dropdown-container")?.querySelector(".dropdown-trigger");
5160
5549
  this.viewYear = year;
5161
5550
  this.yearDropdownOpen = false;
5551
+ this.updateComplete.then(() => trigger?.focus());
5552
+ }
5553
+ /**
5554
+ * Index of the single grid cell carrying tabindex="0". The old expression
5555
+ * (`isSelected || (index === 0 && !value)`) left the grid with ZERO tab
5556
+ * stops once the user arrowed around without a selection.
5557
+ */
5558
+ getRovingIndex(days) {
5559
+ const candidates = [
5560
+ this.focusedDate ? days.findIndex(
5561
+ (d) => d.isCurrentMonth && this.isSameDay(d.date, this.focusedDate)
5562
+ ) : -1,
5563
+ days.findIndex((d) => d.isSelected),
5564
+ days.findIndex((d) => d.isCurrentMonth && d.isToday),
5565
+ days.findIndex((d) => d.isCurrentMonth && !d.isDisabled)
5566
+ ];
5567
+ const found = candidates.find((i) => i >= 0);
5568
+ return found ?? 0;
5162
5569
  }
5163
5570
  render() {
5164
5571
  const days = this.getCalendarDays();
5165
5572
  const weekdayLabels = this.getWeekdayLabels();
5166
5573
  const months = this.getMonthsList();
5167
5574
  const years = this.getYearsList();
5575
+ const rovingIndex = this.getRovingIndex(days);
5168
5576
  const isError = this.error;
5169
5577
  const belowText = isError && this.errorMessage ? this.errorMessage : this.helperText;
5170
5578
  const describedById = belowText ? "date-picker-desc" : void 0;
@@ -5251,22 +5659,31 @@ var RelDatePicker = class extends import_lit14.LitElement {
5251
5659
  class="dropdown-trigger"
5252
5660
  type="button"
5253
5661
  @click=${this.toggleMonthDropdown}
5662
+ @keydown=${(e) => this.handleDropdownTriggerKeydown(e, "month")}
5254
5663
  aria-label="Select month"
5664
+ aria-haspopup="listbox"
5255
5665
  aria-expanded=${this.monthDropdownOpen}
5256
5666
  >
5257
5667
  ${this.getMonthName()}
5258
5668
  </button>
5259
- <div class="dropdown-menu ${this.monthDropdownOpen ? "open" : ""}">
5669
+ <div
5670
+ class="dropdown-menu ${this.monthDropdownOpen ? "open" : ""}"
5671
+ role="listbox"
5672
+ aria-label="Month"
5673
+ @keydown=${this.handleDropdownKeydown}
5674
+ >
5260
5675
  ${months.map(
5261
5676
  (m) => import_lit14.html`
5262
- <div
5677
+ <button
5263
5678
  class="dropdown-item ${m.value === this.viewMonth ? "selected" : ""}"
5679
+ type="button"
5680
+ tabindex="-1"
5264
5681
  @click=${(e) => this.selectMonth(m.value, e)}
5265
5682
  role="option"
5266
5683
  aria-selected=${m.value === this.viewMonth}
5267
5684
  >
5268
5685
  ${m.label}
5269
- </div>
5686
+ </button>
5270
5687
  `
5271
5688
  )}
5272
5689
  </div>
@@ -5277,22 +5694,31 @@ var RelDatePicker = class extends import_lit14.LitElement {
5277
5694
  class="dropdown-trigger"
5278
5695
  type="button"
5279
5696
  @click=${this.toggleYearDropdown}
5697
+ @keydown=${(e) => this.handleDropdownTriggerKeydown(e, "year")}
5280
5698
  aria-label="Select year"
5699
+ aria-haspopup="listbox"
5281
5700
  aria-expanded=${this.yearDropdownOpen}
5282
5701
  >
5283
5702
  ${this.viewYear}
5284
5703
  </button>
5285
- <div class="dropdown-menu ${this.yearDropdownOpen ? "open" : ""}">
5704
+ <div
5705
+ class="dropdown-menu ${this.yearDropdownOpen ? "open" : ""}"
5706
+ role="listbox"
5707
+ aria-label="Year"
5708
+ @keydown=${this.handleDropdownKeydown}
5709
+ >
5286
5710
  ${years.map(
5287
5711
  (y) => import_lit14.html`
5288
- <div
5712
+ <button
5289
5713
  class="dropdown-item ${y === this.viewYear ? "selected" : ""}"
5714
+ type="button"
5715
+ tabindex="-1"
5290
5716
  @click=${(e) => this.selectYear(y, e)}
5291
5717
  role="option"
5292
5718
  aria-selected=${y === this.viewYear}
5293
5719
  >
5294
5720
  ${y}
5295
- </div>
5721
+ </button>
5296
5722
  `
5297
5723
  )}
5298
5724
  </div>
@@ -5335,7 +5761,7 @@ var RelDatePicker = class extends import_lit14.LitElement {
5335
5761
  ?disabled=${day.isDisabled}
5336
5762
  @click=${() => this.selectDay(day)}
5337
5763
  @keydown=${(e) => this.handleDayKeydown(e, day, index)}
5338
- tabindex="${day.isSelected || index === 0 && !this.value ? 0 : -1}"
5764
+ tabindex="${index === rovingIndex ? 0 : -1}"
5339
5765
  >
5340
5766
  ${day.day}
5341
5767
  </button>
@@ -5629,6 +6055,11 @@ RelDatePicker.styles = import_lit14.css`
5629
6055
  }
5630
6056
 
5631
6057
  .dropdown-item {
6058
+ display: block;
6059
+ width: 100%;
6060
+ border: none;
6061
+ background: transparent;
6062
+ text-align: left;
5632
6063
  padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
5633
6064
  cursor: pointer;
5634
6065
  color: var(--rel-text-default);
@@ -5640,6 +6071,11 @@ RelDatePicker.styles = import_lit14.css`
5640
6071
  letter-spacing: var(--rel-body-sm-regular-letter-spacing);
5641
6072
  }
5642
6073
 
6074
+ .dropdown-item:focus-visible {
6075
+ outline: 2px solid var(--rel-border-focus);
6076
+ outline-offset: -2px;
6077
+ }
6078
+
5643
6079
  .dropdown-item:hover {
5644
6080
  background: var(--rel-bg-surface-elevated);
5645
6081
  }
@@ -5868,6 +6304,32 @@ var RelRangePicker = class extends import_lit15.LitElement {
5868
6304
  this.viewMonth = (/* @__PURE__ */ new Date()).getMonth();
5869
6305
  this.isSelectingRange = false;
5870
6306
  this.hoverDate = null;
6307
+ this.focusedDate = null;
6308
+ this.onReposition = () => {
6309
+ if (this.open && this.mode === "popover") this.updatePopoverPosition();
6310
+ };
6311
+ this.updatePopoverPosition = () => {
6312
+ const panel = this.popoverEl;
6313
+ if (!panel || !this.open || this.mode !== "popover") return;
6314
+ const t = this.getBoundingClientRect();
6315
+ const c = panel.getBoundingClientRect();
6316
+ let top = t.bottom + 4;
6317
+ if (top + c.height > window.innerHeight - 8) {
6318
+ top = Math.max(8, t.top - c.height - 4);
6319
+ }
6320
+ let left = t.left;
6321
+ if (left + c.width > window.innerWidth - 8) {
6322
+ left = Math.max(8, window.innerWidth - c.width - 8);
6323
+ }
6324
+ panel.style.position = "fixed";
6325
+ panel.style.top = `${top}px`;
6326
+ panel.style.left = `${left}px`;
6327
+ const applied = panel.getBoundingClientRect();
6328
+ top -= applied.top - top;
6329
+ left -= applied.left - left;
6330
+ panel.style.top = `${top}px`;
6331
+ panel.style.left = `${left}px`;
6332
+ };
5871
6333
  // ── Open / close ──
5872
6334
  this.handleClickOutside = (e) => {
5873
6335
  if (!this.open) return;
@@ -5899,6 +6361,69 @@ var RelRangePicker = class extends import_lit15.LitElement {
5899
6361
  const qs = this.getQuickSelects().find((q) => q.label === label);
5900
6362
  if (qs) this.selectQuickSelect(qs);
5901
6363
  };
6364
+ this.handleGridKeydown = (e) => {
6365
+ const cell = e.target.closest?.(".day-cell");
6366
+ const iso = cell?.dataset.iso;
6367
+ if (!iso) return;
6368
+ const date = this.parseISODate(iso);
6369
+ if (!date) return;
6370
+ if (e.key === "Enter" || e.key === " ") {
6371
+ e.preventDefault();
6372
+ const isDisabled = this.isDateDisabled(date);
6373
+ if (!isDisabled) {
6374
+ this.handleDayClick({
6375
+ date,
6376
+ day: date.getDate(),
6377
+ isCurrentMonth: true,
6378
+ isToday: false,
6379
+ isDraftFrom: false,
6380
+ isDraftTo: false,
6381
+ isInRange: false,
6382
+ isDisabled,
6383
+ isWeekend: false
6384
+ });
6385
+ }
6386
+ return;
6387
+ }
6388
+ const next = new Date(date);
6389
+ const dow = next.getDay();
6390
+ switch (e.key) {
6391
+ case "ArrowLeft":
6392
+ next.setDate(next.getDate() - 1);
6393
+ break;
6394
+ case "ArrowRight":
6395
+ next.setDate(next.getDate() + 1);
6396
+ break;
6397
+ case "ArrowUp":
6398
+ next.setDate(next.getDate() - 7);
6399
+ break;
6400
+ case "ArrowDown":
6401
+ next.setDate(next.getDate() + 7);
6402
+ break;
6403
+ case "Home":
6404
+ next.setDate(
6405
+ next.getDate() - (this.weekStart === "mon" ? dow === 0 ? 6 : dow - 1 : dow)
6406
+ );
6407
+ break;
6408
+ case "End":
6409
+ next.setDate(
6410
+ next.getDate() + (this.weekStart === "mon" ? dow === 0 ? 0 : 7 - dow : 6 - dow)
6411
+ );
6412
+ break;
6413
+ case "PageUp":
6414
+ if (e.shiftKey) next.setFullYear(next.getFullYear() - 1);
6415
+ else next.setMonth(next.getMonth() - 1);
6416
+ break;
6417
+ case "PageDown":
6418
+ if (e.shiftKey) next.setFullYear(next.getFullYear() + 1);
6419
+ else next.setMonth(next.getMonth() + 1);
6420
+ break;
6421
+ default:
6422
+ return;
6423
+ }
6424
+ e.preventDefault();
6425
+ this.moveGridFocusTo(next);
6426
+ };
5902
6427
  }
5903
6428
  // ── Lifecycle ──
5904
6429
  connectedCallback() {
@@ -5910,14 +6435,64 @@ var RelRangePicker = class extends import_lit15.LitElement {
5910
6435
  super.disconnectedCallback();
5911
6436
  document.removeEventListener("click", this.handleClickOutside);
5912
6437
  document.removeEventListener("keydown", this.handleGlobalKeydown);
6438
+ window.removeEventListener("resize", this.onReposition);
6439
+ window.removeEventListener("scroll", this.onReposition, true);
6440
+ this.hideTopLayer();
5913
6441
  }
5914
- updated() {
6442
+ updated(changed) {
5915
6443
  this.style.width = this.width;
5916
6444
  this.style.minWidth = this.minWidth;
5917
6445
  this.style.maxWidth = this.maxWidth;
5918
6446
  this.style.height = this.height;
5919
6447
  this.style.minHeight = this.minHeight;
5920
6448
  this.style.maxHeight = this.maxHeight;
6449
+ if (changed.has("open")) {
6450
+ if (this.open) {
6451
+ this.showTopLayer();
6452
+ window.addEventListener("resize", this.onReposition);
6453
+ window.addEventListener("scroll", this.onReposition, true);
6454
+ } else {
6455
+ window.removeEventListener("resize", this.onReposition);
6456
+ window.removeEventListener("scroll", this.onReposition, true);
6457
+ this.hideTopLayer();
6458
+ }
6459
+ }
6460
+ }
6461
+ /* A transformed ancestor (e.g. rel-modal's centered panel with
6462
+ * translate(-50%,-50%)) becomes the containing block for position:fixed
6463
+ * descendants, and its overflow:hidden clips position:absolute ones —
6464
+ * either way the picker shell breaks inside a modal. Promoting shell
6465
+ * (and popup-mode backdrop) to the browser top layer sidesteps both —
6466
+ * same fix as rel-popover / rel-autocomplete / rel-select. `manual`
6467
+ * keeps outside-click and Escape handling under this component's own
6468
+ * control. While in the top layer the CSS anchoring (absolute, below
6469
+ * the trigger) is meaningless, so popover mode gets inline fixed
6470
+ * coordinates from updatePopoverPosition(). */
6471
+ showTopLayer() {
6472
+ for (const el of [this.mode === "popup" ? this.backdropEl : void 0, this.popoverEl]) {
6473
+ if (!el || typeof el.showPopover !== "function") continue;
6474
+ try {
6475
+ if (!el.matches(":popover-open")) el.showPopover();
6476
+ } catch {
6477
+ }
6478
+ }
6479
+ if (this.mode === "popover") {
6480
+ requestAnimationFrame(() => this.updatePopoverPosition());
6481
+ }
6482
+ }
6483
+ hideTopLayer() {
6484
+ for (const el of [this.popoverEl, this.backdropEl]) {
6485
+ if (!el || typeof el.hidePopover !== "function") continue;
6486
+ try {
6487
+ if (el.matches(":popover-open")) el.hidePopover();
6488
+ } catch {
6489
+ }
6490
+ }
6491
+ if (this.popoverEl) {
6492
+ this.popoverEl.style.position = "";
6493
+ this.popoverEl.style.top = "";
6494
+ this.popoverEl.style.left = "";
6495
+ }
5921
6496
  }
5922
6497
  toggleOpen() {
5923
6498
  if (this.disabled) return;
@@ -5933,6 +6508,7 @@ var RelRangePicker = class extends import_lit15.LitElement {
5933
6508
  this.draftActivePreset = null;
5934
6509
  this.isSelectingRange = false;
5935
6510
  this.hoverDate = null;
6511
+ this.focusedDate = null;
5936
6512
  if (this.draftFrom) {
5937
6513
  const date = this.parseISODate(this.draftFrom);
5938
6514
  if (date) {
@@ -6231,10 +6807,45 @@ var RelRangePicker = class extends import_lit15.LitElement {
6231
6807
  if (!this.isSelectingRange || day.isDisabled) return;
6232
6808
  this.hoverDate = this.formatISODate(day.date);
6233
6809
  }
6810
+ // ── Grid keyboard navigation (APG grid pattern) ──
6811
+ /**
6812
+ * The cell that carries tabindex="0". Prefers the last roved date, then the
6813
+ * draft start, then today, then the first selectable day — always exactly
6814
+ * one Tab stop, even after arrowing away with nothing selected.
6815
+ */
6816
+ getRovingIso(days) {
6817
+ const inView = days.filter((d) => d.isCurrentMonth);
6818
+ if (inView.length === 0) return "";
6819
+ const candidates = [
6820
+ this.focusedDate,
6821
+ this.draftFrom,
6822
+ this.formatISODate(/* @__PURE__ */ new Date())
6823
+ ];
6824
+ for (const iso of candidates) {
6825
+ if (iso && inView.some((d) => this.formatISODate(d.date) === iso)) {
6826
+ return iso;
6827
+ }
6828
+ }
6829
+ const firstEnabled = inView.find((d) => !d.isDisabled) ?? inView[0];
6830
+ return this.formatISODate(firstEnabled.date);
6831
+ }
6832
+ async moveGridFocusTo(date) {
6833
+ this.focusedDate = this.formatISODate(date);
6834
+ if (date.getMonth() !== this.viewMonth || date.getFullYear() !== this.viewYear) {
6835
+ this.viewMonth = date.getMonth();
6836
+ this.viewYear = date.getFullYear();
6837
+ }
6838
+ if (this.isSelectingRange && !this.isDateDisabled(date)) {
6839
+ this.hoverDate = this.focusedDate;
6840
+ }
6841
+ await this.updateComplete;
6842
+ this.renderRoot.querySelector(`.day-cell[data-iso="${this.focusedDate}"]`)?.focus();
6843
+ }
6234
6844
  // ── Render ──
6235
6845
  render() {
6236
6846
  const quickSelects = this.getQuickSelects();
6237
6847
  const days = this.getCalendarDays();
6848
+ const rovingIso = this.getRovingIso(days);
6238
6849
  const weekdayLabels = this.getWeekdayLabels();
6239
6850
  const draftFromDisplay = this.draftFrom ? this.formatDisplayDate(this.draftFrom) : "";
6240
6851
  const draftToDisplay = this.draftTo ? this.formatDisplayDate(this.draftTo) : "";
@@ -6273,10 +6884,11 @@ var RelRangePicker = class extends import_lit15.LitElement {
6273
6884
  </button>
6274
6885
  `}
6275
6886
 
6276
- ${isPopup && this.open ? import_lit15.html`<div part="overlay" class="backdrop" @click=${this.cancelPopover}></div>` : import_lit15.nothing}
6887
+ ${isPopup && this.open ? import_lit15.html`<div part="overlay" class="backdrop" popover="manual" @click=${this.cancelPopover}></div>` : import_lit15.nothing}
6277
6888
 
6278
6889
  <div
6279
6890
  part="panel" class="popover ${this.open ? "open" : ""} ${isPopup ? "popup" : ""}"
6891
+ popover="manual"
6280
6892
  role="dialog"
6281
6893
  aria-label="Select date range"
6282
6894
  >
@@ -6362,8 +6974,10 @@ var RelRangePicker = class extends import_lit15.LitElement {
6362
6974
  @mouseleave=${() => {
6363
6975
  this.hoverDate = null;
6364
6976
  }}
6977
+ @keydown=${this.handleGridKeydown}
6365
6978
  >
6366
6979
  ${days.map((day) => {
6980
+ const iso = this.formatISODate(day.date);
6367
6981
  const cellClasses = [
6368
6982
  "day-cell",
6369
6983
  day.isInRange ? "in-range" : "",
@@ -6378,8 +6992,11 @@ var RelRangePicker = class extends import_lit15.LitElement {
6378
6992
  <div
6379
6993
  class="${cellClasses}"
6380
6994
  role="gridcell"
6381
- aria-label="${this.formatISODate(day.date)}"
6995
+ tabindex="${day.isCurrentMonth && iso === rovingIso ? 0 : -1}"
6996
+ data-iso="${iso}"
6997
+ aria-label="${iso}"
6382
6998
  aria-selected="${day.isDraftFrom || day.isDraftTo || day.isInRange}"
6999
+ aria-disabled="${day.isDisabled ? "true" : import_lit15.nothing}"
6383
7000
  @click=${() => this.handleDayClick(day)}
6384
7001
  @mouseenter=${() => this.handleDayHover(day)}
6385
7002
  >
@@ -6487,6 +7104,17 @@ RelRangePicker.styles = import_lit15.css`
6487
7104
 
6488
7105
  /* ── Popover shell ── */
6489
7106
  .popover {
7107
+ /* Reset native [popover] UA styles first (the shell is promoted to
7108
+ the top layer so transformed / overflow:hidden ancestors — e.g. a
7109
+ centered rel-modal panel — can't hijack or clip it). While in the
7110
+ top layer, JS overrides the anchored position below with inline
7111
+ fixed coordinates; the CSS anchoring is the no-Popover-API
7112
+ fallback. */
7113
+ margin: 0;
7114
+ padding: 0;
7115
+ width: auto;
7116
+ height: auto;
7117
+ color: inherit;
6490
7118
  position: absolute;
6491
7119
  top: calc(100% + 4px);
6492
7120
  left: 0;
@@ -6511,9 +7139,16 @@ RelRangePicker.styles = import_lit15.css`
6511
7139
  .backdrop {
6512
7140
  position: fixed;
6513
7141
  inset: 0;
6514
- z-index: 2147483646;
6515
- background: rgba(0,0,0,0.4);
6516
- }
7142
+ /* Reset native [popover] UA styles — the backdrop is promoted to the
7143
+ top layer right before the panel so the panel stacks above it. */
7144
+ margin: 0;
7145
+ padding: 0;
7146
+ width: auto;
7147
+ height: auto;
7148
+ border: none;
7149
+ z-index: 2147483646;
7150
+ background: rgba(0,0,0,0.4);
7151
+ }
6517
7152
 
6518
7153
  /* ── Two-panel wrapper ── */
6519
7154
  .picker-wrapper {
@@ -6706,6 +7341,12 @@ RelRangePicker.styles = import_lit15.css`
6706
7341
  cursor: pointer;
6707
7342
  }
6708
7343
 
7344
+ .day-cell:focus-visible {
7345
+ outline: 2px solid var(--rel-border-focus);
7346
+ outline-offset: -2px;
7347
+ z-index: 2;
7348
+ }
7349
+
6709
7350
  .day-cell.other-month {
6710
7351
  opacity: 0.35;
6711
7352
  pointer-events: none;
@@ -6881,9 +7522,18 @@ __decorateClass([
6881
7522
  __decorateClass([
6882
7523
  (0, import_decorators15.state)()
6883
7524
  ], RelRangePicker.prototype, "hoverDate", 2);
7525
+ __decorateClass([
7526
+ (0, import_decorators15.state)()
7527
+ ], RelRangePicker.prototype, "focusedDate", 2);
6884
7528
  __decorateClass([
6885
7529
  (0, import_decorators15.query)(".trigger-button")
6886
7530
  ], RelRangePicker.prototype, "triggerButton", 2);
7531
+ __decorateClass([
7532
+ (0, import_decorators15.query)(".popover")
7533
+ ], RelRangePicker.prototype, "popoverEl", 2);
7534
+ __decorateClass([
7535
+ (0, import_decorators15.query)(".backdrop")
7536
+ ], RelRangePicker.prototype, "backdropEl", 2);
6887
7537
  RelRangePicker = __decorateClass([
6888
7538
  (0, import_decorators15.customElement)("rel-range-picker")
6889
7539
  ], RelRangePicker);
@@ -6904,6 +7554,33 @@ var RelChip = class extends import_lit16.LitElement {
6904
7554
  this.removed = false;
6905
7555
  this.draggableChip = false;
6906
7556
  this.dragging = false;
7557
+ /** The chip body is announced as a button, so it must also act on keys:
7558
+ * Enter/Space re-dispatch as a host click (consumers' @click handlers
7559
+ * fire), Delete/Backspace remove when removable, and arrow keys ask the
7560
+ * container to reorder a draggable chip (`chip-move`) — the keyboard
7561
+ * alternative to pointer drag-and-drop. */
7562
+ this.handleKeydown = (e) => {
7563
+ if (e.key === "Enter" || e.key === " ") {
7564
+ e.preventDefault();
7565
+ this.click();
7566
+ return;
7567
+ }
7568
+ if ((e.key === "Delete" || e.key === "Backspace") && this.removable) {
7569
+ e.preventDefault();
7570
+ this.handleRemove(e);
7571
+ return;
7572
+ }
7573
+ if (this.draggableChip && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
7574
+ e.preventDefault();
7575
+ this.dispatchEvent(
7576
+ new CustomEvent("chip-move", {
7577
+ detail: { direction: e.key === "ArrowLeft" ? -1 : 1 },
7578
+ bubbles: true,
7579
+ composed: true
7580
+ })
7581
+ );
7582
+ }
7583
+ };
6907
7584
  this.onDragStart = (e) => {
6908
7585
  if (!this.draggableChip) return;
6909
7586
  this.dragging = true;
@@ -6957,6 +7634,7 @@ var RelChip = class extends import_lit16.LitElement {
6957
7634
  role="button"
6958
7635
  tabindex="0"
6959
7636
  aria-label="${this.label}"
7637
+ @keydown=${this.handleKeydown}
6960
7638
  >
6961
7639
  ${showAvatar ? import_lit16.html`<rel-avatar type="text" text=${this.avatar} size="xs"></rel-avatar>` : null}
6962
7640
  ${showLeadingIcon ? import_lit16.html`<rel-icon part="icon" class="chip-icon" name="${this.leadingIcon}"></rel-icon>` : null}
@@ -7548,26 +8226,8 @@ var RelDrawer = class extends import_lit20.LitElement {
7548
8226
  this.close("escape");
7549
8227
  return;
7550
8228
  }
7551
- if (e.key === "Tab" && this.modal) {
7552
- const focusable = this.getFocusableElements();
7553
- if (focusable.length === 0) {
7554
- e.preventDefault();
7555
- return;
7556
- }
7557
- const firstFocusable = focusable[0];
7558
- const lastFocusable = focusable[focusable.length - 1];
7559
- const activeElement = document.activeElement;
7560
- if (e.shiftKey) {
7561
- if (activeElement === firstFocusable) {
7562
- e.preventDefault();
7563
- lastFocusable.focus();
7564
- }
7565
- } else {
7566
- if (activeElement === lastFocusable) {
7567
- e.preventDefault();
7568
- firstFocusable.focus();
7569
- }
7570
- }
8229
+ if (e.key === "Tab" && this.modal && this.panelElement) {
8230
+ trapTabKey(e, this.panelElement);
7571
8231
  }
7572
8232
  };
7573
8233
  // Drag-to-close handlers
@@ -7626,12 +8286,19 @@ var RelDrawer = class extends import_lit20.LitElement {
7626
8286
  }
7627
8287
  }
7628
8288
  handleOpen() {
7629
- this.previousFocus = document.activeElement;
8289
+ this.previousFocus = getDeepActiveElement();
7630
8290
  this.lockScroll();
7631
8291
  if (this.modal) {
7632
8292
  this.updateComplete.then(() => {
7633
- const focusTarget = this.querySelector("[autofocus]") ?? this.querySelector(".close-button") ?? this.querySelector(".panel");
7634
- focusTarget?.focus();
8293
+ const panel = this.panelElement;
8294
+ if (!panel) return;
8295
+ const focusTarget = getFocusableElements(panel)[0];
8296
+ if (focusTarget) {
8297
+ focusTarget.focus();
8298
+ } else {
8299
+ panel.tabIndex = -1;
8300
+ panel.focus();
8301
+ }
7635
8302
  });
7636
8303
  }
7637
8304
  document.addEventListener("keydown", this.handleKeydown);
@@ -7693,29 +8360,6 @@ var RelDrawer = class extends import_lit20.LitElement {
7693
8360
  document.body.style.overflow = this.originalBodyOverflow;
7694
8361
  document.body.style.paddingRight = this.originalBodyPaddingRight;
7695
8362
  }
7696
- getFocusableElements() {
7697
- if (!this.panelElement) return [];
7698
- const selectors = [
7699
- "a[href]",
7700
- "button:not([disabled])",
7701
- "textarea:not([disabled])",
7702
- "input:not([disabled])",
7703
- "select:not([disabled])",
7704
- '[tabindex]:not([tabindex="-1"])'
7705
- ].join(",");
7706
- const elements = Array.from(
7707
- this.panelElement.querySelectorAll(selectors)
7708
- );
7709
- const slots = this.panelElement.querySelectorAll("slot");
7710
- slots.forEach((slot) => {
7711
- const assigned = slot.assignedElements({ flatten: true });
7712
- assigned.forEach((el) => {
7713
- const focusableInSlot = el.querySelectorAll(selectors);
7714
- elements.push(...Array.from(focusableInSlot));
7715
- });
7716
- });
7717
- return elements;
7718
- }
7719
8363
  handleOverlayClick(e) {
7720
8364
  if (!this.dismissible || !this.closeOnBackdrop) return;
7721
8365
  if (e.target === this.overlayElement) {
@@ -8726,12 +9370,16 @@ var RelMenu = class extends import_lit24.LitElement {
8726
9370
  this.type = "default";
8727
9371
  this.items = [];
8728
9372
  this.focusedIndex = 0;
9373
+ this.subFocusedIndex = -1;
8729
9374
  this._items = [];
9375
+ /** Stable id prefix for aria-activedescendant targets. */
9376
+ this.menuId = `rel-menu-${Math.random().toString(36).slice(2)}`;
8730
9377
  /** Index of a submenu locked open by a click (survives hover-out). -1 = none. */
8731
9378
  this.lockedIndex = -1;
8732
9379
  this.handleDocumentClick = (e) => {
8733
9380
  if (!e.composedPath().includes(this)) {
8734
9381
  this.lockedIndex = -1;
9382
+ this.subFocusedIndex = -1;
8735
9383
  this._items = this._items.map((i) => ({ ...i, open: false }));
8736
9384
  }
8737
9385
  };
@@ -8818,35 +9466,95 @@ var RelMenu = class extends import_lit24.LitElement {
8818
9466
  })
8819
9467
  );
8820
9468
  }
9469
+ /** Keyboard-open locks the submenu (no hover-out to close it) and moves
9470
+ * virtual focus onto its first entry. */
9471
+ openSubmenuAt(index) {
9472
+ this.lockedIndex = index;
9473
+ this.subFocusedIndex = 0;
9474
+ this._items = this._items.map((i, idx) => ({ ...i, open: idx === index }));
9475
+ }
9476
+ closeSubmenus() {
9477
+ this.lockedIndex = -1;
9478
+ this.subFocusedIndex = -1;
9479
+ this._items = this._items.map((i) => ({ ...i, open: false }));
9480
+ }
8821
9481
  onKeydown(ev) {
8822
9482
  const last = this._items.length - 1;
9483
+ const focusedItem = this._items[this.focusedIndex];
9484
+ if (this.subFocusedIndex >= 0 && this.type === "arrow" && focusedItem?.open && focusedItem.submenu?.length) {
9485
+ const subLast = focusedItem.submenu.length - 1;
9486
+ switch (ev.key) {
9487
+ case "ArrowDown":
9488
+ this.subFocusedIndex = Math.min(subLast, this.subFocusedIndex + 1);
9489
+ ev.preventDefault();
9490
+ return;
9491
+ case "ArrowUp":
9492
+ this.subFocusedIndex = Math.max(0, this.subFocusedIndex - 1);
9493
+ ev.preventDefault();
9494
+ return;
9495
+ case "Home":
9496
+ this.subFocusedIndex = 0;
9497
+ ev.preventDefault();
9498
+ return;
9499
+ case "End":
9500
+ this.subFocusedIndex = subLast;
9501
+ ev.preventDefault();
9502
+ return;
9503
+ case "ArrowLeft":
9504
+ case "Escape":
9505
+ this.closeSubmenus();
9506
+ ev.preventDefault();
9507
+ ev.stopPropagation();
9508
+ return;
9509
+ case "Enter":
9510
+ case " ":
9511
+ this.onSubClick(this.focusedIndex, this.subFocusedIndex);
9512
+ ev.preventDefault();
9513
+ return;
9514
+ default:
9515
+ return;
9516
+ }
9517
+ }
8823
9518
  if (ev.key === "ArrowDown") {
8824
9519
  this.focusedIndex = Math.min(last, this.focusedIndex + 1);
8825
9520
  ev.preventDefault();
8826
9521
  } else if (ev.key === "ArrowUp") {
8827
9522
  this.focusedIndex = Math.max(0, this.focusedIndex - 1);
8828
9523
  ev.preventDefault();
9524
+ } else if (ev.key === "Home") {
9525
+ this.focusedIndex = 0;
9526
+ ev.preventDefault();
9527
+ } else if (ev.key === "End") {
9528
+ this.focusedIndex = last;
9529
+ ev.preventDefault();
8829
9530
  } else if (ev.key === "ArrowRight") {
8830
9531
  const item = this._items[this.focusedIndex];
8831
- if (this.type === "arrow" && item?.submenu?.length) {
8832
- this.lockedIndex = this.focusedIndex;
8833
- this._items = this._items.map((i, idx) => ({
8834
- ...i,
8835
- open: idx === this.focusedIndex ? true : i.open
8836
- }));
9532
+ if (this.type === "arrow" && item?.submenu?.length && !item.disabled) {
9533
+ this.openSubmenuAt(this.focusedIndex);
9534
+ ev.preventDefault();
8837
9535
  }
8838
9536
  } else if (ev.key === "ArrowLeft" || ev.key === "Escape") {
8839
- this.lockedIndex = -1;
8840
- this._items = this._items.map((i) => ({ ...i, open: false }));
9537
+ const anyOpen = this._items.some((i) => i.open);
9538
+ if (anyOpen) {
9539
+ this.closeSubmenus();
9540
+ ev.preventDefault();
9541
+ if (ev.key === "Escape") ev.stopPropagation();
9542
+ }
8841
9543
  } else if (ev.key === "Enter" || ev.key === " ") {
8842
9544
  const item = this._items[this.focusedIndex];
8843
- this.onItemClick(item, this.focusedIndex);
9545
+ if (this.type === "arrow" && item?.submenu?.length && !item.disabled) {
9546
+ this.openSubmenuAt(this.focusedIndex);
9547
+ } else {
9548
+ this.onItemClick(item, this.focusedIndex);
9549
+ }
8844
9550
  ev.preventDefault();
8845
9551
  }
8846
9552
  }
8847
9553
  renderMenuItem(item, index) {
8848
- const isFocused = this.focusedIndex === index;
9554
+ const isFocused = this.focusedIndex === index && this.subFocusedIndex < 0;
8849
9555
  const hasSubmenu = this.type === "arrow" && !!item.submenu?.length;
9556
+ const role = this.type === "checkbox" ? "menuitemcheckbox" : this.type === "select-option" ? "menuitemradio" : "menuitem";
9557
+ const ariaChecked = this.type === "checkbox" ? String(!!item.checked) : this.type === "select-option" ? String(!!item.active) : import_lit24.nothing;
8850
9558
  const control = this.type === "checkbox" ? import_lit24.html`<rel-checkbox
8851
9559
  class="menu-control"
8852
9560
  label="${item.label}"
@@ -8864,8 +9572,13 @@ var RelMenu = class extends import_lit24.LitElement {
8864
9572
  `;
8865
9573
  return import_lit24.html`
8866
9574
  <div
9575
+ id="${this.menuId}-item-${index}"
8867
9576
  class="rel-menu-item ${item.active ? "active" : ""} ${item.disabled ? "disabled" : ""}"
8868
- role="menuitem"
9577
+ role="${role}"
9578
+ aria-checked="${ariaChecked}"
9579
+ aria-disabled="${item.disabled ? "true" : import_lit24.nothing}"
9580
+ aria-haspopup="${hasSubmenu ? "menu" : import_lit24.nothing}"
9581
+ aria-expanded="${hasSubmenu ? String(!!item.open) : import_lit24.nothing}"
8869
9582
  ?data-focused="${isFocused}"
8870
9583
  @mouseenter="${() => this.onItemEnter(item, index)}"
8871
9584
  @mouseleave="${() => this.onItemLeave(item, index)}"
@@ -8874,12 +9587,16 @@ var RelMenu = class extends import_lit24.LitElement {
8874
9587
  ${hasSubmenu ? import_lit24.html`
8875
9588
  <div
8876
9589
  part="panel" class="rel-submenu ${item.open ? "visible" : ""}"
9590
+ role="menu"
8877
9591
  @click="${(e) => e.stopPropagation()}">
8878
9592
  ${item.submenu.map(
8879
9593
  (sub, subIndex) => import_lit24.html`
8880
9594
  <div
8881
- class="rel-menu-item ${sub.active ? "active" : ""}"
9595
+ id="${this.menuId}-item-${index}-sub-${subIndex}"
9596
+ class="rel-menu-item ${sub.active ? "active" : ""} ${sub.disabled ? "disabled" : ""}"
8882
9597
  role="menuitem"
9598
+ aria-disabled="${sub.disabled ? "true" : import_lit24.nothing}"
9599
+ ?data-focused="${this.focusedIndex === index && this.subFocusedIndex === subIndex}"
8883
9600
  @click="${(e) => this.onSubClick(index, subIndex, e)}">
8884
9601
  <span class="menu-label">${sub.label}</span>
8885
9602
  </div>
@@ -8891,12 +9608,14 @@ var RelMenu = class extends import_lit24.LitElement {
8891
9608
  `;
8892
9609
  }
8893
9610
  render() {
9611
+ const activeDescendant = this._items.length === 0 ? import_lit24.nothing : this.subFocusedIndex >= 0 ? `${this.menuId}-item-${this.focusedIndex}-sub-${this.subFocusedIndex}` : `${this.menuId}-item-${this.focusedIndex}`;
8894
9612
  return import_lit24.html`
8895
9613
  <style>${RelMenu._css}</style>
8896
9614
  <div
8897
9615
  part="list" class="rel-menu ${this.type === "fixed" || this.type === "fit" ? this.type : ""}"
8898
9616
  role="menu"
8899
9617
  tabindex="0"
9618
+ aria-activedescendant="${activeDescendant}"
8900
9619
  @keydown="${this.onKeydown}"
8901
9620
  @click="${(e) => e.stopPropagation()}">
8902
9621
  ${this._items.map((item, index) => this.renderMenuItem(item, index))}
@@ -8986,7 +9705,10 @@ RelMenu._css = `
8986
9705
  cursor: not-allowed;
8987
9706
  }
8988
9707
 
8989
- .rel-menu-item[data-focused="true"] {
9708
+ /* [data-focused] (presence, not ="true"): the ?data-focused boolean
9709
+ binding renders the attribute with an EMPTY value, so a ="true"
9710
+ selector never matches. */
9711
+ .rel-menu-item[data-focused] {
8990
9712
  outline: 2px solid var(--rel-border-focus);
8991
9713
  outline-offset: -2px;
8992
9714
  }
@@ -9077,6 +9799,9 @@ __decorateClass([
9077
9799
  __decorateClass([
9078
9800
  (0, import_decorators24.state)()
9079
9801
  ], RelMenu.prototype, "focusedIndex", 2);
9802
+ __decorateClass([
9803
+ (0, import_decorators24.state)()
9804
+ ], RelMenu.prototype, "subFocusedIndex", 2);
9080
9805
  __decorateClass([
9081
9806
  (0, import_decorators24.state)()
9082
9807
  ], RelMenu.prototype, "_items", 2);
@@ -9087,16 +9812,159 @@ RelMenu = __decorateClass([
9087
9812
  // src/components/rel-menubar/rel-menubar.ts
9088
9813
  var import_lit25 = require("lit");
9089
9814
  var import_decorators25 = require("lit/decorators.js");
9815
+
9816
+ // src/utils/shortcuts.ts
9817
+ function isApplePlatform() {
9818
+ if (typeof navigator === "undefined") return false;
9819
+ return /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent);
9820
+ }
9821
+ var KEY_ALIASES = {
9822
+ esc: "escape",
9823
+ space: " ",
9824
+ plus: "+",
9825
+ return: "enter",
9826
+ up: "arrowup",
9827
+ down: "arrowdown",
9828
+ left: "arrowleft",
9829
+ right: "arrowright"
9830
+ };
9831
+ function parseShortcut(shortcut) {
9832
+ const parsed = {
9833
+ key: "",
9834
+ ctrl: false,
9835
+ meta: false,
9836
+ alt: false,
9837
+ shift: false
9838
+ };
9839
+ const tokens = shortcut.split("+").map((t) => t.trim().toLowerCase()).filter(Boolean);
9840
+ if (shortcut.endsWith("+") && !shortcut.endsWith("++")) tokens.push("+");
9841
+ for (const token of tokens) {
9842
+ switch (token) {
9843
+ case "mod":
9844
+ if (isApplePlatform()) parsed.meta = true;
9845
+ else parsed.ctrl = true;
9846
+ break;
9847
+ case "ctrl":
9848
+ case "control":
9849
+ parsed.ctrl = true;
9850
+ break;
9851
+ case "meta":
9852
+ case "cmd":
9853
+ case "command":
9854
+ case "win":
9855
+ parsed.meta = true;
9856
+ break;
9857
+ case "alt":
9858
+ case "option":
9859
+ parsed.alt = true;
9860
+ break;
9861
+ case "shift":
9862
+ parsed.shift = true;
9863
+ break;
9864
+ default:
9865
+ parsed.key = KEY_ALIASES[token] ?? token;
9866
+ }
9867
+ }
9868
+ return parsed;
9869
+ }
9870
+ function matchesShortcut(e, shortcut) {
9871
+ const parsed = parseShortcut(shortcut);
9872
+ if (!parsed.key) return false;
9873
+ return e.key.toLowerCase() === parsed.key && e.ctrlKey === parsed.ctrl && e.metaKey === parsed.meta && e.altKey === parsed.alt && e.shiftKey === parsed.shift;
9874
+ }
9875
+ function isEditableTarget(e) {
9876
+ const target = e.composedPath()[0];
9877
+ if (!(target instanceof HTMLElement)) return false;
9878
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
9879
+ return true;
9880
+ }
9881
+ return target.isContentEditable;
9882
+ }
9883
+ function bindShortcut(shortcut, handler, options = {}) {
9884
+ const target = options.target ?? document;
9885
+ const listener = (event) => {
9886
+ const e = event;
9887
+ if (!matchesShortcut(e, shortcut)) return;
9888
+ if (!options.allowInEditable && isEditableTarget(e)) return;
9889
+ if (options.preventDefault !== false) e.preventDefault();
9890
+ handler(e);
9891
+ };
9892
+ target.addEventListener("keydown", listener);
9893
+ return () => target.removeEventListener("keydown", listener);
9894
+ }
9895
+ var APPLE_LABELS = {
9896
+ mod: "\u2318",
9897
+ meta: "\u2318",
9898
+ cmd: "\u2318",
9899
+ command: "\u2318",
9900
+ ctrl: "\u2303",
9901
+ control: "\u2303",
9902
+ alt: "\u2325",
9903
+ option: "\u2325",
9904
+ shift: "\u21E7"
9905
+ };
9906
+ var GENERIC_LABELS = {
9907
+ mod: "Ctrl",
9908
+ meta: "Win",
9909
+ cmd: "Win",
9910
+ command: "Win",
9911
+ ctrl: "Ctrl",
9912
+ control: "Ctrl",
9913
+ alt: "Alt",
9914
+ option: "Alt",
9915
+ shift: "Shift"
9916
+ };
9917
+ var KEY_LABELS = {
9918
+ arrowup: "\u2191",
9919
+ arrowdown: "\u2193",
9920
+ arrowleft: "\u2190",
9921
+ arrowright: "\u2192",
9922
+ escape: "Esc",
9923
+ " ": "Space"
9924
+ };
9925
+ function formatShortcutParts(shortcut) {
9926
+ const labels = isApplePlatform() ? APPLE_LABELS : GENERIC_LABELS;
9927
+ return shortcut.split("+").map((t) => t.trim()).filter(Boolean).map((token) => {
9928
+ const lower = token.toLowerCase();
9929
+ if (labels[lower]) return labels[lower];
9930
+ const key = KEY_ALIASES[lower] ?? lower;
9931
+ if (KEY_LABELS[key]) return KEY_LABELS[key];
9932
+ return key.length === 1 ? key.toUpperCase() : key[0].toUpperCase() + key.slice(1);
9933
+ });
9934
+ }
9935
+
9936
+ // src/components/rel-menubar/rel-menubar.ts
9090
9937
  var import_rel_icon10 = require("@reliability-design/icons/rel-icon");
9091
9938
  var RelMenubar = class extends import_lit25.LitElement {
9092
9939
  constructor() {
9093
9940
  super(...arguments);
9941
+ this.bindShortcuts = false;
9094
9942
  this.menus = [];
9095
9943
  this.openMenuIndex = -1;
9096
9944
  this.focusedTriggerIndex = 0;
9097
9945
  this.focusedItemIndex = -1;
9946
+ this.focusedSubItemIndex = -1;
9098
9947
  this.openSubmenuElement = null;
9099
9948
  this.hoverSwitchEnabled = false;
9949
+ /** Stable id prefix for aria-activedescendant targets (light DOM, so ids
9950
+ * must be unique per menubar instance on the page). */
9951
+ this.menubarId = `rel-menubar-${Math.random().toString(36).slice(2)}`;
9952
+ this.handleShortcutKeyDown = (e) => {
9953
+ if (!this.bindShortcuts || e.defaultPrevented) return;
9954
+ if (isEditableTarget(e)) return;
9955
+ for (const { menu } of this.menus) {
9956
+ for (const item of this.getFocusableItems(menu)) {
9957
+ const shortcut = item.getAttribute("data-shortcut");
9958
+ if (!shortcut) continue;
9959
+ if (item.disabled) continue;
9960
+ if (matchesShortcut(e, shortcut)) {
9961
+ e.preventDefault();
9962
+ this.handleItemClick(item);
9963
+ return;
9964
+ }
9965
+ }
9966
+ }
9967
+ };
9100
9968
  this.handleOutsideClick = (e) => {
9101
9969
  if (!e.composedPath().includes(this)) {
9102
9970
  this.closeAllMenus();
@@ -9110,6 +9978,7 @@ var RelMenubar = class extends import_lit25.LitElement {
9110
9978
  super.connectedCallback();
9111
9979
  this.discoverMenus();
9112
9980
  document.addEventListener("click", this.handleOutsideClick);
9981
+ document.addEventListener("keydown", this.handleShortcutKeyDown);
9113
9982
  this._onScroll = () => {
9114
9983
  this._repositionOpenMenu();
9115
9984
  this._repositionOpenSubmenu();
@@ -9119,6 +9988,7 @@ var RelMenubar = class extends import_lit25.LitElement {
9119
9988
  disconnectedCallback() {
9120
9989
  super.disconnectedCallback();
9121
9990
  document.removeEventListener("click", this.handleOutsideClick);
9991
+ document.removeEventListener("keydown", this.handleShortcutKeyDown);
9122
9992
  if (this._onScroll) {
9123
9993
  window.removeEventListener("scroll", this._onScroll, { capture: true });
9124
9994
  }
@@ -9152,12 +10022,26 @@ var RelMenubar = class extends import_lit25.LitElement {
9152
10022
  closeAllMenus() {
9153
10023
  this.openMenuIndex = -1;
9154
10024
  this.openSubmenuElement = null;
10025
+ this.focusedItemIndex = -1;
10026
+ this.focusedSubItemIndex = -1;
9155
10027
  this.hoverSwitchEnabled = false;
9156
10028
  this.requestUpdate();
9157
10029
  }
10030
+ /** Close everything and put DOM focus back on the trigger that owned the
10031
+ * open menu. (Capturing the index BEFORE closing — closeAllMenus resets
10032
+ * openMenuIndex to -1, which used to drop focus to <body>.) */
10033
+ closeAllMenusAndRefocusTrigger() {
10034
+ const idx = this.openMenuIndex;
10035
+ this.closeAllMenus();
10036
+ if (idx !== -1) {
10037
+ this.focusedTriggerIndex = idx;
10038
+ this.focusTrigger(idx);
10039
+ }
10040
+ }
9158
10041
  openMenu(index, triggerElement) {
9159
10042
  this.openMenuIndex = index;
9160
10043
  this.focusedItemIndex = -1;
10044
+ this.focusedSubItemIndex = -1;
9161
10045
  this.openSubmenuElement = null;
9162
10046
  this.requestUpdate();
9163
10047
  this.updateComplete.then(() => {
@@ -9199,27 +10083,20 @@ var RelMenubar = class extends import_lit25.LitElement {
9199
10083
  event.preventDefault();
9200
10084
  this.focusedTriggerIndex = Math.max(0, this.focusedTriggerIndex - 1);
9201
10085
  this.focusTrigger(this.focusedTriggerIndex);
9202
- } else if (key === "Enter" || key === " ") {
10086
+ } else if (key === "Enter" || key === " " || key === "ArrowDown") {
9203
10087
  event.preventDefault();
9204
10088
  const trigger = this.querySelector(
9205
10089
  `.menubar-trigger[data-index="${this.focusedTriggerIndex}"]`
9206
10090
  );
9207
10091
  if (trigger) {
9208
- this.openMenu(this.focusedTriggerIndex, trigger);
9209
- this.updateComplete.then(() => {
9210
- const focusTarget = this.querySelector(
9211
- "[autofocus], [role='menuitem']"
9212
- );
9213
- if (focusTarget) {
9214
- focusTarget.focus();
9215
- } else {
9216
- this.focusMenuItem(0);
9217
- }
9218
- });
10092
+ if (this.openMenuIndex !== this.focusedTriggerIndex) {
10093
+ this.openMenu(this.focusedTriggerIndex, trigger);
10094
+ }
10095
+ this.updateComplete.then(() => this.focusMenuItem(0));
9219
10096
  }
9220
- } else if (key === "ArrowDown" && this.openMenuIndex !== -1) {
10097
+ } else if (key === "Escape" && this.openMenuIndex !== -1) {
9221
10098
  event.preventDefault();
9222
- this.focusMenuItem(0);
10099
+ this.closeAllMenusAndRefocusTrigger();
9223
10100
  }
9224
10101
  }
9225
10102
  focusTrigger(index) {
@@ -9232,6 +10109,36 @@ var RelMenubar = class extends import_lit25.LitElement {
9232
10109
  const { key } = event;
9233
10110
  const menuContent = this.menus[this.openMenuIndex]?.menu;
9234
10111
  if (!menuContent) return;
10112
+ if (this.openSubmenuElement) {
10113
+ const subItems = this.getSubmenuItems(this.openSubmenuElement);
10114
+ if (key === "ArrowDown") {
10115
+ event.preventDefault();
10116
+ this.focusedSubItemIndex = Math.min(
10117
+ subItems.length - 1,
10118
+ this.focusedSubItemIndex + 1
10119
+ );
10120
+ } else if (key === "ArrowUp") {
10121
+ event.preventDefault();
10122
+ this.focusedSubItemIndex = Math.max(0, this.focusedSubItemIndex - 1);
10123
+ } else if (key === "Home") {
10124
+ event.preventDefault();
10125
+ this.focusedSubItemIndex = 0;
10126
+ } else if (key === "End") {
10127
+ event.preventDefault();
10128
+ this.focusedSubItemIndex = subItems.length - 1;
10129
+ } else if (key === "ArrowLeft" || key === "Escape") {
10130
+ event.preventDefault();
10131
+ event.stopPropagation();
10132
+ this.openSubmenuElement = null;
10133
+ this.focusedSubItemIndex = -1;
10134
+ } else if (key === "Enter" || key === " ") {
10135
+ event.preventDefault();
10136
+ const subItem = subItems[this.focusedSubItemIndex];
10137
+ if (subItem) this.handleSubmenuItemClick(subItem);
10138
+ }
10139
+ this.requestUpdate();
10140
+ return;
10141
+ }
9235
10142
  const items = this.getFocusableItems(menuContent);
9236
10143
  if (key === "ArrowDown") {
9237
10144
  event.preventDefault();
@@ -9241,30 +10148,56 @@ var RelMenubar = class extends import_lit25.LitElement {
9241
10148
  event.preventDefault();
9242
10149
  this.focusedItemIndex = Math.max(0, this.focusedItemIndex - 1);
9243
10150
  this.requestUpdate();
10151
+ } else if (key === "Home") {
10152
+ event.preventDefault();
10153
+ this.focusedItemIndex = 0;
10154
+ this.requestUpdate();
10155
+ } else if (key === "End") {
10156
+ event.preventDefault();
10157
+ this.focusedItemIndex = items.length - 1;
10158
+ this.requestUpdate();
9244
10159
  } else if (key === "ArrowRight") {
9245
10160
  event.preventDefault();
9246
10161
  const currentItem = items[this.focusedItemIndex];
9247
10162
  if (currentItem?.hasAttribute("data-submenu")) {
9248
- this.openSubmenu(currentItem);
10163
+ const panel = this.querySelector(
10164
+ `.menubar-panel[data-menu-index="${this.openMenuIndex}"]`
10165
+ );
10166
+ const renderedTrigger = panel?.querySelector(
10167
+ `[data-focus-index="${this.focusedItemIndex}"]`
10168
+ );
10169
+ this.focusedSubItemIndex = 0;
10170
+ this.openSubmenu(currentItem, renderedTrigger ?? void 0);
9249
10171
  }
9250
10172
  } else if (key === "ArrowLeft") {
9251
10173
  event.preventDefault();
9252
- if (this.openSubmenuElement) {
9253
- this.openSubmenuElement = null;
9254
- this.requestUpdate();
9255
- }
9256
10174
  } else if (key === "Escape") {
9257
10175
  event.preventDefault();
9258
- this.closeAllMenus();
9259
- this.focusTrigger(this.openMenuIndex);
10176
+ this.closeAllMenusAndRefocusTrigger();
9260
10177
  } else if (key === "Enter" || key === " ") {
9261
10178
  event.preventDefault();
9262
10179
  const currentItem = items[this.focusedItemIndex];
9263
10180
  if (currentItem) {
9264
- this.handleItemClick(currentItem);
10181
+ if (currentItem.hasAttribute("data-submenu")) {
10182
+ const panel = this.querySelector(
10183
+ `.menubar-panel[data-menu-index="${this.openMenuIndex}"]`
10184
+ );
10185
+ const renderedTrigger = panel?.querySelector(
10186
+ `[data-focus-index="${this.focusedItemIndex}"]`
10187
+ );
10188
+ this.focusedSubItemIndex = 0;
10189
+ this.openSubmenu(currentItem, renderedTrigger ?? void 0);
10190
+ } else {
10191
+ this.handleItemClick(currentItem);
10192
+ }
9265
10193
  }
9266
10194
  }
9267
10195
  }
10196
+ getSubmenuItems(submenuElement) {
10197
+ return Array.from(submenuElement.children).filter(
10198
+ (el) => el.hasAttribute("data-item")
10199
+ );
10200
+ }
9268
10201
  getFocusableItems(menuContent) {
9269
10202
  return Array.from(menuContent.children).filter(
9270
10203
  (el) => el.hasAttribute("data-item") || el.hasAttribute("data-checkbox") || el.hasAttribute("data-radio") || el.hasAttribute("data-submenu")
@@ -9301,27 +10234,41 @@ var RelMenubar = class extends import_lit25.LitElement {
9301
10234
  } else if (item.hasAttribute("data-submenu")) {
9302
10235
  this.openSubmenu(item);
9303
10236
  } else {
9304
- this.closeAllMenus();
10237
+ this.dispatchSelectEvent(item);
10238
+ this.closeAllMenusAndRefocusTrigger();
9305
10239
  }
9306
10240
  }
9307
- openSubmenu(submenuTrigger, renderedTrigger, submenuIndex) {
10241
+ dispatchSelectEvent(item) {
10242
+ this.dispatchEvent(
10243
+ new CustomEvent("rel-menubar-select", {
10244
+ detail: {
10245
+ label: item.textContent?.trim() ?? "",
10246
+ value: item.getAttribute("value") ?? void 0,
10247
+ shortcut: item.getAttribute("data-shortcut") ?? void 0
10248
+ },
10249
+ bubbles: true,
10250
+ composed: true
10251
+ })
10252
+ );
10253
+ }
10254
+ openSubmenu(submenuTrigger, renderedTrigger) {
9308
10255
  this.openSubmenuElement = submenuTrigger;
9309
10256
  this.requestUpdate();
9310
10257
  this.updateComplete.then(() => {
9311
- const submenuId = `submenu-${submenuIndex ?? 0}`;
9312
- const submenuPanel = this.querySelector(
9313
- `.submenu-panel[data-submenu-id="${submenuId}"]`
9314
- );
10258
+ const submenuPanel = this.querySelector(".submenu-panel.open");
9315
10259
  if (submenuPanel) {
9316
- const positionTarget = renderedTrigger ?? submenuTrigger;
9317
- const rect = positionTarget.getBoundingClientRect();
9318
- submenuPanel.style.top = `${rect.top}px`;
9319
- submenuPanel.style.left = `${rect.right + 4}px`;
10260
+ const positionTarget = renderedTrigger ?? submenuPanel.previousElementSibling;
10261
+ const rect = positionTarget?.getBoundingClientRect();
10262
+ if (rect) {
10263
+ submenuPanel.style.top = `${rect.top}px`;
10264
+ submenuPanel.style.left = `${rect.right + 4}px`;
10265
+ }
9320
10266
  }
9321
10267
  });
9322
10268
  }
9323
- handleSubmenuItemClick(_item) {
9324
- this.closeAllMenus();
10269
+ handleSubmenuItemClick(item) {
10270
+ this.dispatchSelectEvent(item);
10271
+ this.closeAllMenusAndRefocusTrigger();
9325
10272
  }
9326
10273
  dispatchChangeEvent(type, labelOrName, value) {
9327
10274
  const detail = type === "checkbox" ? { type, label: labelOrName, name: "", checked: value } : { type, label: "", name: labelOrName, value };
@@ -9333,16 +10280,21 @@ var RelMenubar = class extends import_lit25.LitElement {
9333
10280
  })
9334
10281
  );
9335
10282
  }
9336
- renderMenuItem(item, index) {
9337
- const isFocused = index === this.focusedItemIndex;
10283
+ renderMenuItem(item, focusIndex, menuIndex) {
10284
+ const isFocused = focusIndex >= 0 && focusIndex === this.focusedItemIndex;
10285
+ const itemId = focusIndex >= 0 ? `${this.menubarId}-m${menuIndex}-i${focusIndex}` : import_lit25.nothing;
9338
10286
  if (item.hasAttribute("data-item")) {
9339
10287
  const button = item;
9340
10288
  const disabled = button.disabled;
9341
10289
  const shortcut = button.getAttribute("data-shortcut") || "";
9342
10290
  return import_lit25.html`
9343
10291
  <div
10292
+ id="${itemId}"
9344
10293
  class="menu-item ${isFocused ? "focused" : ""}"
10294
+ role="menuitem"
10295
+ data-focus-index="${focusIndex}"
9345
10296
  data-disabled="${disabled}"
10297
+ aria-disabled="${disabled ? "true" : import_lit25.nothing}"
9346
10298
  @click="${() => !disabled && this.handleItemClick(item)}"
9347
10299
  >
9348
10300
  <span>${button.textContent}</span>
@@ -9351,7 +10303,7 @@ var RelMenubar = class extends import_lit25.LitElement {
9351
10303
  `;
9352
10304
  }
9353
10305
  if (item.hasAttribute("data-separator")) {
9354
- return import_lit25.html`<div class="menu-separator"></div>`;
10306
+ return import_lit25.html`<div class="menu-separator" role="separator"></div>`;
9355
10307
  }
9356
10308
  if (item.hasAttribute("data-checkbox")) {
9357
10309
  const label = item;
@@ -9359,6 +10311,8 @@ var RelMenubar = class extends import_lit25.LitElement {
9359
10311
  const labelText = Array.from(label.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE).map((n) => n.textContent?.trim()).filter(Boolean).join(" ") || label.getAttribute("label") || "";
9360
10312
  return import_lit25.html`
9361
10313
  <rel-checkbox
10314
+ id="${itemId}"
10315
+ data-focus-index="${focusIndex}"
9362
10316
  label=${labelText}
9363
10317
  .status=${checkbox?.checked ? "checked" : "unchecked"}
9364
10318
  @rel-change=${(e) => {
@@ -9370,17 +10324,21 @@ var RelMenubar = class extends import_lit25.LitElement {
9370
10324
  }
9371
10325
  if (item.hasAttribute("data-submenu")) {
9372
10326
  const label = item.getAttribute("label") || item.textContent?.trim() || "";
9373
- const submenuId = `submenu-${index}`;
9374
10327
  return import_lit25.html`
9375
10328
  <div
10329
+ id="${itemId}"
9376
10330
  class="menu-item menu-submenu-trigger ${isFocused ? "focused" : ""}"
9377
- @click="${(e) => this.openSubmenu(item, e.currentTarget, index)}"
9378
- @mouseenter="${(e) => this.openSubmenu(item, e.currentTarget, index)}"
10331
+ role="menuitem"
10332
+ data-focus-index="${focusIndex}"
10333
+ aria-haspopup="menu"
10334
+ aria-expanded="${this.openSubmenuElement === item}"
10335
+ @click="${(e) => this.openSubmenu(item, e.currentTarget)}"
10336
+ @mouseenter="${(e) => this.openSubmenu(item, e.currentTarget)}"
9379
10337
  >
9380
10338
  <span>${label}</span>
9381
10339
  <rel-icon name="chevron-right"></rel-icon>
9382
10340
  </div>
9383
- ${this.renderSubmenu(item, submenuId)}
10341
+ ${this.renderSubmenu(item)}
9384
10342
  `;
9385
10343
  }
9386
10344
  if (item.hasAttribute("data-radio-group")) {
@@ -9405,24 +10363,23 @@ var RelMenubar = class extends import_lit25.LitElement {
9405
10363
  }
9406
10364
  return null;
9407
10365
  }
9408
- renderSubmenu(submenuElement, submenuId) {
10366
+ renderSubmenu(submenuElement) {
9409
10367
  const isOpen = this.openSubmenuElement === submenuElement;
9410
- const submenuItems = Array.from(submenuElement.children);
10368
+ const submenuItems = this.getSubmenuItems(submenuElement);
9411
10369
  return import_lit25.html`
9412
- <div class="submenu-panel ${isOpen ? "open" : ""}" data-submenu-id="${submenuId}">
9413
- ${submenuItems.map((subItem) => {
9414
- if (subItem.hasAttribute("data-item")) {
9415
- const button = subItem;
9416
- return import_lit25.html`
9417
- <div
9418
- class="menu-item"
9419
- @click="${() => this.handleSubmenuItemClick(subItem)}"
9420
- >
9421
- <span>${button.textContent}</span>
9422
- </div>
9423
- `;
9424
- }
9425
- return null;
10370
+ <div class="submenu-panel ${isOpen ? "open" : ""}" role="menu">
10371
+ ${submenuItems.map((subItem, subIdx) => {
10372
+ const button = subItem;
10373
+ return import_lit25.html`
10374
+ <div
10375
+ id="${isOpen ? `${this.menubarId}-sub-${subIdx}` : import_lit25.nothing}"
10376
+ class="menu-item ${isOpen && subIdx === this.focusedSubItemIndex ? "focused" : ""}"
10377
+ role="menuitem"
10378
+ @click="${() => this.handleSubmenuItemClick(subItem)}"
10379
+ >
10380
+ <span>${button.textContent}</span>
10381
+ </div>
10382
+ `;
9426
10383
  })}
9427
10384
  </div>
9428
10385
  `;
@@ -9430,28 +10387,40 @@ var RelMenubar = class extends import_lit25.LitElement {
9430
10387
  renderMenuPanel(menuState, index) {
9431
10388
  const isOpen = index === this.openMenuIndex;
9432
10389
  const items = Array.from(menuState.menu.children);
10390
+ const focusables = this.getFocusableItems(menuState.menu);
10391
+ const activeDescendant = !isOpen ? import_lit25.nothing : this.focusedSubItemIndex >= 0 ? `${this.menubarId}-sub-${this.focusedSubItemIndex}` : this.focusedItemIndex >= 0 ? `${this.menubarId}-m${index}-i${this.focusedItemIndex}` : import_lit25.nothing;
9433
10392
  return import_lit25.html`
9434
10393
  <div
9435
10394
  class="menubar-panel ${isOpen ? "open" : ""}"
9436
10395
  data-menu-index="${index}"
9437
10396
  tabindex="-1"
10397
+ role="menu"
10398
+ aria-activedescendant="${activeDescendant}"
9438
10399
  @keydown="${this.handlePanelKeyDown}"
9439
10400
  >
9440
- ${items.map((item, idx) => this.renderMenuItem(item, idx))}
10401
+ ${items.map(
10402
+ (item) => this.renderMenuItem(item, focusables.indexOf(item), index)
10403
+ )}
9441
10404
  </div>
9442
10405
  `;
9443
10406
  }
9444
10407
  render() {
9445
10408
  return import_lit25.html`
9446
10409
  <style>${RelMenubar._css}</style>
9447
- <div class="menubar-container">
10410
+ <div class="menubar-container" role="menubar">
9448
10411
  ${this.menus.map(
9449
10412
  (menuState, index) => import_lit25.html`
9450
10413
  <button
9451
10414
  class="menubar-trigger ${index === this.openMenuIndex ? "active" : ""}"
9452
10415
  data-index="${index}"
10416
+ role="menuitem"
10417
+ aria-haspopup="menu"
10418
+ aria-expanded="${index === this.openMenuIndex}"
9453
10419
  @click="${(e) => this.handleTriggerClick(index, e)}"
9454
10420
  @mouseenter="${(e) => this.handleTriggerMouseEnter(index, e)}"
10421
+ @focus="${() => {
10422
+ this.focusedTriggerIndex = index;
10423
+ }}"
9455
10424
  @keydown="${this.handleTriggerKeyDown}"
9456
10425
  >
9457
10426
  ${menuState.menu.label}
@@ -9622,6 +10591,9 @@ RelMenubar._css = `
9622
10591
  cursor: pointer;
9623
10592
  }
9624
10593
  `;
10594
+ __decorateClass([
10595
+ (0, import_decorators25.property)({ type: Boolean, attribute: "bind-shortcuts" })
10596
+ ], RelMenubar.prototype, "bindShortcuts", 2);
9625
10597
  __decorateClass([
9626
10598
  (0, import_decorators25.state)()
9627
10599
  ], RelMenubar.prototype, "menus", 2);
@@ -9634,6 +10606,9 @@ __decorateClass([
9634
10606
  __decorateClass([
9635
10607
  (0, import_decorators25.state)()
9636
10608
  ], RelMenubar.prototype, "focusedItemIndex", 2);
10609
+ __decorateClass([
10610
+ (0, import_decorators25.state)()
10611
+ ], RelMenubar.prototype, "focusedSubItemIndex", 2);
9637
10612
  __decorateClass([
9638
10613
  (0, import_decorators25.state)()
9639
10614
  ], RelMenubar.prototype, "openSubmenuElement", 2);
@@ -10302,7 +11277,6 @@ RelTopbar.styles = import_lit34.css`
10302
11277
  color: var(--rel-topbar-item-color);
10303
11278
 
10304
11279
 
10305
-
10306
11280
  text-decoration: none;
10307
11281
  font-family: var(--rel-body-md-medium-font-family);
10308
11282
  font-weight: var(--rel-body-md-medium-font-weight);
@@ -10552,8 +11526,6 @@ RelTopbarItem.styles = import_lit35.css`
10552
11526
  color: var(--_item-color);
10553
11527
 
10554
11528
 
10555
-
10556
-
10557
11529
  text-decoration: none;
10558
11530
  white-space: nowrap;
10559
11531
  position: relative;
@@ -10785,9 +11757,7 @@ RelTopbarProfile.styles = import_lit36.css`
10785
11757
 
10786
11758
  .name {
10787
11759
 
10788
-
10789
11760
  color: var(--rel-text-default);
10790
-
10791
11761
  white-space: nowrap;
10792
11762
  overflow: hidden;
10793
11763
  text-overflow: ellipsis;
@@ -10806,9 +11776,7 @@ RelTopbarProfile.styles = import_lit36.css`
10806
11776
 
10807
11777
  .subtitle {
10808
11778
 
10809
-
10810
11779
  color: var(--rel-text-placeholder);
10811
-
10812
11780
  white-space: nowrap;
10813
11781
  overflow: hidden;
10814
11782
  text-overflow: ellipsis;
@@ -10833,9 +11801,7 @@ RelTopbarProfile.styles = import_lit36.css`
10833
11801
  padding: 2px var(--rel-spacing-2xs, 4px);
10834
11802
  border-radius: var(--rel-border-radius-xs, 2px);
10835
11803
 
10836
-
10837
11804
  text-transform: uppercase;
10838
-
10839
11805
  flex-shrink: 0;
10840
11806
  font-family: var(--rel-body-xs-semibold-font-family);
10841
11807
  font-weight: var(--rel-body-xs-semibold-font-weight);
@@ -11182,7 +12148,7 @@ var RelOtp = class extends import_lit37.LitElement {
11182
12148
  data-index="${index}"
11183
12149
  type="${this.mask ? "password" : "text"}"
11184
12150
  inputmode="${this.getInputMode()}"
11185
- autocomplete="new-password"
12151
+ autocomplete="one-time-code"
11186
12152
  pattern="${this.pattern || ""}"
11187
12153
  placeholder="${this.placeholder}"
11188
12154
  maxlength="1"
@@ -11245,7 +12211,6 @@ RelOtp.styles = import_lit37.css`
11245
12211
  align-items: center;
11246
12212
  justify-content: center;
11247
12213
  color: var(--rel-border-default);
11248
-
11249
12214
  user-select: none;
11250
12215
  padding: 0 var(--rel-spacing-3xs);
11251
12216
  font-family: var(--rel-action-xl-font-family);
@@ -11266,7 +12231,6 @@ RelOtp.styles = import_lit37.css`
11266
12231
  border-radius: var(--rel-border-radius-sm);
11267
12232
  background: var(--rel-bg-neutral);
11268
12233
 
11269
-
11270
12234
  text-align: center;
11271
12235
  color: var(--rel-text-default);
11272
12236
  outline: none;
@@ -11488,6 +12452,14 @@ var RelPagination = class extends import_lit38.LitElement {
11488
12452
  }
11489
12453
  return pages;
11490
12454
  }
12455
+ /** Abbreviate large page numbers (368178 → "368k") so buttons stay compact.
12456
+ Exact below 10,000; floored to k up to 1M; then M with one decimal. */
12457
+ formatPageLabel(n) {
12458
+ if (n < 1e4) return String(n);
12459
+ if (n < 1e6) return `${Math.floor(n / 1e3)}k`;
12460
+ const m = Math.floor(n / 1e5) / 10;
12461
+ return `${Number.isInteger(m) ? m.toFixed(0) : m}M`;
12462
+ }
11491
12463
  handlePageChange(newPage) {
11492
12464
  const total = this.computedTotalPages;
11493
12465
  if (this.disabled || newPage < 1 || newPage > total || newPage === this.activePage) return;
@@ -11533,15 +12505,20 @@ var RelPagination = class extends import_lit38.LitElement {
11533
12505
  <div class="pag-stack">
11534
12506
  ${this.renderNavBtn("prev")}
11535
12507
  <div class="pag-numbers">
11536
- ${pages.map(
11537
- (p) => p === -1 ? import_lit38.html`<span class="pag-ellipsis">...</span>` : import_lit38.html`<button
11538
- type="button"
11539
- class="pag-page-btn ${Math.abs(p - current) <= 1 ? "near" : ""} ${p === 1 || p === total ? "edge" : ""}"
11540
- aria-label="Page ${p}"
11541
- aria-current="${p === current ? "page" : "false"}"
11542
- @click="${() => this.handlePageChange(p)}"
11543
- >${p}</button>`
11544
- )}
12508
+ ${pages.map((p) => {
12509
+ if (p === -1) return import_lit38.html`<span class="pag-ellipsis">...</span>`;
12510
+ const isNear = Math.abs(p - current) <= 1;
12511
+ const isEdge = p === 1 || p === total;
12512
+ const label = isEdge && !isNear ? this.formatPageLabel(p) : String(p);
12513
+ return import_lit38.html`<button
12514
+ type="button"
12515
+ class="pag-page-btn ${isNear ? "near" : ""} ${p === 1 || p === total ? "edge" : ""}"
12516
+ aria-label="Page ${p}"
12517
+ title="${label === String(p) ? import_lit38.nothing : `Page ${p}`}"
12518
+ aria-current="${p === current ? "page" : "false"}"
12519
+ @click="${() => this.handlePageChange(p)}"
12520
+ >${label}</button>`;
12521
+ })}
11545
12522
  </div>
11546
12523
  ${this.renderNavBtn("next")}
11547
12524
  </div>
@@ -11694,9 +12671,10 @@ RelPagination.styles = import_lit38.css`
11694
12671
  display: inline-flex;
11695
12672
  align-items: center;
11696
12673
  justify-content: center;
11697
- width: 40px;
12674
+ width: auto;
12675
+ min-width: 40px;
11698
12676
  height: 40px;
11699
- padding: var(--rel-spacing-xs);
12677
+ padding: 0 var(--rel-spacing-xs);
11700
12678
  border-radius: var(--rel-border-radius-sm);
11701
12679
  border: 1px solid var(--rel-border-default);
11702
12680
  background: var(--rel-bg-neutral);
@@ -11944,6 +12922,14 @@ var RelPopover = class extends import_lit39.LitElement {
11944
12922
  e.stopPropagation();
11945
12923
  this.setOpen(!this.activeOpen);
11946
12924
  };
12925
+ this.onHostKeyDown = (e) => {
12926
+ if (this.disabled) return;
12927
+ if (e.key !== "Enter" && e.key !== " ") return;
12928
+ if (!this.triggerEl || !e.composedPath().includes(this.triggerEl)) return;
12929
+ e.preventDefault();
12930
+ e.stopPropagation();
12931
+ this.setOpen(!this.activeOpen);
12932
+ };
11947
12933
  this.onOutsidePointerDown = (e) => {
11948
12934
  if (!this.activeOpen) return;
11949
12935
  const path = e.composedPath();
@@ -12017,10 +13003,12 @@ var RelPopover = class extends import_lit39.LitElement {
12017
13003
  super.connectedCallback();
12018
13004
  this.internalOpen = this.isControlled() ? !!this.open : this.defaultOpen;
12019
13005
  this.addEventListener("pointerdown", this.onHostPointerDown, true);
13006
+ this.addEventListener("keydown", this.onHostKeyDown, true);
12020
13007
  }
12021
13008
  disconnectedCallback() {
12022
13009
  super.disconnectedCallback();
12023
13010
  this.removeEventListener("pointerdown", this.onHostPointerDown, true);
13011
+ this.removeEventListener("keydown", this.onHostKeyDown, true);
12024
13012
  this.removeGlobalListeners();
12025
13013
  this.hideNativePopover();
12026
13014
  }
@@ -12061,7 +13049,7 @@ var RelPopover = class extends import_lit39.LitElement {
12061
13049
  * OPEN / CLOSE
12062
13050
  * ======================= */
12063
13051
  onOpen() {
12064
- this.returnFocusEl = document.activeElement ?? this.triggerEl;
13052
+ this.returnFocusEl = getDeepActiveElement() ?? this.triggerEl;
12065
13053
  this.addGlobalListeners();
12066
13054
  this.showNativePopover();
12067
13055
  requestAnimationFrame(() => {
@@ -12169,6 +13157,12 @@ var RelPopover = class extends import_lit39.LitElement {
12169
13157
  this.triggerEl.setAttribute("aria-haspopup", "dialog");
12170
13158
  this.triggerEl.setAttribute("aria-expanded", String(this.activeOpen));
12171
13159
  this.triggerEl.setAttribute("aria-controls", this.uniqueId);
13160
+ if (this.triggerEl.tabIndex < 0 && !this.triggerEl.hasAttribute("tabindex") && getFocusableElements(this.triggerEl).length === 0) {
13161
+ this.triggerEl.setAttribute("tabindex", "0");
13162
+ if (!this.triggerEl.hasAttribute("role")) {
13163
+ this.triggerEl.setAttribute("role", "button");
13164
+ }
13165
+ }
12172
13166
  }
12173
13167
  /* =======================
12174
13168
  * RENDER
@@ -12648,6 +13642,14 @@ var RelResizableGroup = class extends import_lit42.LitElement {
12648
13642
  const delta = e.key === "ArrowUp" ? -step : step;
12649
13643
  this.applyKeyboardResize(handleIndex, delta);
12650
13644
  applied = true;
13645
+ } else if (e.key === "Home" || e.key === "End") {
13646
+ const leftPanel = this.panels[handleIndex];
13647
+ if (leftPanel && handleIndex + 1 < this.panels.length) {
13648
+ e.preventDefault();
13649
+ const target2 = e.key === "Home" ? leftPanel.minSize : leftPanel.maxSize;
13650
+ this.applyKeyboardResize(handleIndex, target2 - leftPanel.currentSize);
13651
+ applied = true;
13652
+ }
12651
13653
  }
12652
13654
  if (applied) {
12653
13655
  this.dispatchResizeEvent("rel-resize-end");
@@ -12687,6 +13689,13 @@ var RelResizableGroup = class extends import_lit42.LitElement {
12687
13689
  });
12688
13690
  const sizes = this.calculateInitialSizes();
12689
13691
  this.applySizes(sizes);
13692
+ this.handles.forEach((handle, idx) => {
13693
+ const left = sizes[idx];
13694
+ const right = sizes[idx + 1];
13695
+ if (left !== void 0 && right !== void 0 && left + right > 0) {
13696
+ handle.updateAriaValue(left / (left + right) * 100);
13697
+ }
13698
+ });
12690
13699
  }
12691
13700
  calculateInitialSizes() {
12692
13701
  const sizes = [];
@@ -13065,6 +14074,26 @@ var RelSegmentedButton = class extends import_lit45.LitElement {
13065
14074
  this.active = "";
13066
14075
  this.size = "medium";
13067
14076
  this.align = "left";
14077
+ /** APG radio-group navigation: arrows move focus AND select, the group is
14078
+ * one Tab stop (roving tabindex on the selected segment). */
14079
+ this.onKeyDown = (e) => {
14080
+ const prevKey = e.key === "ArrowLeft" || e.key === "ArrowUp";
14081
+ const nextKey = e.key === "ArrowRight" || e.key === "ArrowDown";
14082
+ if (!prevKey && !nextKey && e.key !== "Home" && e.key !== "End") return;
14083
+ const enabled = this.options.filter((o) => !o.disabled);
14084
+ if (enabled.length === 0) return;
14085
+ e.preventDefault();
14086
+ const currentIdx = enabled.findIndex((o) => o.value === this.active);
14087
+ let target;
14088
+ if (e.key === "Home") target = 0;
14089
+ else if (e.key === "End") target = enabled.length - 1;
14090
+ else if (prevKey) target = currentIdx <= 0 ? enabled.length - 1 : currentIdx - 1;
14091
+ else target = (currentIdx + 1) % enabled.length;
14092
+ this.setActive(enabled[target]);
14093
+ this.updateComplete.then(() => {
14094
+ this.shadowRoot?.querySelector('button[tabindex="0"]')?.focus();
14095
+ });
14096
+ };
13068
14097
  }
13069
14098
  setActive(option) {
13070
14099
  if (!option.disabled) {
@@ -13078,7 +14107,7 @@ var RelSegmentedButton = class extends import_lit45.LitElement {
13078
14107
  );
13079
14108
  }
13080
14109
  }
13081
- renderSegment(option) {
14110
+ renderSegment(option, rovingValue) {
13082
14111
  const isActive = this.active === option.value;
13083
14112
  const classes = [
13084
14113
  "segment",
@@ -13088,6 +14117,9 @@ var RelSegmentedButton = class extends import_lit45.LitElement {
13088
14117
  return import_lit45.html`
13089
14118
  <button
13090
14119
  part="option" class="${classes}"
14120
+ role="radio"
14121
+ aria-checked="${isActive}"
14122
+ tabindex="${!option.disabled && option.value === rovingValue ? 0 : -1}"
13091
14123
  ?disabled="${option.disabled}"
13092
14124
  @click="${() => this.setActive(option)}">
13093
14125
  ${option.label}
@@ -13100,9 +14132,15 @@ var RelSegmentedButton = class extends import_lit45.LitElement {
13100
14132
  this.size,
13101
14133
  `align-${this.align}`
13102
14134
  ].join(" ");
14135
+ const enabled = this.options.filter((o) => !o.disabled);
14136
+ const rovingValue = enabled.some((o) => o.value === this.active) ? this.active : enabled[0]?.value ?? null;
13103
14137
  return import_lit45.html`
13104
- <div part="base" class="${containerClasses}">
13105
- ${this.options.map((option) => this.renderSegment(option))}
14138
+ <div
14139
+ part="base" class="${containerClasses}"
14140
+ role="radiogroup"
14141
+ @keydown="${this.onKeyDown}"
14142
+ >
14143
+ ${this.options.map((option) => this.renderSegment(option, rovingValue))}
13106
14144
  </div>
13107
14145
  `;
13108
14146
  }
@@ -13264,6 +14302,8 @@ var RelSelect = class extends import_lit46.LitElement {
13264
14302
  this.noSearch = false;
13265
14303
  this.open = false;
13266
14304
  this.highlightedIndex = -1;
14305
+ this.selectAllHighlighted = false;
14306
+ this.typeaheadBuffer = "";
13267
14307
  this.panelStyle = "";
13268
14308
  this.searchValue = "";
13269
14309
  this.internalState = "default";
@@ -13299,11 +14339,20 @@ var RelSelect = class extends import_lit46.LitElement {
13299
14339
  if (left + t.width > window.innerWidth - 8) {
13300
14340
  left = Math.max(8, window.innerWidth - t.width - 8);
13301
14341
  }
14342
+ this.listboxEl.style.top = `${top}px`;
14343
+ this.listboxEl.style.left = `${left}px`;
14344
+ this.listboxEl.style.width = `${t.width}px`;
14345
+ const applied = this.listboxEl.getBoundingClientRect();
14346
+ top -= applied.top - top;
14347
+ left -= applied.left - left;
14348
+ this.listboxEl.style.top = `${top}px`;
14349
+ this.listboxEl.style.left = `${left}px`;
13302
14350
  this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
13303
14351
  };
13304
14352
  this.onSearchInput = (e) => {
13305
14353
  this.searchValue = e.target.value;
13306
14354
  this.highlightedIndex = this.filteredOptions.length > 0 ? 0 : -1;
14355
+ this.selectAllHighlighted = false;
13307
14356
  requestAnimationFrame(() => this.updatePosition());
13308
14357
  };
13309
14358
  }
@@ -13316,6 +14365,7 @@ var RelSelect = class extends import_lit46.LitElement {
13316
14365
  disconnectedCallback() {
13317
14366
  super.disconnectedCallback();
13318
14367
  this.removeGlobalListeners();
14368
+ this.hideNativePopover();
13319
14369
  this.chipResizeObserver?.disconnect();
13320
14370
  }
13321
14371
  firstUpdated() {
@@ -13430,8 +14480,10 @@ var RelSelect = class extends import_lit46.LitElement {
13430
14480
  this.open = true;
13431
14481
  this.searchValue = "";
13432
14482
  this.highlightedIndex = -1;
14483
+ this.selectAllHighlighted = false;
13433
14484
  this.addGlobalListeners();
13434
14485
  requestAnimationFrame(() => {
14486
+ this.showNativePopover();
13435
14487
  this.updatePosition();
13436
14488
  if (!this.noSearch) {
13437
14489
  this.searchInputEl?.focus({ preventScroll: true });
@@ -13442,8 +14494,32 @@ var RelSelect = class extends import_lit46.LitElement {
13442
14494
  if (!this.open) return;
13443
14495
  this.open = false;
13444
14496
  this.removeGlobalListeners();
14497
+ this.hideNativePopover();
13445
14498
  this.triggerEl?.focus();
13446
14499
  }
14500
+ /* A transformed ancestor (e.g. rel-modal's centered panel with
14501
+ * translate(-50%,-50%)) becomes the containing block for position:fixed
14502
+ * descendants, breaking viewport-based coordinates and letting the
14503
+ * panel's overflow:hidden clip the listbox. Promoting the listbox to
14504
+ * the browser top layer sidesteps both — same fix as rel-popover /
14505
+ * rel-autocomplete. `manual` keeps light-dismiss and Escape handling
14506
+ * under this component's own control. */
14507
+ showNativePopover() {
14508
+ const p = this.listboxEl;
14509
+ if (!p || typeof p.showPopover !== "function") return;
14510
+ try {
14511
+ if (!p.matches(":popover-open")) p.showPopover();
14512
+ } catch {
14513
+ }
14514
+ }
14515
+ hideNativePopover() {
14516
+ const p = this.listboxEl;
14517
+ if (!p || typeof p.hidePopover !== "function") return;
14518
+ try {
14519
+ if (p.matches(":popover-open")) p.hidePopover();
14520
+ } catch {
14521
+ }
14522
+ }
13447
14523
  selectSingleOption(option) {
13448
14524
  this.selected = option.value;
13449
14525
  if (this.state === "error") this.state = "default";
@@ -13480,6 +14556,11 @@ var RelSelect = class extends import_lit46.LitElement {
13480
14556
  }
13481
14557
  onTriggerKeyDown(e) {
13482
14558
  if (this.state === "disabled") return;
14559
+ if (this.multiple && (e.key === "Backspace" || e.key === "Delete") && this.selectedValues.length > 0) {
14560
+ e.preventDefault();
14561
+ this.removeChip(this.selectedValues[this.selectedValues.length - 1]);
14562
+ return;
14563
+ }
13483
14564
  if (!this.open) {
13484
14565
  if (["ArrowDown", "ArrowUp", "Enter", " "].includes(e.key)) {
13485
14566
  e.preventDefault();
@@ -13490,26 +14571,60 @@ var RelSelect = class extends import_lit46.LitElement {
13490
14571
  if (this.noSearch) this.handleListNavigation(e);
13491
14572
  }
13492
14573
  onSearchKeyDown(e) {
14574
+ if (this.multiple && e.key === "Backspace" && this.searchValue === "" && this.selectedValues.length > 0) {
14575
+ this.removeChip(this.selectedValues[this.selectedValues.length - 1]);
14576
+ return;
14577
+ }
13493
14578
  this.handleListNavigation(e);
13494
14579
  }
13495
14580
  handleListNavigation(e) {
13496
14581
  const filtered = this.filteredOptions;
14582
+ const hasSelectAll = this.multiple && filtered.length > 0;
13497
14583
  switch (e.key) {
13498
14584
  case "ArrowDown":
13499
14585
  e.preventDefault();
13500
- this.highlightedIndex = Math.min(filtered.length - 1, this.highlightedIndex + 1);
14586
+ if (this.selectAllHighlighted) {
14587
+ this.selectAllHighlighted = false;
14588
+ this.highlightedIndex = filtered.length > 0 ? 0 : -1;
14589
+ } else if (this.highlightedIndex === -1 && hasSelectAll) {
14590
+ this.selectAllHighlighted = true;
14591
+ } else {
14592
+ this.highlightedIndex = Math.min(filtered.length - 1, this.highlightedIndex + 1);
14593
+ }
14594
+ this.scrollHighlightedIntoView();
13501
14595
  break;
13502
14596
  case "ArrowUp":
13503
14597
  e.preventDefault();
13504
- this.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
14598
+ if (!this.selectAllHighlighted && this.highlightedIndex <= 0 && hasSelectAll) {
14599
+ this.highlightedIndex = -1;
14600
+ this.selectAllHighlighted = true;
14601
+ } else {
14602
+ this.highlightedIndex = Math.max(0, this.highlightedIndex - 1);
14603
+ }
14604
+ this.scrollHighlightedIntoView();
14605
+ break;
14606
+ case "Home":
14607
+ if (filtered.length === 0) break;
14608
+ e.preventDefault();
14609
+ this.selectAllHighlighted = false;
14610
+ this.highlightedIndex = 0;
14611
+ this.scrollHighlightedIntoView();
14612
+ break;
14613
+ case "End":
14614
+ if (filtered.length === 0) break;
14615
+ e.preventDefault();
14616
+ this.selectAllHighlighted = false;
14617
+ this.highlightedIndex = filtered.length - 1;
14618
+ this.scrollHighlightedIntoView();
14619
+ break;
14620
+ case " ":
14621
+ if (!this.noSearch) return;
14622
+ e.preventDefault();
14623
+ this.activateHighlighted();
13505
14624
  break;
13506
14625
  case "Enter":
13507
14626
  e.preventDefault();
13508
- if (this.highlightedIndex >= 0 && filtered[this.highlightedIndex]) {
13509
- const opt = filtered[this.highlightedIndex];
13510
- if (this.multiple) this.toggleMultiOption(opt);
13511
- else this.selectSingleOption(opt);
13512
- }
14627
+ this.activateHighlighted();
13513
14628
  break;
13514
14629
  case "Escape":
13515
14630
  e.preventDefault();
@@ -13518,8 +14633,55 @@ var RelSelect = class extends import_lit46.LitElement {
13518
14633
  case "Tab":
13519
14634
  this.closeListbox();
13520
14635
  break;
14636
+ default:
14637
+ this.handleTypeahead(e);
14638
+ break;
14639
+ }
14640
+ }
14641
+ /** APG typeahead for the no-search mode (with a search field, typing
14642
+ * filters instead). Accumulates printable keys for 500 ms and jumps the
14643
+ * highlight to the next option whose label starts with the buffer. */
14644
+ handleTypeahead(e) {
14645
+ if (!this.noSearch) return;
14646
+ if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return;
14647
+ e.preventDefault();
14648
+ const isNewQuery = this.typeaheadBuffer === "";
14649
+ this.typeaheadBuffer += e.key.toLowerCase();
14650
+ clearTimeout(this.typeaheadTimer);
14651
+ this.typeaheadTimer = window.setTimeout(() => {
14652
+ this.typeaheadBuffer = "";
14653
+ }, 500);
14654
+ const filtered = this.filteredOptions;
14655
+ if (filtered.length === 0) return;
14656
+ const start = Math.max(0, this.highlightedIndex) + (isNewQuery ? 1 : 0);
14657
+ for (let offset = 0; offset < filtered.length; offset++) {
14658
+ const idx = (start + offset) % filtered.length;
14659
+ if (filtered[idx].label.toLowerCase().startsWith(this.typeaheadBuffer)) {
14660
+ this.selectAllHighlighted = false;
14661
+ this.highlightedIndex = idx;
14662
+ this.scrollHighlightedIntoView();
14663
+ return;
14664
+ }
14665
+ }
14666
+ }
14667
+ activateHighlighted() {
14668
+ if (this.selectAllHighlighted) {
14669
+ this.toggleSelectAll();
14670
+ return;
14671
+ }
14672
+ const filtered = this.filteredOptions;
14673
+ if (this.highlightedIndex >= 0 && filtered[this.highlightedIndex]) {
14674
+ const opt = filtered[this.highlightedIndex];
14675
+ if (this.multiple) this.toggleMultiOption(opt);
14676
+ else this.selectSingleOption(opt);
13521
14677
  }
13522
14678
  }
14679
+ scrollHighlightedIntoView() {
14680
+ if (this.highlightedIndex < 0) return;
14681
+ requestAnimationFrame(() => {
14682
+ this.renderRoot.querySelector(`#${this.uniqueId}-option-${this.highlightedIndex}`)?.scrollIntoView({ block: "nearest" });
14683
+ });
14684
+ }
13523
14685
  renderChips() {
13524
14686
  const values = this.selectedValues;
13525
14687
  const visible = values.slice(0, Math.min(this.visibleChipCount, this.maxVisibleChips));
@@ -13597,7 +14759,7 @@ var RelSelect = class extends import_lit46.LitElement {
13597
14759
  this.open ? "open" : ""
13598
14760
  ].filter(Boolean).join(" ");
13599
14761
  const listboxId = `${this.uniqueId}-listbox`;
13600
- const activeOptionId = this.open && this.highlightedIndex >= 0 ? `${this.uniqueId}-option-${this.highlightedIndex}` : "";
14762
+ const activeOptionId = !this.open ? "" : this.selectAllHighlighted ? `${this.uniqueId}-select-all` : this.highlightedIndex >= 0 ? `${this.uniqueId}-option-${this.highlightedIndex}` : "";
13601
14763
  const selectedSingle = opts.find((o) => o.value === this.selected);
13602
14764
  return import_lit46.html`
13603
14765
  <div class="${wrapperClasses}">
@@ -13654,39 +14816,52 @@ var RelSelect = class extends import_lit46.LitElement {
13654
14816
  <div
13655
14817
  id="${listboxId}"
13656
14818
  part="panel list" class="select-listbox"
14819
+ popover="manual"
13657
14820
  role="listbox"
13658
14821
  aria-multiselectable="${this.multiple}"
13659
14822
  aria-label="${accessibleName || "Options"}"
13660
14823
  style="${this.panelStyle}"
13661
14824
  ?data-open="${this.open}"
13662
14825
  >
13663
- ${!this.noSearch ? import_lit46.html`<div class="select-search">
13664
- <div class="search-input-wrap">
13665
- <rel-icon name="search" size="${this.searchIconSize}" class="icon"></rel-icon>
13666
- <input
13667
- part="input" class="search-input"
13668
- type="text"
13669
- placeholder="${this.searchPlaceholder}"
13670
- .value="${this.searchValue}"
13671
- @input="${this.onSearchInput}"
13672
- @keydown="${(e) => this.onSearchKeyDown(e)}"
13673
- />
13674
- </div>
13675
- </div>` : ""}
13676
-
13677
- ${filtered.length === 0 ? import_lit46.html`<div class="select-empty">
13678
- ${opts.length === 0 ? "No options available" : "No matches found"}
13679
- </div>` : this.multiple ? import_lit46.html`
13680
- <div class="select-all-row">
13681
- <rel-checkbox
13682
- label="${this.selectAllLabel}"
13683
- status="${this.selectAllStatus}"
13684
- @rel-change=${(e) => {
14826
+ ${!this.noSearch || this.multiple && filtered.length > 0 ? import_lit46.html`<div class="select-header">
14827
+ ${!this.noSearch ? import_lit46.html`<div class="select-search">
14828
+ <div class="search-input-wrap">
14829
+ <rel-icon name="search" size="${this.searchIconSize}" class="icon"></rel-icon>
14830
+ <input
14831
+ part="input" class="search-input"
14832
+ type="text"
14833
+ placeholder="${this.searchPlaceholder}"
14834
+ .value="${this.searchValue}"
14835
+ @input="${this.onSearchInput}"
14836
+ @keydown="${(e) => this.onSearchKeyDown(e)}"
14837
+ />
14838
+ </div>
14839
+ </div>` : ""}
14840
+ ${this.multiple && filtered.length > 0 ? import_lit46.html`<div
14841
+ id="${this.uniqueId}-select-all"
14842
+ class="select-all-row ${this.selectAllHighlighted ? "highlighted" : ""}"
14843
+ role="option"
14844
+ aria-selected="${this.selectAllStatus === "checked"}"
14845
+ @pointerenter="${() => {
14846
+ this.selectAllHighlighted = true;
14847
+ this.highlightedIndex = -1;
14848
+ }}"
14849
+ @pointerleave="${() => this.selectAllHighlighted = false}"
14850
+ >
14851
+ <rel-checkbox
14852
+ label="${this.selectAllLabel}"
14853
+ status="${this.selectAllStatus}"
14854
+ @rel-change=${(e) => {
13685
14855
  e.stopPropagation();
13686
14856
  this.toggleSelectAll();
13687
14857
  }}
13688
- ></rel-checkbox>
13689
- </div>
14858
+ ></rel-checkbox>
14859
+ </div>` : ""}
14860
+ </div>` : ""}
14861
+
14862
+ ${filtered.length === 0 ? import_lit46.html`<div class="select-empty">
14863
+ ${opts.length === 0 ? "No options available" : "No matches found"}
14864
+ </div>` : this.multiple ? import_lit46.html`
13690
14865
  <div class="select-options">
13691
14866
  ${(0, import_repeat2.repeat)(
13692
14867
  filtered,
@@ -13697,7 +14872,10 @@ var RelSelect = class extends import_lit46.LitElement {
13697
14872
  class="checkbox-option ${index === this.highlightedIndex ? "highlighted" : ""}"
13698
14873
  role="option"
13699
14874
  aria-selected="${this.selectedValues.includes(option.value)}"
13700
- @pointerenter="${() => this.highlightedIndex = index}"
14875
+ @pointerenter="${() => {
14876
+ this.highlightedIndex = index;
14877
+ this.selectAllHighlighted = false;
14878
+ }}"
13701
14879
  >
13702
14880
  <rel-checkbox
13703
14881
  label="${option.label}"
@@ -14029,6 +15207,15 @@ RelSelect.styles = import_lit46.css`
14029
15207
  position: fixed;
14030
15208
  z-index: 1001;
14031
15209
  display: none;
15210
+ /* Reset native [popover] UA styles (the listbox is promoted to the
15211
+ top layer so transformed ancestors — e.g. a centered rel-modal
15212
+ panel — can't hijack its containing block or clip it). */
15213
+ inset: auto;
15214
+ margin: 0;
15215
+ padding: 0;
15216
+ width: auto;
15217
+ height: auto;
15218
+ color: inherit;
14032
15219
  box-sizing: border-box;
14033
15220
  background: var(--rel-select-bg, var(--rel-bg-neutral));
14034
15221
  border: 1px solid var(--rel-select-menu-border, var(--rel-border-default));
@@ -14045,10 +15232,18 @@ RelSelect.styles = import_lit46.css`
14045
15232
  display: block;
14046
15233
  }
14047
15234
 
14048
- .select-search {
15235
+ /* Search and Select All pin together while the options scroll beneath.
15236
+ z-index is load-bearing: multiselect rows contain rel-checkbox, whose
15237
+ .row wrapper is position:relative (Tab-scroll fix), so without it the
15238
+ positioned rows paint over the sticky header in DOM order. */
15239
+ .select-header {
14049
15240
  position: sticky;
14050
15241
  top: 0;
15242
+ z-index: 1;
14051
15243
  background: var(--rel-select-bg, var(--rel-bg-neutral));
15244
+ }
15245
+
15246
+ .select-search {
14052
15247
  padding: var(--rel-spacing-xs);
14053
15248
  border-bottom: 1px solid var(--rel-select-divider, var(--rel-border-subtle));
14054
15249
  }
@@ -14179,6 +15374,10 @@ RelSelect.styles = import_lit46.css`
14179
15374
  padding: 12px;
14180
15375
  }
14181
15376
 
15377
+ .select-all-row.highlighted {
15378
+ background: var(--rel-select-option-hover, var(--rel-bg-neutral-elevated-1));
15379
+ }
15380
+
14182
15381
  .select-empty {
14183
15382
  padding: var(--rel-spacing-xs) 12px;
14184
15383
  color: var(--rel-select-placeholder-color, var(--rel-text-body));
@@ -14253,6 +15452,9 @@ __decorateClass([
14253
15452
  __decorateClass([
14254
15453
  (0, import_decorators46.state)()
14255
15454
  ], RelSelect.prototype, "highlightedIndex", 2);
15455
+ __decorateClass([
15456
+ (0, import_decorators46.state)()
15457
+ ], RelSelect.prototype, "selectAllHighlighted", 2);
14256
15458
  __decorateClass([
14257
15459
  (0, import_decorators46.state)()
14258
15460
  ], RelSelect.prototype, "panelStyle", 2);
@@ -14450,6 +15652,10 @@ var RelModal = class extends import_lit48.LitElement {
14450
15652
  if (e.key === "Escape" && this.dismissible) {
14451
15653
  e.preventDefault();
14452
15654
  this.open = false;
15655
+ return;
15656
+ }
15657
+ if (e.key === "Tab" && this.modal && this.panelElement) {
15658
+ trapTabKey(e, this.panelElement);
14453
15659
  }
14454
15660
  };
14455
15661
  this.handleOverlayClick = (e) => {
@@ -14500,9 +15706,18 @@ var RelModal = class extends import_lit48.LitElement {
14500
15706
  }
14501
15707
  }
14502
15708
  handleOpen() {
14503
- this.previousFocus = document.activeElement;
15709
+ this.previousFocus = getDeepActiveElement();
14504
15710
  this.lockScroll();
14505
15711
  document.addEventListener("keydown", this.handleKeydown);
15712
+ if (this.modal) {
15713
+ this.updateComplete.then(() => {
15714
+ const panel = this.panelElement;
15715
+ if (!panel) return;
15716
+ const focusTarget = getFocusableElements(panel)[0];
15717
+ if (focusTarget) focusTarget.focus();
15718
+ else panel.focus();
15719
+ });
15720
+ }
14506
15721
  this.dispatchEvent(
14507
15722
  new CustomEvent("rel-open-change", {
14508
15723
  detail: true,
@@ -14862,6 +16077,9 @@ __decorateClass([
14862
16077
  __decorateClass([
14863
16078
  (0, import_decorators48.query)(".overlay")
14864
16079
  ], RelModal.prototype, "overlayElement", 2);
16080
+ __decorateClass([
16081
+ (0, import_decorators48.query)(".panel")
16082
+ ], RelModal.prototype, "panelElement", 2);
14865
16083
  RelModal = __decorateClass([
14866
16084
  (0, import_decorators48.customElement)("rel-modal")
14867
16085
  ], RelModal);
@@ -15877,6 +17095,28 @@ var RelStepper = class extends import_lit51.LitElement {
15877
17095
  if (this.isDisabled(step)) classes.push("disabled");
15878
17096
  return classes.join(" ");
15879
17097
  }
17098
+ onStepKeyDown(e, step, index) {
17099
+ if (e.key === "Enter" || e.key === " ") {
17100
+ e.preventDefault();
17101
+ this.onStepClick(step, index);
17102
+ return;
17103
+ }
17104
+ const prevKey = e.key === "ArrowLeft" || e.key === "ArrowUp";
17105
+ const nextKey = e.key === "ArrowRight" || e.key === "ArrowDown";
17106
+ if (!prevKey && !nextKey && e.key !== "Home" && e.key !== "End") return;
17107
+ const steps = Array.from(
17108
+ this.renderRoot.querySelectorAll(".rel-step:not(.disabled)")
17109
+ );
17110
+ if (steps.length === 0) return;
17111
+ e.preventDefault();
17112
+ const current = steps.indexOf(e.currentTarget);
17113
+ let target;
17114
+ if (e.key === "Home") target = 0;
17115
+ else if (e.key === "End") target = steps.length - 1;
17116
+ else if (prevKey) target = (current - 1 + steps.length) % steps.length;
17117
+ else target = (current + 1) % steps.length;
17118
+ steps[target]?.focus();
17119
+ }
15880
17120
  onStepClick(step, index) {
15881
17121
  if (step.status === "disabled") return;
15882
17122
  this.dispatchEvent(
@@ -15929,7 +17169,12 @@ var RelStepper = class extends import_lit51.LitElement {
15929
17169
  (step, idx) => import_lit51.html`
15930
17170
  <div
15931
17171
  class="${this.getStepClasses(step)}"
17172
+ role="button"
17173
+ tabindex="${this.isDisabled(step) ? -1 : 0}"
17174
+ aria-disabled="${this.isDisabled(step) ? "true" : import_lit51.nothing}"
17175
+ aria-current="${this.isProgress(step) ? "step" : import_lit51.nothing}"
15932
17176
  @click="${() => this.onStepClick(step, idx)}"
17177
+ @keydown="${(e) => this.onStepKeyDown(e, step, idx)}"
15933
17178
  >
15934
17179
  ${isVertical && isTextRight ? import_lit51.html`
15935
17180
  <!-- Indicator with connector below -->
@@ -16016,6 +17261,17 @@ RelStepper.styles = import_lit51.css`
16016
17261
  .rel-step {
16017
17262
  display: flex;
16018
17263
  position: relative;
17264
+ cursor: pointer;
17265
+ }
17266
+
17267
+ .rel-step.disabled {
17268
+ cursor: not-allowed;
17269
+ }
17270
+
17271
+ .rel-step:focus-visible {
17272
+ outline: 2px solid var(--rel-border-focus);
17273
+ outline-offset: 2px;
17274
+ border-radius: var(--rel-border-radius-xs);
16019
17275
  }
16020
17276
 
16021
17277
  .rel-stepper.horizontal.text-below .rel-step {
@@ -16592,6 +17848,8 @@ var RelTab = class extends import_lit53.LitElement {
16592
17848
  this.activeIndex = 0;
16593
17849
  this.size = "md";
16594
17850
  this._tabItems = [];
17851
+ /** Stable id prefix linking tabs to the shared tabpanel. */
17852
+ this.tabsId = `rel-tab-${Math.random().toString(36).slice(2)}`;
16595
17853
  }
16596
17854
  _onSlotChange(e) {
16597
17855
  const slot = e.target;
@@ -16635,13 +17893,25 @@ var RelTab = class extends import_lit53.LitElement {
16635
17893
  next = (next + dir + tabs.length) % tabs.length;
16636
17894
  if (!tabs[next]?.disabled) break;
16637
17895
  }
16638
- this.selectTab(next);
16639
- this.updateComplete.then(() => {
16640
- const tabEls = this.shadowRoot?.querySelectorAll("[role=tab]");
16641
- tabEls?.[next]?.focus();
16642
- });
17896
+ this.selectAndFocusTab(next);
17897
+ return;
17898
+ }
17899
+ if (event.key === "Home" || event.key === "End") {
17900
+ event.preventDefault();
17901
+ const tabs = this._effectiveTabs;
17902
+ const order = [...tabs.keys()];
17903
+ if (event.key === "End") order.reverse();
17904
+ const next = order.find((i) => !tabs[i]?.disabled);
17905
+ if (next !== void 0) this.selectAndFocusTab(next);
16643
17906
  }
16644
17907
  }
17908
+ selectAndFocusTab(index) {
17909
+ this.selectTab(index);
17910
+ this.updateComplete.then(() => {
17911
+ const tabEls = this.shadowRoot?.querySelectorAll("[role=tab]");
17912
+ tabEls?.[index]?.focus();
17913
+ });
17914
+ }
16645
17915
  render() {
16646
17916
  const tabs = this._effectiveTabs;
16647
17917
  return import_lit53.html`
@@ -16651,9 +17921,11 @@ var RelTab = class extends import_lit53.LitElement {
16651
17921
  (tab, i) => import_lit53.html`
16652
17922
  <div
16653
17923
  part="item" class="tab ${i === this.activeIndex ? "active" : ""} ${tab.disabled ? "disabled" : ""}"
17924
+ id="${this.tabsId}-tab-${i}"
16654
17925
  role="tab"
16655
17926
  aria-selected="${i === this.activeIndex}"
16656
17927
  aria-disabled="${tab.disabled ?? false}"
17928
+ aria-controls="${this.tabsId}-panel"
16657
17929
  tabindex="${tab.disabled ? -1 : i === this.activeIndex ? 0 : -1}"
16658
17930
  @click="${() => this.selectTab(i)}"
16659
17931
  @keydown="${(e) => this.onKeyDown(e, i)}"
@@ -16665,7 +17937,13 @@ var RelTab = class extends import_lit53.LitElement {
16665
17937
  `
16666
17938
  )}
16667
17939
  </div>
16668
- <div class="tab-content">
17940
+ <div
17941
+ class="tab-content"
17942
+ id="${this.tabsId}-panel"
17943
+ role="tabpanel"
17944
+ aria-labelledby="${this.tabsId}-tab-${this.activeIndex}"
17945
+ tabindex="0"
17946
+ >
16669
17947
  <slot></slot>
16670
17948
  </div>
16671
17949
  `;
@@ -16696,7 +17974,6 @@ RelTab.styles = import_lit53.css`
16696
17974
  padding: var(--rel-spacing-sm) var(--rel-spacing-md);
16697
17975
  cursor: pointer;
16698
17976
  color: var(--rel-text-body);
16699
-
16700
17977
  transition: color 0.2s ease;
16701
17978
  display: flex;
16702
17979
  align-items: center;
@@ -16814,7 +18091,8 @@ var RelTimePicker = class extends import_lit54.LitElement {
16814
18091
  this.required = false;
16815
18092
  this.readonly = false;
16816
18093
  this.format = "12h";
16817
- this.minuteStep = 1;
18094
+ this.seconds = false;
18095
+ this.minuteStep = 5;
16818
18096
  this.minTime = null;
16819
18097
  this.maxTime = null;
16820
18098
  this.open = false;
@@ -16826,6 +18104,8 @@ var RelTimePicker = class extends import_lit54.LitElement {
16826
18104
  this.fullWidth = false;
16827
18105
  this.draftHour = null;
16828
18106
  this.draftMinute = null;
18107
+ this.draftSecond = null;
18108
+ this.openUp = false;
16829
18109
  this.handleClickOutside = (e) => {
16830
18110
  if (!this.open) return;
16831
18111
  const path = e.composedPath();
@@ -16850,40 +18130,58 @@ var RelTimePicker = class extends import_lit54.LitElement {
16850
18130
  document.removeEventListener("keydown", this.handleEscapeKey);
16851
18131
  }
16852
18132
  updated(changedProperties) {
16853
- if (changedProperties.has("open") && this.open) {
18133
+ if (changedProperties.has("open")) {
18134
+ if (!this.open) {
18135
+ this.openUp = false;
18136
+ return;
18137
+ }
16854
18138
  this.resetDraftFromValue();
16855
18139
  setTimeout(() => {
18140
+ this.updatePanelPlacement();
16856
18141
  this.scrollColumnsToSelection();
16857
18142
  const target = this.columnsElement?.querySelector(
16858
18143
  ".option-button.selected:not(:disabled)"
16859
18144
  ) ?? this.columnsElement?.querySelector(
16860
18145
  ".option-button:not(:disabled)"
16861
18146
  );
16862
- target?.focus();
18147
+ target?.focus({ preventScroll: true });
16863
18148
  }, 0);
16864
18149
  }
16865
18150
  }
18151
+ /** Open upward when the panel doesn't fit below but does fit above. */
18152
+ updatePanelPlacement() {
18153
+ const panel = this.renderRoot.querySelector(".popover");
18154
+ if (!panel) return;
18155
+ const rect = this.getBoundingClientRect();
18156
+ const panelHeight = panel.offsetHeight;
18157
+ const spaceBelow = window.innerHeight - rect.bottom;
18158
+ this.openUp = spaceBelow < panelHeight + 8 && rect.top > panelHeight + 8;
18159
+ }
16866
18160
  // ── Parsing / formatting ──
16867
18161
  parseTime(time) {
16868
18162
  if (!time) return null;
16869
- const match = time.match(/^(\d{1,2}):(\d{2})$/);
18163
+ const match = time.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/);
16870
18164
  if (!match) return null;
16871
18165
  const hour = parseInt(match[1], 10);
16872
18166
  const minute = parseInt(match[2], 10);
16873
- if (hour > 23 || minute > 59) return null;
16874
- return { hour, minute };
18167
+ const second = match[3] ? parseInt(match[3], 10) : 0;
18168
+ if (hour > 23 || minute > 59 || second > 59) return null;
18169
+ return { hour, minute, second };
16875
18170
  }
16876
- formatValue(hour, minute) {
16877
- return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
18171
+ formatValue(hour, minute, second = 0) {
18172
+ const hh = String(hour).padStart(2, "0");
18173
+ const mm = String(minute).padStart(2, "0");
18174
+ return this.seconds ? `${hh}:${mm}:${String(second).padStart(2, "0")}` : `${hh}:${mm}`;
16878
18175
  }
16879
- formatDisplay(hour, minute) {
18176
+ formatDisplay(hour, minute, second = 0) {
16880
18177
  const mm = String(minute).padStart(2, "0");
18178
+ const ss = this.seconds ? `:${String(second).padStart(2, "0")}` : "";
16881
18179
  if (this.format === "24h") {
16882
- return `${String(hour).padStart(2, "0")}:${mm}`;
18180
+ return `${String(hour).padStart(2, "0")}:${mm}${ss}`;
16883
18181
  }
16884
18182
  const period = hour < 12 ? "AM" : "PM";
16885
18183
  const hour12 = hour % 12 === 0 ? 12 : hour % 12;
16886
- return `${hour12}:${mm} ${period}`;
18184
+ return `${hour12}:${mm}${ss} ${period}`;
16887
18185
  }
16888
18186
  toMinutes(hour, minute) {
16889
18187
  return hour * 60 + minute;
@@ -16933,10 +18231,17 @@ var RelTimePicker = class extends import_lit54.LitElement {
16933
18231
  }
16934
18232
  const period = this.draftPeriod ?? "AM";
16935
18233
  const base = period === "AM" ? 0 : 12;
16936
- return Array.from({ length: 12 }, (_, i) => ({
16937
- label: i === 0 ? "12" : String(i).padStart(2, "0"),
16938
- hour24: base + i
16939
- }));
18234
+ return Array.from({ length: 12 }, (_, i) => {
18235
+ const display = i + 1;
18236
+ return {
18237
+ label: String(display).padStart(2, "0"),
18238
+ hour24: base + display % 12
18239
+ };
18240
+ });
18241
+ }
18242
+ /** Seconds shown in the seconds column (always step 1). */
18243
+ getSecondOptions() {
18244
+ return Array.from({ length: 60 }, (_, s) => s);
16940
18245
  }
16941
18246
  get draftPeriod() {
16942
18247
  if (this.draftHour === null) return null;
@@ -16946,6 +18251,7 @@ var RelTimePicker = class extends import_lit54.LitElement {
16946
18251
  const parsed = this.parseTime(this.value);
16947
18252
  this.draftHour = parsed ? parsed.hour : null;
16948
18253
  this.draftMinute = parsed ? parsed.minute : null;
18254
+ this.draftSecond = parsed ? parsed.second : null;
16949
18255
  }
16950
18256
  /** Snap the draft minute to the nearest valid step minute for the hour. */
16951
18257
  clampDraftToRange() {
@@ -16988,7 +18294,11 @@ var RelTimePicker = class extends import_lit54.LitElement {
16988
18294
  }
16989
18295
  applySelection() {
16990
18296
  if (this.draftHour === null) return;
16991
- const newValue = this.formatValue(this.draftHour, this.draftMinute ?? 0);
18297
+ const newValue = this.formatValue(
18298
+ this.draftHour,
18299
+ this.draftMinute ?? 0,
18300
+ this.draftSecond ?? 0
18301
+ );
16992
18302
  this.value = newValue;
16993
18303
  this.dispatchEvent(
16994
18304
  new CustomEvent("rel-change", {
@@ -17004,6 +18314,7 @@ var RelTimePicker = class extends import_lit54.LitElement {
17004
18314
  const now = /* @__PURE__ */ new Date();
17005
18315
  const step = this.minuteStepSafe;
17006
18316
  this.draftHour = now.getHours();
18317
+ this.draftSecond = this.seconds ? now.getSeconds() : null;
17007
18318
  this.draftMinute = Math.round(now.getMinutes() / step) * step;
17008
18319
  if (this.draftMinute >= 60) {
17009
18320
  this.draftMinute = 0;
@@ -17026,6 +18337,7 @@ var RelTimePicker = class extends import_lit54.LitElement {
17026
18337
  this.value = null;
17027
18338
  this.draftHour = null;
17028
18339
  this.draftMinute = null;
18340
+ this.draftSecond = null;
17029
18341
  this.dispatchEvent(
17030
18342
  new CustomEvent("rel-change", {
17031
18343
  detail: { value: null },
@@ -17054,6 +18366,20 @@ var RelTimePicker = class extends import_lit54.LitElement {
17054
18366
  if (!this.isTimeInRange(this.draftHour, minute)) return;
17055
18367
  this.draftMinute = minute;
17056
18368
  }
18369
+ selectSecond(second) {
18370
+ if (this.draftHour === null) {
18371
+ for (let h = 0; h < 24; h++) {
18372
+ if (this.isHourEnabled(h)) {
18373
+ this.draftHour = h;
18374
+ break;
18375
+ }
18376
+ }
18377
+ if (this.draftHour === null) return;
18378
+ }
18379
+ if (this.draftMinute === null) this.draftMinute = 0;
18380
+ this.draftSecond = second;
18381
+ this.clampDraftToRange();
18382
+ }
17057
18383
  selectPeriod(period) {
17058
18384
  if (!this.isPeriodEnabled(period)) return;
17059
18385
  const base = period === "AM" ? 0 : 12;
@@ -17071,9 +18397,27 @@ var RelTimePicker = class extends import_lit54.LitElement {
17071
18397
  this.clampDraftToRange();
17072
18398
  this.scrollColumnsToSelection();
17073
18399
  }
18400
+ /**
18401
+ * Scrolls the column's own scroll container only. scrollIntoView is off
18402
+ * limits here: it walks ALL scrollable ancestors, so it yanked the page
18403
+ * up/down whenever a column recentred (user-reported jump bug).
18404
+ */
18405
+ scrollOptionInColumn(btn, block) {
18406
+ const list = btn.parentElement;
18407
+ if (!list) return;
18408
+ const top = btn.offsetTop;
18409
+ const height = btn.offsetHeight;
18410
+ if (block === "center") {
18411
+ list.scrollTop = top - (list.clientHeight - height) / 2;
18412
+ } else if (top < list.scrollTop) {
18413
+ list.scrollTop = top;
18414
+ } else if (top + height > list.scrollTop + list.clientHeight) {
18415
+ list.scrollTop = top + height - list.clientHeight;
18416
+ }
18417
+ }
17074
18418
  scrollColumnsToSelection() {
17075
18419
  this.updateComplete.then(() => {
17076
- this.columnsElement?.querySelectorAll(".option-button.selected").forEach((btn) => btn.scrollIntoView({ block: "center" }));
18420
+ this.columnsElement?.querySelectorAll(".option-button.selected").forEach((btn) => this.scrollOptionInColumn(btn, "center"));
17077
18421
  });
17078
18422
  }
17079
18423
  // ── Keyboard ──
@@ -17130,19 +18474,23 @@ var RelTimePicker = class extends import_lit54.LitElement {
17130
18474
  }
17131
18475
  if (target && target !== buttons[index]) {
17132
18476
  target.click();
17133
- target.focus();
17134
- target.scrollIntoView({ block: "nearest" });
18477
+ target.focus({ preventScroll: true });
18478
+ this.scrollOptionInColumn(target, "nearest");
17135
18479
  }
17136
18480
  }
17137
18481
  focusAdjacentColumn(from, direction) {
17138
- const order = this.format === "12h" ? ["hour", "minute", "period"] : ["hour", "minute"];
18482
+ const order = ["hour", "minute"];
18483
+ if (this.seconds) order.push("second");
18484
+ if (this.format === "12h") order.push("period");
17139
18485
  const nextIndex = order.indexOf(from) + direction;
17140
18486
  if (nextIndex < 0 || nextIndex >= order.length) return;
17141
18487
  const lists = this.columnsElement?.querySelectorAll(".column-list");
17142
18488
  const list = lists?.[nextIndex];
17143
18489
  const target = list?.querySelector(".option-button.selected:not(:disabled)") ?? list?.querySelector(".option-button:not(:disabled)");
17144
- target?.focus();
17145
- target?.scrollIntoView({ block: "nearest" });
18490
+ if (target) {
18491
+ target.focus({ preventScroll: true });
18492
+ this.scrollOptionInColumn(target, "nearest");
18493
+ }
17146
18494
  }
17147
18495
  // ── Render ──
17148
18496
  render() {
@@ -17150,10 +18498,10 @@ var RelTimePicker = class extends import_lit54.LitElement {
17150
18498
  const belowText = isError && this.errorMessage ? this.errorMessage : this.helperText;
17151
18499
  const describedById = belowText ? "time-picker-desc" : void 0;
17152
18500
  const parsedValue = this.parseTime(this.value);
17153
- const displayText = parsedValue ? this.formatDisplay(parsedValue.hour, parsedValue.minute) : "";
17154
- const draftDisplay = this.draftHour !== null ? this.formatDisplay(this.draftHour, this.draftMinute ?? 0) : "";
18501
+ const displayText = parsedValue ? this.formatDisplay(parsedValue.hour, parsedValue.minute, parsedValue.second) : "";
17155
18502
  const hourOptions = this.getHourOptions();
17156
18503
  const minuteOptions = this.getMinuteOptions();
18504
+ const secondOptions = this.seconds ? this.getSecondOptions() : [];
17157
18505
  const draftPeriod = this.draftPeriod;
17158
18506
  const wrapperClasses = [
17159
18507
  "time-picker-wrapper",
@@ -17218,104 +18566,106 @@ var RelTimePicker = class extends import_lit54.LitElement {
17218
18566
 
17219
18567
  <div
17220
18568
  part="panel"
17221
- class="popover ${this.open ? "open" : ""}"
18569
+ class="popover ${this.open ? "open" : ""} ${this.openUp ? "open-up" : ""}"
17222
18570
  role="dialog"
17223
18571
  aria-label="Choose time"
17224
18572
  >
17225
- <div class="time-readout">
17226
- <span class="time-readout-label">Time</span>
17227
- <span class="time-readout-value ${draftDisplay ? "" : "empty"}">
17228
- ${draftDisplay || (this.format === "24h" ? "--:--" : "--:-- --")}
17229
- </span>
17230
- </div>
17231
-
17232
18573
  <div class="columns">
17233
- <div class="column">
17234
- <div class="column-header" aria-hidden="true">Hour</div>
17235
- <div class="column-list" role="listbox" aria-label="Hours">
17236
- ${hourOptions.map(
18574
+ <div class="column-list" role="listbox" aria-label="Hours">
18575
+ ${hourOptions.map(
17237
18576
  (opt, i) => import_lit54.html`
17238
- <button
17239
- part="option"
17240
- class="option-button ${this.draftHour === opt.hour24 ? "selected" : ""}"
17241
- type="button"
17242
- role="option"
17243
- aria-selected=${this.draftHour === opt.hour24}
17244
- ?disabled=${!this.isHourEnabled(opt.hour24)}
17245
- @click=${() => this.selectHour(opt.hour24)}
17246
- @keydown=${(e) => this.handleOptionKeydown(e, "hour")}
17247
- tabindex=${this.draftHour === opt.hour24 || this.draftHour === null && i === 0 ? 0 : -1}
17248
- >
17249
- ${opt.label}
17250
- </button>
17251
- `
18577
+ <button
18578
+ part="option"
18579
+ class="option-button ${this.draftHour === opt.hour24 ? "selected" : ""}"
18580
+ type="button"
18581
+ role="option"
18582
+ aria-selected=${this.draftHour === opt.hour24}
18583
+ ?disabled=${!this.isHourEnabled(opt.hour24)}
18584
+ @click=${() => this.selectHour(opt.hour24)}
18585
+ @keydown=${(e) => this.handleOptionKeydown(e, "hour")}
18586
+ tabindex=${this.draftHour === opt.hour24 || this.draftHour === null && i === 0 ? 0 : -1}
18587
+ >
18588
+ ${opt.label}
18589
+ </button>
18590
+ `
17252
18591
  )}
17253
- </div>
17254
18592
  </div>
17255
18593
 
17256
- <div class="column">
17257
- <div class="column-header" aria-hidden="true">Min</div>
17258
- <div class="column-list" role="listbox" aria-label="Minutes">
17259
- ${minuteOptions.map(
18594
+ <div class="column-divider" aria-hidden="true"></div>
18595
+
18596
+ <div class="column-list" role="listbox" aria-label="Minutes">
18597
+ ${minuteOptions.map(
17260
18598
  (m, i) => import_lit54.html`
17261
- <button
17262
- part="option"
17263
- class="option-button ${this.draftMinute === m ? "selected" : ""}"
17264
- type="button"
17265
- role="option"
17266
- aria-selected=${this.draftMinute === m}
17267
- ?disabled=${this.draftHour !== null && !this.isTimeInRange(this.draftHour, m)}
17268
- @click=${() => this.selectMinute(m)}
17269
- @keydown=${(e) => this.handleOptionKeydown(e, "minute")}
17270
- tabindex=${this.draftMinute === m || this.draftMinute === null && i === 0 ? 0 : -1}
17271
- >
17272
- ${String(m).padStart(2, "0")}
17273
- </button>
17274
- `
18599
+ <button
18600
+ part="option"
18601
+ class="option-button ${this.draftMinute === m ? "selected" : ""}"
18602
+ type="button"
18603
+ role="option"
18604
+ aria-selected=${this.draftMinute === m}
18605
+ ?disabled=${this.draftHour !== null && !this.isTimeInRange(this.draftHour, m)}
18606
+ @click=${() => this.selectMinute(m)}
18607
+ @keydown=${(e) => this.handleOptionKeydown(e, "minute")}
18608
+ tabindex=${this.draftMinute === m || this.draftMinute === null && i === 0 ? 0 : -1}
18609
+ >
18610
+ ${String(m).padStart(2, "0")}
18611
+ </button>
18612
+ `
17275
18613
  )}
17276
- </div>
17277
18614
  </div>
17278
18615
 
18616
+ ${this.seconds ? import_lit54.html`
18617
+ <div class="column-divider" aria-hidden="true"></div>
18618
+ <div class="column-list" role="listbox" aria-label="Seconds">
18619
+ ${secondOptions.map(
18620
+ (s, i) => import_lit54.html`
18621
+ <button
18622
+ part="option"
18623
+ class="option-button ${this.draftSecond === s ? "selected" : ""}"
18624
+ type="button"
18625
+ role="option"
18626
+ aria-selected=${this.draftSecond === s}
18627
+ @click=${() => this.selectSecond(s)}
18628
+ @keydown=${(e) => this.handleOptionKeydown(e, "second")}
18629
+ tabindex=${this.draftSecond === s || this.draftSecond === null && i === 0 ? 0 : -1}
18630
+ >
18631
+ ${String(s).padStart(2, "0")}
18632
+ </button>
18633
+ `
18634
+ )}
18635
+ </div>
18636
+ ` : import_lit54.nothing}
17279
18637
  ${this.format === "12h" ? import_lit54.html`
17280
- <div class="column">
17281
- <div class="column-header" aria-hidden="true">AM/PM</div>
17282
- <div class="column-list" role="listbox" aria-label="AM or PM">
17283
- ${["AM", "PM"].map(
18638
+ <div class="column-divider" aria-hidden="true"></div>
18639
+ <div class="column-list" role="listbox" aria-label="AM or PM">
18640
+ ${["AM", "PM"].map(
17284
18641
  (p) => import_lit54.html`
17285
- <button
17286
- part="option"
17287
- class="option-button ${draftPeriod === p ? "selected" : ""}"
17288
- type="button"
17289
- role="option"
17290
- aria-selected=${draftPeriod === p}
17291
- ?disabled=${!this.isPeriodEnabled(p)}
17292
- @click=${() => this.selectPeriod(p)}
17293
- @keydown=${(e) => this.handleOptionKeydown(e, "period")}
17294
- tabindex=${draftPeriod === p || draftPeriod === null && p === "AM" ? 0 : -1}
17295
- >
17296
- ${p}
17297
- </button>
17298
- `
18642
+ <button
18643
+ part="option"
18644
+ class="option-button ${draftPeriod === p ? "selected" : ""}"
18645
+ type="button"
18646
+ role="option"
18647
+ aria-selected=${draftPeriod === p}
18648
+ ?disabled=${!this.isPeriodEnabled(p)}
18649
+ @click=${() => this.selectPeriod(p)}
18650
+ @keydown=${(e) => this.handleOptionKeydown(e, "period")}
18651
+ tabindex=${draftPeriod === p || draftPeriod === null && p === "AM" ? 0 : -1}
18652
+ >
18653
+ ${p}
18654
+ </button>
18655
+ `
17299
18656
  )}
17300
- </div>
17301
18657
  </div>
17302
18658
  ` : import_lit54.nothing}
17303
18659
  </div>
17304
18660
 
17305
18661
  <div part="footer" class="time-footer">
17306
- <rel-button
17307
- variant="secondary"
17308
- size="small"
17309
- border="corner-radius"
17310
- @click=${() => this.selectNow()}
17311
- >
18662
+ <rel-button variant="link" size="small" @click=${() => this.selectNow()}>
17312
18663
  Now
17313
18664
  </rel-button>
17314
18665
  <div class="footer-right">
17315
18666
  <rel-button
17316
- variant="secondary"
18667
+ variant="ghost"
17317
18668
  size="small"
17318
- border="corner-radius"
17319
18669
  @click=${() => this.cancelPopover()}
17320
18670
  >
17321
18671
  Cancel
@@ -17324,11 +18674,10 @@ var RelTimePicker = class extends import_lit54.LitElement {
17324
18674
  variant="primary"
17325
18675
  tone="default"
17326
18676
  size="small"
17327
- border="corner-radius"
17328
18677
  ?disabled=${this.draftHour === null}
17329
18678
  @click=${() => this.applySelection()}
17330
18679
  >
17331
- Apply
18680
+ OK
17332
18681
  </rel-button>
17333
18682
  </div>
17334
18683
  </div>
@@ -17512,16 +18861,19 @@ RelTimePicker.styles = import_lit54.css`
17512
18861
  height: 16px;
17513
18862
  }
17514
18863
 
18864
+ /* Panel (Figma "Time Picker"): fixed width per size, full-bleed content. */
17515
18865
  .popover {
17516
18866
  position: absolute;
17517
18867
  top: calc(100% + 4px);
17518
18868
  left: 0;
17519
18869
  z-index: 1000;
17520
18870
  background: var(--rel-bg-surface);
17521
- border: 1px solid var(--rel-border-default);
18871
+ border: 1px solid var(--rel-border-subtle);
17522
18872
  border-radius: var(--rel-border-radius-sm);
17523
18873
  box-shadow: var(--rel-shadows-shadow-300);
17524
- padding: var(--rel-spacing-md);
18874
+ overflow: hidden;
18875
+ min-width: 220px;
18876
+ width: 250px;
17525
18877
  display: none;
17526
18878
  }
17527
18879
 
@@ -17529,94 +18881,79 @@ RelTimePicker.styles = import_lit54.css`
17529
18881
  display: block;
17530
18882
  }
17531
18883
 
17532
- /* Draft readout (mirrors the range-picker FROM/TO block) */
17533
- .time-readout {
17534
- display: flex;
17535
- align-items: baseline;
17536
- gap: var(--rel-spacing-sm);
17537
- padding-bottom: var(--rel-spacing-sm);
17538
- margin-bottom: var(--rel-spacing-sm);
17539
- border-bottom: 1px solid var(--rel-border-subtle);
17540
- }
17541
-
17542
- .time-readout-label {
17543
- color: var(--rel-text-placeholder);
17544
- text-transform: uppercase;
17545
- font-family: var(--rel-body-xs-semibold-font-family);
17546
- font-weight: var(--rel-body-xs-semibold-font-weight);
17547
- font-size: var(--rel-body-xs-semibold-font-size);
17548
- line-height: var(--rel-body-xs-semibold-line-height);
17549
- letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
17550
- }
17551
-
17552
- .time-readout-value {
17553
- color: var(--rel-text-default);
17554
- font-family: var(--rel-body-md-semibold-font-family);
17555
- font-weight: var(--rel-body-md-semibold-font-weight);
17556
- font-size: var(--rel-body-md-semibold-font-size);
17557
- line-height: var(--rel-body-md-semibold-line-height);
17558
- letter-spacing: var(--rel-body-md-semibold-letter-spacing);
17559
- }
17560
-
17561
- .time-readout-value.empty {
17562
- color: var(--rel-text-placeholder);
18884
+ /* Flip above the field when the viewport has no room below. */
18885
+ .popover.open-up {
18886
+ top: auto;
18887
+ bottom: calc(100% + 4px);
17563
18888
  }
17564
18889
 
18890
+ /* Full-bleed column row with 1px dividers (Figma "Columns"). */
17565
18891
  .columns {
17566
18892
  display: flex;
17567
- gap: var(--rel-spacing-xs);
17568
- }
17569
-
17570
- /* Columns share the panel width equally, so the two-column 24h layout
17571
- fills the same footprint the footer dictates instead of bunching left. */
17572
- .column {
17573
- display: flex;
17574
- flex-direction: column;
17575
- flex: 1;
17576
- min-width: 64px;
18893
+ align-items: stretch;
18894
+ height: 260px;
17577
18895
  }
17578
18896
 
17579
- .column-header {
17580
- text-align: center;
17581
- color: var(--rel-text-placeholder);
17582
- padding: var(--rel-spacing-xs);
17583
- text-transform: uppercase;
17584
- font-family: var(--rel-body-xs-semibold-font-family);
17585
- font-weight: var(--rel-body-xs-semibold-font-weight);
17586
- font-size: var(--rel-body-xs-semibold-font-size);
17587
- line-height: var(--rel-body-xs-semibold-line-height);
17588
- letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
18897
+ .column-divider {
18898
+ flex-shrink: 0;
18899
+ width: 1px;
18900
+ background: var(--rel-border-subtle);
17589
18901
  }
17590
18902
 
18903
+ /* Each column scrolls independently. The 4px rail is always reserved
18904
+ (scrollbar-gutter) so columns stay aligned whether or not they
18905
+ overflow; the thumb only appears on overflow. */
17591
18906
  .column-list {
18907
+ flex: 1;
18908
+ min-width: 0;
17592
18909
  display: flex;
17593
18910
  flex-direction: column;
17594
- gap: var(--rel-spacing-3xs);
17595
- max-height: 224px;
18911
+ /* Positioned so option offsetTop is list-relative — the in-column
18912
+ scroll math depends on it (we never scrollIntoView: it scrolls every
18913
+ ancestor, page included). */
18914
+ position: relative;
17596
18915
  overflow-y: auto;
17597
- padding: var(--rel-spacing-3xs);
18916
+ padding: var(--rel-spacing-2xs);
17598
18917
  scrollbar-width: thin;
18918
+ scrollbar-color: var(--rel-border-default) transparent;
18919
+ scrollbar-gutter: stable;
18920
+ }
18921
+
18922
+ .column-list::-webkit-scrollbar {
18923
+ width: 4px;
18924
+ }
18925
+
18926
+ .column-list::-webkit-scrollbar-track {
18927
+ background: transparent;
17599
18928
  }
17600
18929
 
18930
+ .column-list::-webkit-scrollbar-thumb {
18931
+ background: var(--rel-border-default);
18932
+ border-radius: var(--rel-border-radius-full-rounded);
18933
+ }
18934
+
18935
+ /* Option cell (Figma ".Time Option"): selected uses the neutral
18936
+ elevated-2 fill + medium weight, NOT the brand color. */
17601
18937
  .option-button {
17602
18938
  border: none;
17603
18939
  background: transparent;
17604
- border-radius: var(--rel-border-radius-sm);
18940
+ border-radius: var(--rel-border-radius-xs);
17605
18941
  color: var(--rel-text-default);
17606
18942
  cursor: pointer;
17607
- padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
17608
- text-align: center;
17609
18943
  flex-shrink: 0;
17610
- transition: all 0.15s ease;
17611
- font-family: var(--rel-body-sm-medium-font-family);
17612
- font-weight: var(--rel-body-sm-medium-font-weight);
17613
- font-size: var(--rel-body-sm-medium-font-size);
17614
- line-height: var(--rel-body-sm-medium-line-height);
17615
- letter-spacing: var(--rel-body-sm-medium-letter-spacing);
18944
+ height: 36px;
18945
+ padding: 0 var(--rel-spacing-xs);
18946
+ text-align: center;
18947
+ transition: background 0.15s ease;
18948
+ font-family: var(--rel-body-md-regular-font-family);
18949
+ font-weight: var(--rel-body-md-regular-font-weight);
18950
+ font-size: var(--rel-body-md-regular-font-size);
18951
+ line-height: var(--rel-body-md-regular-line-height);
18952
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
17616
18953
  }
17617
18954
 
17618
18955
  .option-button:hover:not(:disabled):not(.selected) {
17619
- background: var(--rel-bg-surface-elevated);
18956
+ background: var(--rel-bg-neutral-elevated-1);
17620
18957
  }
17621
18958
 
17622
18959
  .option-button:focus-visible {
@@ -17626,9 +18963,9 @@ RelTimePicker.styles = import_lit54.css`
17626
18963
  }
17627
18964
 
17628
18965
  .option-button.selected {
17629
- background: var(--rel-action-brand-fill);
17630
- color: var(--rel-action-brand-label-icons-primary);
17631
- font-weight: var(--rel-body-md-semibold-font-weight);
18966
+ background: var(--rel-bg-neutral-elevated-2);
18967
+ color: var(--rel-text-default);
18968
+ font-weight: var(--rel-body-md-medium-font-weight);
17632
18969
  }
17633
18970
 
17634
18971
  .option-button:disabled {
@@ -17641,14 +18978,15 @@ RelTimePicker.styles = import_lit54.css`
17641
18978
  background: transparent;
17642
18979
  }
17643
18980
 
17644
- /* Footer (mirrors the range-picker calendar-footer) */
18981
+ /* Footer (Figma "Footer"): 8px padding + 8px extra left so the "Now"
18982
+ link aligns with the buttons' inner padding. Buttons are always S. */
17645
18983
  .time-footer {
17646
18984
  display: flex;
17647
18985
  align-items: center;
17648
18986
  justify-content: space-between;
17649
- gap: var(--rel-spacing-sm);
17650
- padding-top: var(--rel-spacing-sm);
17651
- margin-top: var(--rel-spacing-sm);
18987
+ gap: var(--rel-spacing-xs);
18988
+ padding: var(--rel-spacing-xs);
18989
+ padding-left: calc(var(--rel-spacing-xs) * 2);
17652
18990
  border-top: 1px solid var(--rel-border-subtle);
17653
18991
  }
17654
18992
 
@@ -17658,35 +18996,88 @@ RelTimePicker.styles = import_lit54.css`
17658
18996
  gap: var(--rel-spacing-xs);
17659
18997
  }
17660
18998
 
17661
- @media (prefers-reduced-motion: reduce) {
17662
- .trigger-button,
17663
- .icon-button,
17664
- .option-button {
17665
- transition: none;
17666
- }
18999
+ /* The design narrows OK below the DS small min-width (60 vs 80) so the
19000
+ compact footer fits the 220px panel. */
19001
+ .footer-right rel-button[variant="primary"] {
19002
+ --rel-button-min-width: 60px;
17667
19003
  }
17668
- `;
17669
- __decorateClass([
17670
- (0, import_decorators54.property)({ type: String, reflect: true })
17671
- ], RelTimePicker.prototype, "value", 2);
17672
- __decorateClass([
17673
- (0, import_decorators54.property)({ type: String })
17674
- ], RelTimePicker.prototype, "placeholder", 2);
17675
- __decorateClass([
17676
- (0, import_decorators54.property)({ type: String })
17677
- ], RelTimePicker.prototype, "label", 2);
17678
- __decorateClass([
17679
- (0, import_decorators54.property)({ type: Boolean, reflect: true })
17680
- ], RelTimePicker.prototype, "disabled", 2);
17681
- __decorateClass([
17682
- (0, import_decorators54.property)({ type: Boolean })
17683
- ], RelTimePicker.prototype, "required", 2);
19004
+
19005
+ /* Panel size variants (Figma S/M/L): width 220/250/280, option height
19006
+ 32/36/40, body-sm/md/lg type. Medium is the base above. */
19007
+ .time-picker-wrapper.small .popover {
19008
+ width: 220px;
19009
+ }
19010
+
19011
+ .time-picker-wrapper.small .columns {
19012
+ height: 232px;
19013
+ }
19014
+
19015
+ .time-picker-wrapper.small .option-button {
19016
+ height: 32px;
19017
+ font-family: var(--rel-body-sm-regular-font-family);
19018
+ font-weight: var(--rel-body-sm-regular-font-weight);
19019
+ font-size: var(--rel-body-sm-regular-font-size);
19020
+ line-height: var(--rel-body-sm-regular-line-height);
19021
+ letter-spacing: var(--rel-body-sm-regular-letter-spacing);
19022
+ }
19023
+
19024
+ .time-picker-wrapper.small .option-button.selected {
19025
+ font-weight: var(--rel-body-sm-medium-font-weight);
19026
+ }
19027
+
19028
+ .time-picker-wrapper.large .popover {
19029
+ width: 280px;
19030
+ }
19031
+
19032
+ .time-picker-wrapper.large .columns {
19033
+ height: 288px;
19034
+ }
19035
+
19036
+ .time-picker-wrapper.large .option-button {
19037
+ height: 40px;
19038
+ font-family: var(--rel-body-lg-regular-font-family);
19039
+ font-weight: var(--rel-body-lg-regular-font-weight);
19040
+ font-size: var(--rel-body-lg-regular-font-size);
19041
+ line-height: var(--rel-body-lg-regular-line-height);
19042
+ letter-spacing: var(--rel-body-lg-regular-letter-spacing);
19043
+ }
19044
+
19045
+ .time-picker-wrapper.large .option-button.selected {
19046
+ font-weight: var(--rel-body-lg-medium-font-weight);
19047
+ }
19048
+
19049
+ @media (prefers-reduced-motion: reduce) {
19050
+ .trigger-button,
19051
+ .icon-button,
19052
+ .option-button {
19053
+ transition: none;
19054
+ }
19055
+ }
19056
+ `;
19057
+ __decorateClass([
19058
+ (0, import_decorators54.property)({ type: String, reflect: true })
19059
+ ], RelTimePicker.prototype, "value", 2);
19060
+ __decorateClass([
19061
+ (0, import_decorators54.property)({ type: String })
19062
+ ], RelTimePicker.prototype, "placeholder", 2);
19063
+ __decorateClass([
19064
+ (0, import_decorators54.property)({ type: String })
19065
+ ], RelTimePicker.prototype, "label", 2);
19066
+ __decorateClass([
19067
+ (0, import_decorators54.property)({ type: Boolean, reflect: true })
19068
+ ], RelTimePicker.prototype, "disabled", 2);
19069
+ __decorateClass([
19070
+ (0, import_decorators54.property)({ type: Boolean })
19071
+ ], RelTimePicker.prototype, "required", 2);
17684
19072
  __decorateClass([
17685
19073
  (0, import_decorators54.property)({ type: Boolean, reflect: true })
17686
19074
  ], RelTimePicker.prototype, "readonly", 2);
17687
19075
  __decorateClass([
17688
19076
  (0, import_decorators54.property)({ type: String })
17689
19077
  ], RelTimePicker.prototype, "format", 2);
19078
+ __decorateClass([
19079
+ (0, import_decorators54.property)({ type: Boolean, reflect: true })
19080
+ ], RelTimePicker.prototype, "seconds", 2);
17690
19081
  __decorateClass([
17691
19082
  (0, import_decorators54.property)({ type: Number, attribute: "minute-step" })
17692
19083
  ], RelTimePicker.prototype, "minuteStep", 2);
@@ -17732,6 +19123,12 @@ __decorateClass([
17732
19123
  __decorateClass([
17733
19124
  (0, import_decorators54.state)()
17734
19125
  ], RelTimePicker.prototype, "draftMinute", 2);
19126
+ __decorateClass([
19127
+ (0, import_decorators54.state)()
19128
+ ], RelTimePicker.prototype, "draftSecond", 2);
19129
+ __decorateClass([
19130
+ (0, import_decorators54.state)()
19131
+ ], RelTimePicker.prototype, "openUp", 2);
17735
19132
  __decorateClass([
17736
19133
  (0, import_decorators54.query)(".trigger-button")
17737
19134
  ], RelTimePicker.prototype, "triggerButton", 2);
@@ -17970,6 +19367,26 @@ var RelTooltip = class extends import_lit56.LitElement {
17970
19367
  this.type = "single-line";
17971
19368
  // Stable id so aria-describedby can reference the bubble across renders.
17972
19369
  this.uniqueId = Math.random().toString(36).slice(2);
19370
+ /** APG: Escape dismisses the tooltip. Not swallowed — an enclosing dialog
19371
+ * may legitimately close on the same keystroke. */
19372
+ this.onKeydown = (e) => {
19373
+ if (e.key === "Escape") this.toggleAttribute("data-dismissed", true);
19374
+ };
19375
+ this.clearDismissed = () => {
19376
+ this.removeAttribute("data-dismissed");
19377
+ };
19378
+ }
19379
+ connectedCallback() {
19380
+ super.connectedCallback();
19381
+ this.addEventListener("keydown", this.onKeydown);
19382
+ this.addEventListener("mouseleave", this.clearDismissed);
19383
+ this.addEventListener("focusout", this.clearDismissed);
19384
+ }
19385
+ disconnectedCallback() {
19386
+ super.disconnectedCallback();
19387
+ this.removeEventListener("keydown", this.onKeydown);
19388
+ this.removeEventListener("mouseleave", this.clearDismissed);
19389
+ this.removeEventListener("focusout", this.clearDismissed);
17973
19390
  }
17974
19391
  firstUpdated() {
17975
19392
  this.linkTrigger();
@@ -18036,6 +19453,12 @@ RelTooltip.styles = import_lit56.css`
18036
19453
  opacity: 1;
18037
19454
  }
18038
19455
 
19456
+ /* Escape-dismissed (APG): stays hidden until hover/focus leaves and
19457
+ returns. Declared after the hover/focus rules so source order wins. */
19458
+ :host([data-dismissed]) .tooltip-bubble {
19459
+ opacity: 0;
19460
+ }
19461
+
18039
19462
  /* Positioning */
18040
19463
  .tooltip-bubble.above {
18041
19464
  bottom: calc(100% + 6px);
@@ -18125,10 +19548,914 @@ RelTooltip = __decorateClass([
18125
19548
  (0, import_decorators56.customElement)("rel-tooltip")
18126
19549
  ], RelTooltip);
18127
19550
 
19551
+ // src/components/rel-upload/rel-upload.ts
19552
+ var import_lit57 = require("lit");
19553
+ var import_decorators57 = require("lit/decorators.js");
19554
+ var import_rel_icon17 = require("@reliability-design/icons/rel-icon");
19555
+ var uid = 0;
19556
+ var RelUpload = class extends import_lit57.LitElement {
19557
+ constructor() {
19558
+ super(...arguments);
19559
+ this.accept = "";
19560
+ this.maxSize = "";
19561
+ this.multiple = false;
19562
+ this.disabled = false;
19563
+ this.hint = "";
19564
+ this.dropzoneTitle = "Drag and drop your files here";
19565
+ this.browseLabel = "Browse files";
19566
+ this.files = [];
19567
+ this.concurrency = 3;
19568
+ this.delayThreshold = 15e3;
19569
+ this.dragOver = false;
19570
+ this.delayed = false;
19571
+ this.lastSignature = "";
19572
+ this.openPicker = () => {
19573
+ if (this.disabled) return;
19574
+ this.inputEl?.click();
19575
+ };
19576
+ this.onInputChange = (e) => {
19577
+ const input = e.target;
19578
+ this.ingest(input.files);
19579
+ input.value = "";
19580
+ };
19581
+ this.onDragOver = (e) => {
19582
+ if (this.disabled) return;
19583
+ e.preventDefault();
19584
+ this.dragOver = true;
19585
+ };
19586
+ this.onDragLeave = (e) => {
19587
+ e.preventDefault();
19588
+ this.dragOver = false;
19589
+ };
19590
+ this.onDrop = (e) => {
19591
+ e.preventDefault();
19592
+ this.dragOver = false;
19593
+ if (this.disabled) return;
19594
+ this.ingest(e.dataTransfer?.files ?? null);
19595
+ };
19596
+ }
19597
+ // ── derived ───────────────────────────────────────────────────────────────
19598
+ /**
19599
+ * Aggregate state. Read-only; corresponds to the Figma modal variants.
19600
+ *
19601
+ * `failed` means there is nothing left to send AND something errored — while
19602
+ * any row is still `ready` the primary action is Upload, not Retry.
19603
+ */
19604
+ get phase() {
19605
+ if (this.files.length === 0) return "empty";
19606
+ if (this.files.some((f) => f.status === "uploading")) {
19607
+ return this.delayed ? "delayed" : "uploading";
19608
+ }
19609
+ if (this.pending.length > 0) return "selected";
19610
+ if (this.files.some((f) => f.status === "error")) return "failed";
19611
+ return "selected";
19612
+ }
19613
+ /** Files still eligible to be sent. */
19614
+ get pending() {
19615
+ return this.files.filter((f) => f.status === "ready");
19616
+ }
19617
+ get failedFiles() {
19618
+ return this.files.filter((f) => f.status === "error");
19619
+ }
19620
+ /**
19621
+ * Failed rows worth sending again. A file rejected by `accept`/`max-size`
19622
+ * will fail validation every time, so retrying it is pointless — re-check
19623
+ * rather than re-queueing it blindly.
19624
+ */
19625
+ get retryable() {
19626
+ const maxBytes = this.parseMaxBytes();
19627
+ return this.failedFiles.filter(
19628
+ (f) => !!f.file && this.matchesAccept(f.file) && f.file.size <= maxBytes
19629
+ );
19630
+ }
19631
+ get canUpload() {
19632
+ if (this.disabled) return false;
19633
+ const phase = this.phase;
19634
+ if (phase === "uploading" || phase === "delayed") return false;
19635
+ if (phase === "failed") return this.retryable.length > 0;
19636
+ return this.pending.length > 0;
19637
+ }
19638
+ /** Footer button label — "Upload", "Upload 3 files" or "Retry upload". */
19639
+ get primaryLabel() {
19640
+ if (this.phase === "failed") return "Retry upload";
19641
+ const n = this.files.length;
19642
+ return n > 1 ? `Upload ${n} files` : "Upload";
19643
+ }
19644
+ /** The snapshot `<rel-upload-actions>` renders from. */
19645
+ get state() {
19646
+ return {
19647
+ phase: this.phase,
19648
+ total: this.files.length,
19649
+ failed: this.failedFiles.length,
19650
+ canUpload: this.canUpload,
19651
+ primaryLabel: this.primaryLabel
19652
+ };
19653
+ }
19654
+ get listHeading() {
19655
+ const n = this.files.length;
19656
+ switch (this.phase) {
19657
+ case "uploading":
19658
+ case "delayed":
19659
+ return `Uploading ${n} ${n === 1 ? "file" : "files"}`;
19660
+ case "failed":
19661
+ return `${n} ${n === 1 ? "file" : "files"} - ${this.failedFiles.length} failed`;
19662
+ default:
19663
+ return n === 1 ? "Selected file (1)" : `Selected files (${n})`;
19664
+ }
19665
+ }
19666
+ get hintText() {
19667
+ if (this.hint) return this.hint;
19668
+ const formats = this.acceptDisplay;
19669
+ const size = this.maxSize ? ` - up to ${this.maxSize} per file` : "";
19670
+ return `${formats}${size}`;
19671
+ }
19672
+ get acceptDisplay() {
19673
+ const tokens = this.accept.split(",").map((s) => s.trim()).filter(Boolean).map((tok) => {
19674
+ if (tok.startsWith(".")) return tok.slice(1).toUpperCase();
19675
+ if (tok.endsWith("/*")) return tok.slice(0, -2).toUpperCase();
19676
+ return (tok.split("/").pop() ?? tok).toUpperCase();
19677
+ });
19678
+ if (tokens.length === 0) return "Any file type";
19679
+ if (tokens.length === 1) return tokens[0];
19680
+ return `${tokens.slice(0, -1).join(", ")} or ${tokens[tokens.length - 1]}`;
19681
+ }
19682
+ get alertContent() {
19683
+ if (this.phase === "delayed") {
19684
+ return {
19685
+ type: "warning",
19686
+ message1: "This is taking longer than usual",
19687
+ message2: "Large files can take a few minutes on a slow connection. You can leave this open or come back later."
19688
+ };
19689
+ }
19690
+ const failed = this.failedFiles.length;
19691
+ if (failed > 0 && this.phase !== "uploading") {
19692
+ const limit = this.maxSize ? `under ${this.maxSize} and ` : "";
19693
+ return {
19694
+ type: "error",
19695
+ message1: failed === 1 && this.files.length === 1 ? "We couldn't upload your file" : `We couldn't upload ${failed} of your files`,
19696
+ message2: `Check that each file is ${limit}in a supported format, then try again.`
19697
+ };
19698
+ }
19699
+ return null;
19700
+ }
19701
+ // ── validation ────────────────────────────────────────────────────────────
19702
+ parseMaxBytes() {
19703
+ const s = this.maxSize.trim().toLowerCase();
19704
+ if (!s) return Infinity;
19705
+ const m = s.match(/^([\d.]+)\s*(b|kb|mb|gb)?$/);
19706
+ if (!m) return Infinity;
19707
+ const n = parseFloat(m[1]);
19708
+ const unit = m[2] || "b";
19709
+ const mult = unit === "gb" ? 1e9 : unit === "mb" ? 1e6 : unit === "kb" ? 1e3 : 1;
19710
+ return n * mult;
19711
+ }
19712
+ matchesAccept(file) {
19713
+ const raw = this.accept.trim();
19714
+ if (!raw) return true;
19715
+ const tokens = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
19716
+ const name = file.name.toLowerCase();
19717
+ const type = (file.type || "").toLowerCase();
19718
+ return tokens.some((tok) => {
19719
+ if (tok.startsWith(".")) return name.endsWith(tok);
19720
+ if (tok.endsWith("/*")) return type.startsWith(tok.slice(0, -1));
19721
+ return type === tok;
19722
+ });
19723
+ }
19724
+ static fmtSize(bytes) {
19725
+ if (bytes < 1e3) return `${bytes} B`;
19726
+ if (bytes < 1e6) return `${(bytes / 1e3).toFixed(0)} KB`;
19727
+ return `${(bytes / 1e6).toFixed(1)} MB`;
19728
+ }
19729
+ static ext(name) {
19730
+ const i = name.lastIndexOf(".");
19731
+ return i > -1 ? name.slice(i + 1).toUpperCase() : "FILE";
19732
+ }
19733
+ /**
19734
+ * Validate and append. Invalid files are kept as `error` rows rather than
19735
+ * dropped — that is how the Figma Failure state surfaces them.
19736
+ */
19737
+ ingest(list) {
19738
+ if (!list || list.length === 0) return;
19739
+ const maxBytes = this.parseMaxBytes();
19740
+ const incoming = this.multiple ? Array.from(list) : Array.from(list).slice(0, 1);
19741
+ const added = [];
19742
+ const rejected = [];
19743
+ for (const f of incoming) {
19744
+ const entry = {
19745
+ id: `rel-upload-${++uid}`,
19746
+ name: f.name,
19747
+ size: f.size,
19748
+ status: "ready",
19749
+ file: f
19750
+ };
19751
+ if (!this.matchesAccept(f)) {
19752
+ entry.status = "error";
19753
+ entry.error = "unsupported file type";
19754
+ rejected.push({ name: f.name, reason: "format", message: `${f.name}: unsupported file type` });
19755
+ } else if (f.size > maxBytes) {
19756
+ entry.status = "error";
19757
+ entry.error = `file exceeds the ${this.maxSize} limit`;
19758
+ rejected.push({
19759
+ name: f.name,
19760
+ reason: "size",
19761
+ message: `${f.name}: exceeds ${this.maxSize}`
19762
+ });
19763
+ }
19764
+ added.push(entry);
19765
+ }
19766
+ const names = new Set(added.map((f) => f.name));
19767
+ const kept = this.multiple ? this.files.filter((f) => !names.has(f.name)) : [];
19768
+ this.files = [...kept, ...added];
19769
+ if (rejected.length) this.emit("rel-invalid", { rejected });
19770
+ this.emit("rel-files-added", { added, rejected });
19771
+ this.emit("rel-change", { files: this.files });
19772
+ this.notify();
19773
+ }
19774
+ // ── upload orchestration (self-driving mode) ──────────────────────────────
19775
+ patch(id, partial) {
19776
+ this.files = this.files.map((f) => f.id === id ? { ...f, ...partial } : f);
19777
+ this.notify();
19778
+ }
19779
+ startDelayTimer() {
19780
+ this.stopDelayTimer();
19781
+ if (this.delayThreshold <= 0) return;
19782
+ this.delayTimer = setTimeout(() => {
19783
+ if (this.files.some((f) => f.status === "uploading")) {
19784
+ this.delayed = true;
19785
+ this.notify();
19786
+ }
19787
+ }, this.delayThreshold);
19788
+ }
19789
+ stopDelayTimer() {
19790
+ if (this.delayTimer) clearTimeout(this.delayTimer);
19791
+ this.delayTimer = void 0;
19792
+ }
19793
+ async runBatch(targets) {
19794
+ const uploader = this.uploader;
19795
+ if (!uploader || targets.length === 0) return;
19796
+ this.aborter = new AbortController();
19797
+ const signal = this.aborter.signal;
19798
+ this.startDelayTimer();
19799
+ const queue = [...targets];
19800
+ const workers = Array.from({ length: Math.max(1, this.concurrency) }, async () => {
19801
+ while (queue.length) {
19802
+ if (signal.aborted) return;
19803
+ const item = queue.shift();
19804
+ if (!item || !item.file) continue;
19805
+ this.patch(item.id, { status: "uploading", progress: 0, loaded: 0, error: void 0 });
19806
+ try {
19807
+ await uploader(item.file, {
19808
+ file: item,
19809
+ signal,
19810
+ onProgress: (loaded, total) => {
19811
+ const t = total ?? item.size;
19812
+ const progress = t > 0 ? Math.min(100, Math.round(loaded / t * 100)) : 0;
19813
+ this.patch(item.id, { loaded, progress });
19814
+ this.emit("rel-progress", { id: item.id, progress, loaded, total: t });
19815
+ }
19816
+ });
19817
+ if (signal.aborted) {
19818
+ this.patch(item.id, { status: "ready", progress: void 0, loaded: void 0 });
19819
+ return;
19820
+ }
19821
+ this.patch(item.id, { status: "success", progress: 100, loaded: item.size });
19822
+ this.emit("rel-file-complete", { id: item.id, file: item.file });
19823
+ } catch (err) {
19824
+ if (signal.aborted) {
19825
+ this.patch(item.id, { status: "ready", progress: void 0, loaded: void 0 });
19826
+ return;
19827
+ }
19828
+ const message = err instanceof Error ? err.message : String(err);
19829
+ this.patch(item.id, { status: "error", error: message, progress: void 0 });
19830
+ this.emit("rel-file-error", { id: item.id, file: item.file, error: message });
19831
+ }
19832
+ }
19833
+ });
19834
+ await Promise.all(workers);
19835
+ this.stopDelayTimer();
19836
+ this.delayed = false;
19837
+ this.aborter = void 0;
19838
+ if (!signal.aborted) {
19839
+ this.emit("rel-complete", {
19840
+ succeeded: this.files.filter((f) => f.status === "success"),
19841
+ failed: this.failedFiles
19842
+ });
19843
+ }
19844
+ this.notify();
19845
+ }
19846
+ // ── public API ────────────────────────────────────────────────────────────
19847
+ /** Start (or resume) the batch. Emits `rel-upload`; runs it when `uploader` is set. */
19848
+ upload() {
19849
+ if (!this.canUpload) return;
19850
+ const targets = this.pending;
19851
+ this.emit("rel-upload", { files: targets });
19852
+ if (this.uploader) void this.runBatch(targets);
19853
+ else this.notify();
19854
+ }
19855
+ /** Re-queue every retryable failed row and send again. */
19856
+ retry() {
19857
+ const targets = this.retryable;
19858
+ if (targets.length === 0) return;
19859
+ const ids = new Set(targets.map((f) => f.id));
19860
+ this.files = this.files.map(
19861
+ (f) => ids.has(f.id) ? { ...f, status: "ready", error: void 0, progress: void 0 } : f
19862
+ );
19863
+ const requeued = this.files.filter((f) => ids.has(f.id));
19864
+ this.emit("rel-retry", { files: requeued });
19865
+ if (this.uploader) void this.runBatch(requeued);
19866
+ else this.notify();
19867
+ }
19868
+ /** Abort anything in flight and emit `rel-cancel`. Does not clear the list. */
19869
+ cancel() {
19870
+ this.aborter?.abort();
19871
+ this.stopDelayTimer();
19872
+ this.delayed = false;
19873
+ this.emit("rel-cancel");
19874
+ this.notify();
19875
+ }
19876
+ /** Remove one row by id. */
19877
+ removeFile(id) {
19878
+ const target = this.files.find((f) => f.id === id);
19879
+ this.files = this.files.filter((f) => f.id !== id);
19880
+ this.emit("rel-remove", { id, file: target?.file });
19881
+ this.emit("rel-change", { files: this.files });
19882
+ this.notify();
19883
+ }
19884
+ /** Clear everything and abort in-flight transfers. */
19885
+ reset() {
19886
+ this.aborter?.abort();
19887
+ this.stopDelayTimer();
19888
+ this.files = [];
19889
+ this.delayed = false;
19890
+ if (this.inputEl) this.inputEl.value = "";
19891
+ this.emit("rel-change", { files: this.files });
19892
+ this.notify();
19893
+ }
19894
+ // ── events ────────────────────────────────────────────────────────────────
19895
+ emit(type, detail) {
19896
+ this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
19897
+ }
19898
+ /**
19899
+ * Emit `rel-state` only when the derived snapshot actually changes, so
19900
+ * progress ticks don't spam `<rel-upload-actions>`.
19901
+ */
19902
+ notify() {
19903
+ const s = this.state;
19904
+ const signature = `${s.phase}|${s.total}|${s.failed}|${s.canUpload}|${s.primaryLabel}`;
19905
+ if (signature === this.lastSignature) return;
19906
+ this.lastSignature = signature;
19907
+ this.emit("rel-state", s);
19908
+ }
19909
+ connectedCallback() {
19910
+ super.connectedCallback();
19911
+ queueMicrotask(() => this.emit("rel-state", this.state));
19912
+ }
19913
+ disconnectedCallback() {
19914
+ super.disconnectedCallback();
19915
+ this.stopDelayTimer();
19916
+ this.aborter?.abort();
19917
+ }
19918
+ updated() {
19919
+ this.notify();
19920
+ }
19921
+ // ── render ────────────────────────────────────────────────────────────────
19922
+ rowMeta(f) {
19923
+ switch (f.status) {
19924
+ case "uploading": {
19925
+ const pct = f.progress ?? 0;
19926
+ const loaded = RelUpload.fmtSize(f.loaded ?? 0);
19927
+ return `${pct}% - ${loaded} of ${RelUpload.fmtSize(f.size)}`;
19928
+ }
19929
+ case "success":
19930
+ return `${RelUpload.ext(f.name)} - ${RelUpload.fmtSize(f.size)} - Uploaded`;
19931
+ case "error":
19932
+ return `Upload failed - ${f.error ?? "please try again"}`;
19933
+ default: {
19934
+ const base = `${RelUpload.ext(f.name)} - ${RelUpload.fmtSize(f.size)}`;
19935
+ const busy = this.phase === "uploading" || this.phase === "delayed";
19936
+ return busy ? `${base} - Waiting` : base;
19937
+ }
19938
+ }
19939
+ }
19940
+ renderRow(f) {
19941
+ return import_lit57.html`
19942
+ <div class="row ${f.status}">
19943
+ <rel-icon name="document" size="md"></rel-icon>
19944
+
19945
+ <div class="row-content">
19946
+ <span class="row-name" title=${f.name}>${f.name}</span>
19947
+ ${f.status === "uploading" ? import_lit57.html`<rel-progress
19948
+ mode="linear-determinate"
19949
+ .value=${f.progress ?? 0}
19950
+ ></rel-progress>` : import_lit57.nothing}
19951
+ <span class="row-meta">${this.rowMeta(f)}</span>
19952
+ </div>
19953
+
19954
+ <div class="row-trailing">
19955
+ ${f.status === "success" ? import_lit57.html`<rel-icon
19956
+ class="status-success"
19957
+ name="check-circle"
19958
+ size="md"
19959
+ role="img"
19960
+ aria-label="Uploaded"
19961
+ ></rel-icon>` : import_lit57.nothing}
19962
+ ${f.status === "error" ? import_lit57.html`<rel-icon
19963
+ class="status-error"
19964
+ name="error"
19965
+ size="md"
19966
+ role="img"
19967
+ aria-label="Failed"
19968
+ ></rel-icon>` : import_lit57.nothing}
19969
+ <button
19970
+ class="remove"
19971
+ aria-label="Remove ${f.name}"
19972
+ ?disabled=${this.disabled}
19973
+ @click=${() => this.removeFile(f.id)}
19974
+ >
19975
+ <rel-icon name="delete" size="md"></rel-icon>
19976
+ </button>
19977
+ </div>
19978
+ </div>
19979
+ `;
19980
+ }
19981
+ render() {
19982
+ const alert = this.alertContent;
19983
+ return import_lit57.html`
19984
+ <div class="body">
19985
+ ${alert ? import_lit57.html`<rel-alert
19986
+ type=${alert.type}
19987
+ appearance="subtle"
19988
+ message1=${alert.message1}
19989
+ message2=${alert.message2}
19990
+ ></rel-alert>` : import_lit57.nothing}
19991
+
19992
+ <div
19993
+ class="dropzone ${this.dragOver ? "dragover" : ""}"
19994
+ @click=${this.openPicker}
19995
+ @dragover=${this.onDragOver}
19996
+ @dragleave=${this.onDragLeave}
19997
+ @drop=${this.onDrop}
19998
+ >
19999
+ <span class="icon-badge">
20000
+ <rel-icon name="upload" size="md"></rel-icon>
20001
+ </span>
20002
+
20003
+ <div class="dz-copy">
20004
+ <p class="dz-title">${this.dropzoneTitle}</p>
20005
+ <p class="dz-hint">${this.hintText}</p>
20006
+ </div>
20007
+
20008
+ <button
20009
+ class="browse"
20010
+ type="button"
20011
+ ?disabled=${this.disabled}
20012
+ @click=${(e) => {
20013
+ e.stopPropagation();
20014
+ this.openPicker();
20015
+ }}
20016
+ >
20017
+ ${this.browseLabel}
20018
+ </button>
20019
+ </div>
20020
+
20021
+ ${this.files.length ? import_lit57.html`<div class="file-list">
20022
+ <p class="list-heading">${this.listHeading}</p>
20023
+ <div class="rows" role="list">${this.files.map((f) => this.renderRow(f))}</div>
20024
+ </div>` : import_lit57.nothing}
20025
+
20026
+ <input
20027
+ class="file-input"
20028
+ type="file"
20029
+ ?multiple=${this.multiple}
20030
+ accept=${this.accept || import_lit57.nothing}
20031
+ @change=${this.onInputChange}
20032
+ />
20033
+ </div>
20034
+ `;
20035
+ }
20036
+ };
20037
+ RelUpload.styles = import_lit57.css`
20038
+ :host {
20039
+ display: block;
20040
+ box-sizing: border-box;
20041
+ color: var(--rel-text-default);
20042
+ font-family: var(--rel-fonts-font-family);
20043
+ }
20044
+
20045
+ :host([disabled]) {
20046
+ opacity: 0.6;
20047
+ }
20048
+
20049
+ :host([disabled]) .body {
20050
+ pointer-events: none;
20051
+ }
20052
+
20053
+ .body {
20054
+ display: flex;
20055
+ flex-direction: column;
20056
+ gap: var(--rel-spacing-lg, 20px);
20057
+ }
20058
+
20059
+ /* ── Dropzone ─────────────────────────────────────────────────────────── */
20060
+
20061
+ .dropzone {
20062
+ box-sizing: border-box;
20063
+ display: flex;
20064
+ flex-direction: column;
20065
+ align-items: center;
20066
+ justify-content: center;
20067
+ gap: var(--rel-spacing-md, 16px);
20068
+ padding: var(--rel-spacing-2xl, 32px) var(--rel-spacing-xl, 24px);
20069
+ background: var(--rel-bg-surface);
20070
+ border: 1px dashed var(--rel-border-accent);
20071
+ border-radius: var(--rel-border-radius-md, 16px);
20072
+ text-align: center;
20073
+ cursor: pointer;
20074
+ transition: border-color 0.15s ease, background 0.15s ease;
20075
+ }
20076
+
20077
+ /* Design gap: no drag-over frame exists in Figma. */
20078
+ .dropzone.dragover {
20079
+ border-style: solid;
20080
+ border-color: var(--rel-border-brand);
20081
+ background: color-mix(in srgb, var(--rel-bg-brand) 8%, var(--rel-bg-surface));
20082
+ }
20083
+
20084
+ .icon-badge {
20085
+ display: inline-flex;
20086
+ align-items: center;
20087
+ justify-content: center;
20088
+ padding: var(--rel-spacing-sm, 12px);
20089
+ background: var(--rel-bg-surface);
20090
+ border: 1px solid var(--rel-border-subtle);
20091
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
20092
+ }
20093
+
20094
+ .dz-copy {
20095
+ display: flex;
20096
+ flex-direction: column;
20097
+ align-items: center;
20098
+ gap: var(--rel-spacing-2xs, 4px);
20099
+ }
20100
+
20101
+ .dz-title {
20102
+ margin: 0;
20103
+ color: var(--rel-text-default);
20104
+ font-family: var(--rel-body-lg-medium-font-family);
20105
+ font-weight: var(--rel-body-lg-medium-font-weight);
20106
+ font-size: var(--rel-body-lg-medium-font-size);
20107
+ line-height: var(--rel-body-lg-medium-line-height);
20108
+ }
20109
+
20110
+ .dz-hint {
20111
+ margin: 0;
20112
+ color: var(--rel-text-caption);
20113
+ font-family: var(--rel-body-sm-regular-font-family);
20114
+ font-weight: var(--rel-body-sm-regular-font-weight);
20115
+ font-size: var(--rel-body-sm-regular-font-size);
20116
+ line-height: var(--rel-body-sm-regular-line-height);
20117
+ }
20118
+
20119
+ /* The dropzone is a mouse affordance only — the Browse button is the sole
20120
+ focusable control, so there are no nested interactive elements. */
20121
+ .browse {
20122
+ display: inline-flex;
20123
+ align-items: center;
20124
+ gap: var(--rel-spacing-xs, 8px);
20125
+ padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-md, 16px);
20126
+ background: transparent;
20127
+ color: var(--rel-text-default);
20128
+ border: 1px solid var(--rel-action-neutral-border-outline, var(--rel-border-accent));
20129
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
20130
+ cursor: pointer;
20131
+ font-family: var(--rel-body-md-semibold-font-family);
20132
+ font-weight: var(--rel-body-md-semibold-font-weight);
20133
+ font-size: var(--rel-body-md-semibold-font-size);
20134
+ line-height: var(--rel-body-md-semibold-line-height);
20135
+ }
20136
+
20137
+ .browse:hover:not(:disabled) {
20138
+ background: var(--rel-action-neutral-outline-hover, var(--rel-bg-surface-elevated));
20139
+ }
20140
+
20141
+ .browse:focus-visible {
20142
+ outline: 2px solid var(--rel-border-focus);
20143
+ outline-offset: 2px;
20144
+ }
20145
+
20146
+ .browse:disabled {
20147
+ cursor: not-allowed;
20148
+ }
20149
+
20150
+ /* ── File list ────────────────────────────────────────────────────────── */
20151
+
20152
+ .file-list {
20153
+ display: flex;
20154
+ flex-direction: column;
20155
+ gap: var(--rel-spacing-sm, 12px);
20156
+ }
20157
+
20158
+ .list-heading {
20159
+ margin: 0;
20160
+ color: var(--rel-text-default);
20161
+ font-family: var(--rel-body-md-semibold-font-family);
20162
+ font-weight: var(--rel-body-md-semibold-font-weight);
20163
+ font-size: var(--rel-body-md-semibold-font-size);
20164
+ line-height: var(--rel-body-md-semibold-line-height);
20165
+ }
20166
+
20167
+ .rows {
20168
+ display: flex;
20169
+ flex-direction: column;
20170
+ gap: var(--rel-spacing-xs, 8px);
20171
+ }
20172
+
20173
+ .row {
20174
+ display: flex;
20175
+ align-items: center;
20176
+ gap: var(--rel-spacing-sm, 12px);
20177
+ box-sizing: border-box;
20178
+ padding: var(--rel-spacing-sm, 12px) var(--rel-spacing-md, 16px);
20179
+ background: var(--rel-bg-surface);
20180
+ border: 1px solid var(--rel-border-default);
20181
+ border-radius: var(--rel-border-radius-sm, 8px);
20182
+ }
20183
+
20184
+ .row.error {
20185
+ background: var(--rel-bg-danger-light);
20186
+ border-color: var(--rel-border-danger);
20187
+ }
20188
+
20189
+ .row > rel-icon {
20190
+ flex-shrink: 0;
20191
+ }
20192
+
20193
+ .row-content {
20194
+ display: flex;
20195
+ flex-direction: column;
20196
+ gap: var(--rel-spacing-3xs, 2px);
20197
+ flex: 1;
20198
+ min-width: 0;
20199
+ }
20200
+
20201
+ .row.uploading .row-content {
20202
+ gap: var(--rel-spacing-2xs, 4px);
20203
+ }
20204
+
20205
+ .row-name {
20206
+ overflow: hidden;
20207
+ text-overflow: ellipsis;
20208
+ white-space: nowrap;
20209
+ color: var(--rel-text-default);
20210
+ font-family: var(--rel-body-md-regular-font-family);
20211
+ font-weight: var(--rel-body-md-regular-font-weight);
20212
+ font-size: var(--rel-body-md-regular-font-size);
20213
+ line-height: var(--rel-body-md-regular-line-height);
20214
+ }
20215
+
20216
+ .row-meta {
20217
+ color: var(--rel-text-caption);
20218
+ font-family: var(--rel-body-sm-regular-font-family);
20219
+ font-weight: var(--rel-body-sm-regular-font-weight);
20220
+ font-size: var(--rel-body-sm-regular-font-size);
20221
+ line-height: var(--rel-body-sm-regular-line-height);
20222
+ }
20223
+
20224
+ .row.success .row-meta {
20225
+ color: var(--rel-text-success);
20226
+ }
20227
+
20228
+ .row.error .row-meta {
20229
+ color: var(--rel-text-danger);
20230
+ }
20231
+
20232
+ /* Figma's 4px rounded track; rel-progress ships a 6px square one. */
20233
+ rel-progress::part(track) {
20234
+ height: 4px;
20235
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
20236
+ }
20237
+
20238
+ rel-progress::part(indicator) {
20239
+ background: var(--rel-bg-success);
20240
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
20241
+ }
20242
+
20243
+ .row-trailing {
20244
+ display: flex;
20245
+ align-items: center;
20246
+ gap: var(--rel-spacing-sm, 12px);
20247
+ flex-shrink: 0;
20248
+ }
20249
+
20250
+ .status-success {
20251
+ color: var(--rel-text-success);
20252
+ }
20253
+
20254
+ .status-error {
20255
+ color: var(--rel-text-danger);
20256
+ }
20257
+
20258
+ .remove {
20259
+ display: inline-flex;
20260
+ align-items: center;
20261
+ justify-content: center;
20262
+ padding: 0;
20263
+ background: none;
20264
+ border: none;
20265
+ cursor: pointer;
20266
+ color: var(--rel-text-default);
20267
+ }
20268
+
20269
+ .remove:focus-visible {
20270
+ outline: 2px solid var(--rel-border-focus);
20271
+ outline-offset: 2px;
20272
+ border-radius: var(--rel-border-radius-xs, 4px);
20273
+ }
20274
+
20275
+ .file-input {
20276
+ display: none;
20277
+ }
20278
+
20279
+ @media (prefers-reduced-motion: reduce) {
20280
+ .dropzone {
20281
+ transition: none;
20282
+ }
20283
+ }
20284
+ `;
20285
+ __decorateClass([
20286
+ (0, import_decorators57.property)({ type: String })
20287
+ ], RelUpload.prototype, "accept", 2);
20288
+ __decorateClass([
20289
+ (0, import_decorators57.property)({ type: String, attribute: "max-size" })
20290
+ ], RelUpload.prototype, "maxSize", 2);
20291
+ __decorateClass([
20292
+ (0, import_decorators57.property)({ type: Boolean })
20293
+ ], RelUpload.prototype, "multiple", 2);
20294
+ __decorateClass([
20295
+ (0, import_decorators57.property)({ type: Boolean, reflect: true })
20296
+ ], RelUpload.prototype, "disabled", 2);
20297
+ __decorateClass([
20298
+ (0, import_decorators57.property)({ type: String })
20299
+ ], RelUpload.prototype, "hint", 2);
20300
+ __decorateClass([
20301
+ (0, import_decorators57.property)({ type: String, attribute: "dropzone-title" })
20302
+ ], RelUpload.prototype, "dropzoneTitle", 2);
20303
+ __decorateClass([
20304
+ (0, import_decorators57.property)({ type: String, attribute: "browse-label" })
20305
+ ], RelUpload.prototype, "browseLabel", 2);
20306
+ __decorateClass([
20307
+ (0, import_decorators57.property)({ attribute: false })
20308
+ ], RelUpload.prototype, "files", 2);
20309
+ __decorateClass([
20310
+ (0, import_decorators57.property)({ attribute: false })
20311
+ ], RelUpload.prototype, "uploader", 2);
20312
+ __decorateClass([
20313
+ (0, import_decorators57.property)({ type: Number })
20314
+ ], RelUpload.prototype, "concurrency", 2);
20315
+ __decorateClass([
20316
+ (0, import_decorators57.property)({ type: Number, attribute: "delay-threshold" })
20317
+ ], RelUpload.prototype, "delayThreshold", 2);
20318
+ __decorateClass([
20319
+ (0, import_decorators57.state)()
20320
+ ], RelUpload.prototype, "dragOver", 2);
20321
+ __decorateClass([
20322
+ (0, import_decorators57.state)()
20323
+ ], RelUpload.prototype, "delayed", 2);
20324
+ __decorateClass([
20325
+ (0, import_decorators57.query)(".file-input")
20326
+ ], RelUpload.prototype, "inputEl", 2);
20327
+ RelUpload = __decorateClass([
20328
+ (0, import_decorators57.customElement)("rel-upload")
20329
+ ], RelUpload);
20330
+
20331
+ // src/components/rel-upload/rel-upload-actions.ts
20332
+ var import_lit58 = require("lit");
20333
+ var import_decorators58 = require("lit/decorators.js");
20334
+ var import_rel_icon18 = require("@reliability-design/icons/rel-icon");
20335
+ var EMPTY_STATE = {
20336
+ phase: "empty",
20337
+ total: 0,
20338
+ failed: 0,
20339
+ canUpload: false,
20340
+ primaryLabel: "Upload"
20341
+ };
20342
+ var RelUploadActions = class extends import_lit58.LitElement {
20343
+ constructor() {
20344
+ super(...arguments);
20345
+ this.for = "";
20346
+ this.cancelLabel = "Cancel";
20347
+ this.noCancel = false;
20348
+ this.snapshot = EMPTY_STATE;
20349
+ this.target = null;
20350
+ this.onState = (e) => {
20351
+ if (!this.target) this.resolveTarget();
20352
+ if (e.target !== this.target) return;
20353
+ this.snapshot = e.detail;
20354
+ };
20355
+ this.onCancel = () => {
20356
+ this.target?.cancel();
20357
+ this.dispatchEvent(new CustomEvent("rel-cancel", { bubbles: true, composed: true }));
20358
+ };
20359
+ this.onPrimary = () => {
20360
+ if (!this.target) return;
20361
+ if (this.snapshot.phase === "failed") this.target.retry();
20362
+ else this.target.upload();
20363
+ };
20364
+ }
20365
+ connectedCallback() {
20366
+ super.connectedCallback();
20367
+ queueMicrotask(() => this.resolveTarget());
20368
+ this.root.addEventListener("rel-state", this.onState);
20369
+ }
20370
+ disconnectedCallback() {
20371
+ super.disconnectedCallback();
20372
+ this.root.removeEventListener("rel-state", this.onState);
20373
+ this.target = null;
20374
+ }
20375
+ updated(changed) {
20376
+ if (changed.has("for")) this.resolveTarget();
20377
+ }
20378
+ /**
20379
+ * Listen on our own root rather than `document`: a shared root means no
20380
+ * shadow boundary sits between us and the sibling `<rel-upload>`, so
20381
+ * `event.target` is never retargeted away from it.
20382
+ */
20383
+ get root() {
20384
+ const node = this.getRootNode();
20385
+ return node instanceof ShadowRoot ? node : this.ownerDocument;
20386
+ }
20387
+ resolveTarget() {
20388
+ const root = this.getRootNode();
20389
+ let found = null;
20390
+ if (this.for) {
20391
+ found = root.querySelector(`#${CSS.escape(this.for)}`);
20392
+ } else {
20393
+ found = this.parentElement?.querySelector("rel-upload") ?? root.querySelector("rel-upload");
20394
+ }
20395
+ this.target = found;
20396
+ if (found) this.snapshot = found.state;
20397
+ }
20398
+ render() {
20399
+ const { phase, canUpload, primaryLabel } = this.snapshot;
20400
+ const busy = phase === "uploading" || phase === "delayed";
20401
+ return import_lit58.html`
20402
+ <div class="actions" part="base">
20403
+ ${this.noCancel ? import_lit58.nothing : import_lit58.html`<rel-button part="cancel" variant="secondary" @click=${this.onCancel}>
20404
+ ${this.cancelLabel}
20405
+ </rel-button>`}
20406
+
20407
+ <rel-button
20408
+ part="primary"
20409
+ variant="primary"
20410
+ ?loading=${busy}
20411
+ ?disabled=${!canUpload}
20412
+ aria-label=${busy ? "Uploading" : primaryLabel}
20413
+ @click=${this.onPrimary}
20414
+ >
20415
+ ${busy ? import_lit58.nothing : import_lit58.html`${phase === "failed" ? import_lit58.html`<rel-icon name="refresh" size="sm"></rel-icon>` : import_lit58.nothing}${primaryLabel}`}
20416
+ </rel-button>
20417
+ </div>
20418
+ `;
20419
+ }
20420
+ };
20421
+ RelUploadActions.styles = import_lit58.css`
20422
+ :host {
20423
+ display: block;
20424
+ font-family: var(--rel-fonts-font-family);
20425
+ }
20426
+
20427
+ .actions {
20428
+ display: flex;
20429
+ align-items: center;
20430
+ justify-content: flex-end;
20431
+ gap: var(--rel-spacing-xs, 8px);
20432
+ }
20433
+
20434
+ rel-button rel-icon {
20435
+ margin-inline-end: var(--rel-spacing-xs, 8px);
20436
+ }
20437
+ `;
20438
+ __decorateClass([
20439
+ (0, import_decorators58.property)({ type: String })
20440
+ ], RelUploadActions.prototype, "for", 2);
20441
+ __decorateClass([
20442
+ (0, import_decorators58.property)({ type: String, attribute: "cancel-label" })
20443
+ ], RelUploadActions.prototype, "cancelLabel", 2);
20444
+ __decorateClass([
20445
+ (0, import_decorators58.property)({ type: Boolean, attribute: "no-cancel" })
20446
+ ], RelUploadActions.prototype, "noCancel", 2);
20447
+ __decorateClass([
20448
+ (0, import_decorators58.state)()
20449
+ ], RelUploadActions.prototype, "snapshot", 2);
20450
+ RelUploadActions = __decorateClass([
20451
+ (0, import_decorators58.customElement)("rel-upload-actions")
20452
+ ], RelUploadActions);
20453
+
18128
20454
  // src/index.ts
18129
20455
  __reExport(index_exports, require("@reliability-design/icons"), module.exports);
18130
20456
  // Annotate the CommonJS export names for ESM import in node:
18131
20457
  0 && (module.exports = {
20458
+ FocusTrap,
18132
20459
  RelAccordion,
18133
20460
  RelAlert,
18134
20461
  RelAutocomplete,
@@ -18188,6 +20515,17 @@ __reExport(index_exports, require("@reliability-design/icons"), module.exports);
18188
20515
  RelTopbarMobilePanel,
18189
20516
  RelTopbarProfile,
18190
20517
  RelTopbarUtilities,
20518
+ RelUpload,
20519
+ RelUploadActions,
20520
+ bindShortcut,
20521
+ formatShortcutParts,
20522
+ getDeepActiveElement,
20523
+ getFocusableElements,
20524
+ isApplePlatform,
20525
+ isEditableTarget,
20526
+ matchesShortcut,
20527
+ parseShortcut,
20528
+ trapTabKey,
18191
20529
  ...require("@reliability-design/icons")
18192
20530
  });
18193
20531
  //# sourceMappingURL=index.js.map