@reliability-design/web 1.4.2 → 1.4.4

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.
package/dist/index.mjs CHANGED
@@ -681,6 +681,7 @@ var RelTextfield = class extends LitElement3 {
681
681
  this.internalState = "default";
682
682
  this._wasError = false;
683
683
  this.uniqueId = `textfield-${Math.random().toString(36).slice(2)}`;
684
+ this._warnedNoName = false;
684
685
  }
685
686
  get isDisabled() {
686
687
  return this.disabled || this.state === "disabled";
@@ -696,6 +697,15 @@ var RelTextfield = class extends LitElement3 {
696
697
  get helper() {
697
698
  return this.helperText ?? this.supportingText;
698
699
  }
700
+ /** labelStyle supersedes the deprecated floatingLabel boolean. */
701
+ get resolvedLabelStyle() {
702
+ if (this.labelStyle) return this.labelStyle;
703
+ return this.floatingLabel ? "floating" : "default";
704
+ }
705
+ /** True when no visible label renders and the input needs an ARIA name. */
706
+ get labelHidden() {
707
+ return this.resolvedLabelStyle === "none";
708
+ }
699
709
  connectedCallback() {
700
710
  super.connectedCallback();
701
711
  const leftIcon = this.getAttribute("lefticon") || this.getAttribute("leftIcon");
@@ -703,6 +713,20 @@ var RelTextfield = class extends LitElement3 {
703
713
  const rightIcon = this.getAttribute("righticon") || this.getAttribute("rightIcon");
704
714
  if (rightIcon) this.rightIcon = rightIcon;
705
715
  }
716
+ willUpdate(changed) {
717
+ if (changed.has("labelStyle") && this.labelStyle) {
718
+ this.floatingLabel = this.labelStyle === "floating";
719
+ }
720
+ }
721
+ updated() {
722
+ if (this._warnedNoName || !this.labelHidden) return;
723
+ if (this.fieldAriaLabel || this.fieldAriaLabelledby || this.getAttribute("aria-label")) return;
724
+ this._warnedNoName = true;
725
+ console.warn(
726
+ '<rel-textfield label-style="none"> has no accessible name. Set `field-aria-label` or `field-aria-labelledby` \u2014 a placeholder is not a valid naming mechanism.',
727
+ this
728
+ );
729
+ }
706
730
  /** Focuses the inner input/textarea (the host itself is not focusable). */
707
731
  focus(options) {
708
732
  this.renderRoot.querySelector("input, textarea")?.focus(options);
@@ -770,15 +794,20 @@ var RelTextfield = class extends LitElement3 {
770
794
  !this.multiline && leadingIcon ? "has-left-icon" : "",
771
795
  !this.multiline && this.rightIcon && this.variant !== "legend" ? "has-right-icon" : ""
772
796
  ].filter(Boolean).join(" ");
797
+ const labelStyle = this.resolvedLabelStyle;
798
+ const labelHidden = labelStyle === "none";
773
799
  const labelTemplate = (extraClass = "") => {
774
- if (!this.label) return "";
800
+ if (!this.label || labelHidden) return "";
775
801
  return html3`<label
776
802
  part="label"
777
803
  class="textfield-label${extraClass}"
778
804
  for="${inputId}"
779
805
  >${this.label}${this.required ? html3`<span class="required-mark" aria-hidden="true">*</span>` : ""}</label>`;
780
806
  };
781
- const ariaLabelledby = this.variant === "legend" && this.label ? legendId : void 0;
807
+ const legendLabelledby = this.variant === "legend" && this.label && !labelHidden ? legendId : void 0;
808
+ const ariaLabelledby = this.fieldAriaLabelledby ?? legendLabelledby;
809
+ const hasVisibleLabel = !labelHidden && !!this.label;
810
+ const ariaLabel = !hasVisibleLabel && !ariaLabelledby ? this.fieldAriaLabel : void 0;
782
811
  const inputTemplate = this.multiline ? html3`<textarea part="input"
783
812
  id="${inputId}"
784
813
  class="${inputClasses}"
@@ -792,6 +821,7 @@ var RelTextfield = class extends LitElement3 {
792
821
  ?required="${this.required}"
793
822
  aria-invalid="${isError}"
794
823
  aria-required="${this.required}"
824
+ aria-label="${ariaLabel ?? nothing}"
795
825
  aria-labelledby="${ariaLabelledby ?? nothing}"
796
826
  aria-describedby="${describedById ?? nothing}"
797
827
  @focus="${this.handleFocus}"
@@ -810,6 +840,7 @@ var RelTextfield = class extends LitElement3 {
810
840
  ?required="${this.required}"
811
841
  aria-invalid="${isError}"
812
842
  aria-required="${this.required}"
843
+ aria-label="${ariaLabel ?? nothing}"
813
844
  aria-labelledby="${ariaLabelledby ?? nothing}"
814
845
  aria-describedby="${describedById ?? nothing}"
815
846
  @focus="${this.handleFocus}"
@@ -831,7 +862,7 @@ var RelTextfield = class extends LitElement3 {
831
862
  const fieldsetClass = ["field-fieldset", this.multiline ? "multiline" : ""].filter(Boolean).join(" ");
832
863
  return html3`
833
864
  <div class="${wrapperClasses}">
834
- <fieldset part="control" class="${fieldsetClass}"><legend class="field-legend" id="${legendId}">${this.label ?? ""}${this.required ? html3`<span class="required-mark" aria-hidden="true">*</span>` : ""}</legend>
865
+ <fieldset part="control" class="${fieldsetClass}"><legend class="field-legend" id="${legendId}">${labelHidden ? "" : this.label ?? ""}${this.required && !labelHidden ? html3`<span class="required-mark" aria-hidden="true">*</span>` : ""}</legend>
835
866
  ${!this.multiline ? leadingIconTemplate : ""}
836
867
  ${inputTemplate}
837
868
  </fieldset>
@@ -841,10 +872,10 @@ var RelTextfield = class extends LitElement3 {
841
872
  }
842
873
  return html3`
843
874
  <div class="${wrapperClasses}">
844
- ${!this.floatingLabel ? labelTemplate() : ""}
875
+ ${labelHidden ? html3`<div class="label-reserve" aria-hidden="true"></div>` : labelStyle !== "floating" ? labelTemplate() : ""}
845
876
 
846
877
  <div part="control" class="${["textfield-container", this.isDisabled ? "disabled" : "", this.multiline ? "multiline" : ""].filter(Boolean).join(" ")}">
847
- ${this.floatingLabel ? labelTemplate(leadingIcon && !this.multiline ? " has-left-icon" : "") : ""}
878
+ ${labelStyle === "floating" ? labelTemplate(leadingIcon && !this.multiline ? " has-left-icon" : "") : ""}
848
879
  ${!this.multiline ? leadingIconTemplate : ""}
849
880
 
850
881
  ${inputTemplate}
@@ -903,6 +934,22 @@ RelTextfield.styles = css3`
903
934
  margin-left: 2px;
904
935
  }
905
936
 
937
+ /* ── labelStyle="none" ──
938
+ No label renders. An empty spacer holds the label row's full height so the
939
+ input box still bottom-aligns with labelled siblings in a form row.
940
+ The reserve covers label line-height + the wrapper gap, and the negative
941
+ margin cancels the flex gap that would otherwise sit below it — so
942
+ --rel-field-label-reserve: 0 collapses the row to genuinely zero
943
+ (toolbars, table cells), not just to the leftover gap. */
944
+ .label-reserve {
945
+ flex: none;
946
+ height: var(
947
+ --rel-field-label-reserve,
948
+ calc(var(--rel-body-sm-medium-line-height) + var(--rel-spacing-2xs))
949
+ );
950
+ margin-bottom: calc(-1 * var(--rel-spacing-2xs));
951
+ }
952
+
906
953
  /* Floating label variant */
907
954
  :host([floatinglabel]) .textfield-wrapper {
908
955
  gap: 0;
@@ -1279,6 +1326,12 @@ RelTextfield.styles = css3`
1279
1326
  box-sizing: border-box;
1280
1327
  }
1281
1328
 
1329
+ /* labelStyle="none" + legend: nothing to seat in the notch, so drop the
1330
+ legend entirely and let the top border run continuous. */
1331
+ :host([label-style="none"][variant="legend"]) .field-legend {
1332
+ display: none;
1333
+ }
1334
+
1282
1335
  /* Prefix icon (legend): vertically centered; input inset via has-left-icon */
1283
1336
  :host([variant="legend"]) .icon.left {
1284
1337
  top: 50%;
@@ -1394,9 +1447,18 @@ __decorateClass([
1394
1447
  __decorateClass([
1395
1448
  property3({ type: String })
1396
1449
  ], RelTextfield.prototype, "size", 2);
1450
+ __decorateClass([
1451
+ property3({ type: String, attribute: "label-style", reflect: true })
1452
+ ], RelTextfield.prototype, "labelStyle", 2);
1397
1453
  __decorateClass([
1398
1454
  property3({ type: Boolean, reflect: true })
1399
1455
  ], RelTextfield.prototype, "floatingLabel", 2);
1456
+ __decorateClass([
1457
+ property3({ type: String, attribute: "field-aria-label" })
1458
+ ], RelTextfield.prototype, "fieldAriaLabel", 2);
1459
+ __decorateClass([
1460
+ property3({ type: String, attribute: "field-aria-labelledby" })
1461
+ ], RelTextfield.prototype, "fieldAriaLabelledby", 2);
1400
1462
  __decorateClass([
1401
1463
  property3({ type: String, reflect: true })
1402
1464
  ], RelTextfield.prototype, "variant", 2);
@@ -1506,12 +1568,26 @@ var RelAutocomplete = class extends LitElement4 {
1506
1568
  if (left + t.width > window.innerWidth - 8) {
1507
1569
  left = Math.max(8, window.innerWidth - t.width - 8);
1508
1570
  }
1571
+ this.panelEl.style.top = `${top}px`;
1572
+ this.panelEl.style.left = `${left}px`;
1573
+ this.panelEl.style.width = `${t.width}px`;
1574
+ const applied = this.panelEl.getBoundingClientRect();
1575
+ top -= applied.top - top;
1576
+ left -= applied.left - left;
1577
+ this.panelEl.style.top = `${top}px`;
1578
+ this.panelEl.style.left = `${left}px`;
1509
1579
  this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
1510
1580
  };
1511
1581
  }
1582
+ /** Name used for the suggestion panel — the visible label, or fieldAriaLabel
1583
+ * once labelStyle="none" hides it. Mirrors rel-textfield's own resolution. */
1584
+ get accessibleName() {
1585
+ return (this.labelStyle === "none" ? this.fieldAriaLabel : this.label) || "";
1586
+ }
1512
1587
  disconnectedCallback() {
1513
1588
  super.disconnectedCallback();
1514
1589
  this.removeGlobalListeners();
1590
+ this.hideNativePopover();
1515
1591
  clearTimeout(this.debounceTimer);
1516
1592
  }
1517
1593
  willUpdate(changed) {
@@ -1571,13 +1647,40 @@ var RelAutocomplete = class extends LitElement4 {
1571
1647
  this.open = true;
1572
1648
  this.highlightedIndex = -1;
1573
1649
  this.addGlobalListeners();
1574
- requestAnimationFrame(() => this.updatePosition());
1650
+ requestAnimationFrame(() => {
1651
+ this.showNativePopover();
1652
+ this.updatePosition();
1653
+ });
1575
1654
  }
1576
1655
  closePanel() {
1577
1656
  if (!this.open) return;
1578
1657
  this.open = false;
1579
1658
  this.highlightedIndex = -1;
1580
1659
  this.removeGlobalListeners();
1660
+ this.hideNativePopover();
1661
+ }
1662
+ /* A transformed ancestor (e.g. rel-modal's centered panel with
1663
+ * translate(-50%,-50%)) becomes the containing block for position:fixed
1664
+ * descendants, breaking viewport-based coordinates and letting the
1665
+ * panel's overflow:hidden clip the suggestions. Promoting the panel to
1666
+ * the browser top layer sidesteps both — same fix as rel-popover.
1667
+ * `manual` keeps light-dismiss and Escape handling under this
1668
+ * component's own control. */
1669
+ showNativePopover() {
1670
+ const p = this.panelEl;
1671
+ if (!p || typeof p.showPopover !== "function") return;
1672
+ try {
1673
+ if (!p.matches(":popover-open")) p.showPopover();
1674
+ } catch {
1675
+ }
1676
+ }
1677
+ hideNativePopover() {
1678
+ const p = this.panelEl;
1679
+ if (!p || typeof p.hidePopover !== "function") return;
1680
+ try {
1681
+ if (p.matches(":popover-open")) p.hidePopover();
1682
+ } catch {
1683
+ }
1581
1684
  }
1582
1685
  /** Re-evaluates whether the panel should be showing for the current state. */
1583
1686
  syncPanel() {
@@ -1613,17 +1716,17 @@ var RelAutocomplete = class extends LitElement4 {
1613
1716
  this.syncPanel();
1614
1717
  }
1615
1718
  }
1616
- async runSearch(query10) {
1719
+ async runSearch(query12) {
1617
1720
  const seq = ++this.searchSeq;
1618
1721
  try {
1619
- const results = await this.searchFn(query10);
1722
+ const results = await this.searchFn(query12);
1620
1723
  if (seq !== this.searchSeq) return;
1621
1724
  this.remoteItems = results ?? [];
1622
1725
  } catch (err) {
1623
1726
  if (seq !== this.searchSeq) return;
1624
1727
  this.remoteItems = [];
1625
1728
  this.dispatchEvent(
1626
- new CustomEvent("rel-search-error", { detail: { query: query10, error: err }, bubbles: true, composed: true })
1729
+ new CustomEvent("rel-search-error", { detail: { query: query12, error: err }, bubbles: true, composed: true })
1627
1730
  );
1628
1731
  } finally {
1629
1732
  if (seq === this.searchSeq) {
@@ -1769,7 +1872,10 @@ var RelAutocomplete = class extends LitElement4 {
1769
1872
  .value="${this.inputText}"
1770
1873
  .size="${this.size}"
1771
1874
  .variant="${this.variant}"
1875
+ .labelStyle="${this.labelStyle}"
1772
1876
  .floatingLabel="${this.floatingLabel}"
1877
+ .fieldAriaLabel="${this.fieldAriaLabel}"
1878
+ .fieldAriaLabelledby="${this.fieldAriaLabelledby}"
1773
1879
  .prefixIcon="${this.prefixIcon}"
1774
1880
  .disabled="${this.disabled}"
1775
1881
  .readonly="${this.readonly}"
@@ -1790,8 +1896,9 @@ var RelAutocomplete = class extends LitElement4 {
1790
1896
  id="${panelId}"
1791
1897
  part="panel list"
1792
1898
  class="ac-panel"
1899
+ popover="manual"
1793
1900
  role="listbox"
1794
- aria-label="${this.label || "Suggestions"}"
1901
+ aria-label="${this.accessibleName || "Suggestions"}"
1795
1902
  style="${this.panelStyle}"
1796
1903
  ?data-open="${this.open}"
1797
1904
  >
@@ -1844,6 +1951,15 @@ RelAutocomplete.styles = css4`
1844
1951
  position: fixed;
1845
1952
  z-index: 1001;
1846
1953
  display: none;
1954
+ /* Reset native [popover] UA styles (the panel is promoted to the
1955
+ top layer so transformed ancestors — e.g. a centered rel-modal
1956
+ panel — can't hijack its containing block or clip it). */
1957
+ inset: auto;
1958
+ margin: 0;
1959
+ padding: 0;
1960
+ width: auto;
1961
+ height: auto;
1962
+ color: inherit;
1847
1963
  box-sizing: border-box;
1848
1964
  background: var(--rel-autocomplete-bg, var(--rel-bg-neutral));
1849
1965
  border: 1px solid var(--rel-autocomplete-menu-border, var(--rel-border-default));
@@ -1970,9 +2086,18 @@ __decorateClass([
1970
2086
  __decorateClass([
1971
2087
  property4({ type: String })
1972
2088
  ], RelAutocomplete.prototype, "variant", 2);
2089
+ __decorateClass([
2090
+ property4({ type: String, attribute: "label-style", reflect: true })
2091
+ ], RelAutocomplete.prototype, "labelStyle", 2);
1973
2092
  __decorateClass([
1974
2093
  property4({ type: Boolean, attribute: "floating-label" })
1975
2094
  ], RelAutocomplete.prototype, "floatingLabel", 2);
2095
+ __decorateClass([
2096
+ property4({ type: String, attribute: "field-aria-label" })
2097
+ ], RelAutocomplete.prototype, "fieldAriaLabel", 2);
2098
+ __decorateClass([
2099
+ property4({ type: String, attribute: "field-aria-labelledby" })
2100
+ ], RelAutocomplete.prototype, "fieldAriaLabelledby", 2);
1976
2101
  __decorateClass([
1977
2102
  property4({ type: String, attribute: "prefix-icon" })
1978
2103
  ], RelAutocomplete.prototype, "prefixIcon", 2);
@@ -2684,8 +2809,8 @@ RelButton.styles = css8`
2684
2809
  pointer-events:none on the HOST would make it transparent to the
2685
2810
  pointer and suppress the cursor entirely. */
2686
2811
  cursor: not-allowed;
2687
- /* The disabled tokens are full-contrast pairs (light: #0B0B0B on
2688
- #C2C2C2, dark: #FFF on #404040), so on their own they render a
2812
+ /* The disabled tokens are full-contrast pairs (light: near-black on
2813
+ gray-300, dark: white on gray-700), so on their own they render a
2689
2814
  crisp button that reads as enabled. Dulling happens here instead —
2690
2815
  same opacity mechanism the rest of the library uses. */
2691
2816
  opacity: 0.5;
@@ -4308,6 +4433,9 @@ var RelCheckbox = class extends LitElement13 {
4308
4433
  this.state = "default";
4309
4434
  this.status = "unchecked";
4310
4435
  this.value = "";
4436
+ this.size = "medium";
4437
+ this.density = "compact";
4438
+ this.alignment = "leading";
4311
4439
  this._onChange = () => {
4312
4440
  };
4313
4441
  this._onTouched = () => {
@@ -4319,6 +4447,17 @@ var RelCheckbox = class extends LitElement13 {
4319
4447
  set checked(val) {
4320
4448
  this.status = val ? "checked" : "unchecked";
4321
4449
  }
4450
+ willUpdate(changed) {
4451
+ if ((changed.has("alignment") || changed.has("density")) && this.alignment === "trailing" && this.density === "compact") {
4452
+ console.warn(
4453
+ '[rel-checkbox] density="compact" has no trailing layout; rendering comfortable density.'
4454
+ );
4455
+ }
4456
+ }
4457
+ /** Trailing has no compact layout in the spec — trailing wins. */
4458
+ get effectiveDensity() {
4459
+ return this.alignment === "trailing" ? "comfortable" : this.density;
4460
+ }
4322
4461
  toggle() {
4323
4462
  if (this.state === "disabled") return;
4324
4463
  this.status = this.status === "checked" ? "unchecked" : "checked";
@@ -4366,12 +4505,22 @@ var RelCheckbox = class extends LitElement13 {
4366
4505
  </svg>
4367
4506
  </span>
4368
4507
 
4369
- ${this.icon ? html13`<rel-icon name="${this.icon}" size="md" class="icon-prefix"></rel-icon>` : null}
4508
+ <span class="content">
4509
+ <span class="label-line">
4510
+ ${this.icon ? html13`<rel-icon
4511
+ name="${this.icon}"
4512
+ size="${this.size === "small" ? "sm" : "md"}"
4513
+ class="icon-prefix"
4514
+ ></rel-icon>` : null}
4370
4515
 
4371
- <span part="label" class="label">${this.label}</span>
4516
+ <span part="label" class="label">${this.label}</span>
4517
+ </span>
4518
+
4519
+ ${this.effectiveDensity === "comfortable" && this.supportingtext ? html13`<span part="helper" class="text">${this.supportingtext}</span>` : null}
4520
+ </span>
4372
4521
  </label>
4373
4522
 
4374
- ${this.supportingtext ? html13`<span part="helper" class="text">${this.supportingtext}</span>` : null}
4523
+ ${this.effectiveDensity === "compact" && this.supportingtext ? html13`<span part="helper" class="text text-below">${this.supportingtext}</span>` : null}
4375
4524
  </div>
4376
4525
  `;
4377
4526
  }
@@ -4404,6 +4553,29 @@ RelCheckbox.styles = css13`
4404
4553
  cursor: pointer;
4405
4554
  }
4406
4555
 
4556
+ /* Trailing alignment — box moves to the far edge, label stack to the
4557
+ start. row-reverse keeps the box first in DOM order (input + .box
4558
+ adjacency drives the focus ring) while space-between stretches the
4559
+ row to its container. */
4560
+ :host([alignment="trailing"]) .row {
4561
+ flex-direction: row-reverse;
4562
+ justify-content: space-between;
4563
+ }
4564
+
4565
+ /* Comfortable density — label and supporting text stack beside the box. */
4566
+ .content {
4567
+ display: flex;
4568
+ flex-direction: column;
4569
+ gap: var(--rel-spacing-3xs);
4570
+ min-width: 0;
4571
+ }
4572
+
4573
+ .label-line {
4574
+ display: flex;
4575
+ align-items: center;
4576
+ gap: var(--rel-spacing-xs);
4577
+ }
4578
+
4407
4579
  input {
4408
4580
  position: absolute;
4409
4581
  inset: 0;
@@ -4546,7 +4718,6 @@ RelCheckbox.styles = css13`
4546
4718
 
4547
4719
  .text {
4548
4720
  color: var(--rel-text-caption);
4549
- padding-left: 28px;
4550
4721
 
4551
4722
  font-family: var(--rel-body-sm-regular-font-family);
4552
4723
 
@@ -4559,6 +4730,44 @@ RelCheckbox.styles = css13`
4559
4730
  letter-spacing: var(--rel-body-sm-regular-letter-spacing);
4560
4731
 
4561
4732
  }
4733
+
4734
+ /* Compact density — supporting text sits under the whole row, indented
4735
+ past the box (box width + row gap, matching the pre-existing 28px). */
4736
+ .text-below {
4737
+ padding-left: 28px;
4738
+ }
4739
+
4740
+ /* Size variant (medium is the base scale above). */
4741
+ :host([size="small"]) .box {
4742
+ width: 14px;
4743
+ height: 14px;
4744
+ border-radius: 3px;
4745
+ }
4746
+
4747
+ :host([size="small"]) svg {
4748
+ width: 10px;
4749
+ height: 10px;
4750
+ }
4751
+
4752
+ :host([size="small"]) .label {
4753
+ font-family: var(--rel-body-md-regular-font-family);
4754
+ font-weight: var(--rel-body-md-regular-font-weight);
4755
+ font-size: var(--rel-body-md-regular-font-size);
4756
+ line-height: var(--rel-body-md-regular-line-height);
4757
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
4758
+ }
4759
+
4760
+ :host([size="small"]) .text {
4761
+ font-family: var(--rel-body-xs-regular-font-family);
4762
+ font-weight: var(--rel-body-xs-regular-font-weight);
4763
+ font-size: var(--rel-body-xs-regular-font-size);
4764
+ line-height: var(--rel-body-xs-regular-line-height);
4765
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
4766
+ }
4767
+
4768
+ :host([size="small"]) .text-below {
4769
+ padding-left: 24px;
4770
+ }
4562
4771
  `;
4563
4772
  __decorateClass([
4564
4773
  property13({ type: String })
@@ -4578,6 +4787,15 @@ __decorateClass([
4578
4787
  __decorateClass([
4579
4788
  property13({ type: String })
4580
4789
  ], RelCheckbox.prototype, "value", 2);
4790
+ __decorateClass([
4791
+ property13({ type: String, reflect: true })
4792
+ ], RelCheckbox.prototype, "size", 2);
4793
+ __decorateClass([
4794
+ property13({ type: String, reflect: true })
4795
+ ], RelCheckbox.prototype, "density", 2);
4796
+ __decorateClass([
4797
+ property13({ type: String, reflect: true })
4798
+ ], RelCheckbox.prototype, "alignment", 2);
4581
4799
  RelCheckbox = __decorateClass([
4582
4800
  customElement13("rel-checkbox")
4583
4801
  ], RelCheckbox);
@@ -5709,6 +5927,31 @@ var RelRangePicker = class extends LitElement15 {
5709
5927
  this.viewMonth = (/* @__PURE__ */ new Date()).getMonth();
5710
5928
  this.isSelectingRange = false;
5711
5929
  this.hoverDate = null;
5930
+ this.onReposition = () => {
5931
+ if (this.open && this.mode === "popover") this.updatePopoverPosition();
5932
+ };
5933
+ this.updatePopoverPosition = () => {
5934
+ const panel = this.popoverEl;
5935
+ if (!panel || !this.open || this.mode !== "popover") return;
5936
+ const t = this.getBoundingClientRect();
5937
+ const c = panel.getBoundingClientRect();
5938
+ let top = t.bottom + 4;
5939
+ if (top + c.height > window.innerHeight - 8) {
5940
+ top = Math.max(8, t.top - c.height - 4);
5941
+ }
5942
+ let left = t.left;
5943
+ if (left + c.width > window.innerWidth - 8) {
5944
+ left = Math.max(8, window.innerWidth - c.width - 8);
5945
+ }
5946
+ panel.style.position = "fixed";
5947
+ panel.style.top = `${top}px`;
5948
+ panel.style.left = `${left}px`;
5949
+ const applied = panel.getBoundingClientRect();
5950
+ top -= applied.top - top;
5951
+ left -= applied.left - left;
5952
+ panel.style.top = `${top}px`;
5953
+ panel.style.left = `${left}px`;
5954
+ };
5712
5955
  // ── Open / close ──
5713
5956
  this.handleClickOutside = (e) => {
5714
5957
  if (!this.open) return;
@@ -5751,14 +5994,64 @@ var RelRangePicker = class extends LitElement15 {
5751
5994
  super.disconnectedCallback();
5752
5995
  document.removeEventListener("click", this.handleClickOutside);
5753
5996
  document.removeEventListener("keydown", this.handleGlobalKeydown);
5997
+ window.removeEventListener("resize", this.onReposition);
5998
+ window.removeEventListener("scroll", this.onReposition, true);
5999
+ this.hideTopLayer();
5754
6000
  }
5755
- updated() {
6001
+ updated(changed) {
5756
6002
  this.style.width = this.width;
5757
6003
  this.style.minWidth = this.minWidth;
5758
6004
  this.style.maxWidth = this.maxWidth;
5759
6005
  this.style.height = this.height;
5760
6006
  this.style.minHeight = this.minHeight;
5761
6007
  this.style.maxHeight = this.maxHeight;
6008
+ if (changed.has("open")) {
6009
+ if (this.open) {
6010
+ this.showTopLayer();
6011
+ window.addEventListener("resize", this.onReposition);
6012
+ window.addEventListener("scroll", this.onReposition, true);
6013
+ } else {
6014
+ window.removeEventListener("resize", this.onReposition);
6015
+ window.removeEventListener("scroll", this.onReposition, true);
6016
+ this.hideTopLayer();
6017
+ }
6018
+ }
6019
+ }
6020
+ /* A transformed ancestor (e.g. rel-modal's centered panel with
6021
+ * translate(-50%,-50%)) becomes the containing block for position:fixed
6022
+ * descendants, and its overflow:hidden clips position:absolute ones —
6023
+ * either way the picker shell breaks inside a modal. Promoting shell
6024
+ * (and popup-mode backdrop) to the browser top layer sidesteps both —
6025
+ * same fix as rel-popover / rel-autocomplete / rel-select. `manual`
6026
+ * keeps outside-click and Escape handling under this component's own
6027
+ * control. While in the top layer the CSS anchoring (absolute, below
6028
+ * the trigger) is meaningless, so popover mode gets inline fixed
6029
+ * coordinates from updatePopoverPosition(). */
6030
+ showTopLayer() {
6031
+ for (const el of [this.mode === "popup" ? this.backdropEl : void 0, this.popoverEl]) {
6032
+ if (!el || typeof el.showPopover !== "function") continue;
6033
+ try {
6034
+ if (!el.matches(":popover-open")) el.showPopover();
6035
+ } catch {
6036
+ }
6037
+ }
6038
+ if (this.mode === "popover") {
6039
+ requestAnimationFrame(() => this.updatePopoverPosition());
6040
+ }
6041
+ }
6042
+ hideTopLayer() {
6043
+ for (const el of [this.popoverEl, this.backdropEl]) {
6044
+ if (!el || typeof el.hidePopover !== "function") continue;
6045
+ try {
6046
+ if (el.matches(":popover-open")) el.hidePopover();
6047
+ } catch {
6048
+ }
6049
+ }
6050
+ if (this.popoverEl) {
6051
+ this.popoverEl.style.position = "";
6052
+ this.popoverEl.style.top = "";
6053
+ this.popoverEl.style.left = "";
6054
+ }
5762
6055
  }
5763
6056
  toggleOpen() {
5764
6057
  if (this.disabled) return;
@@ -6114,10 +6407,11 @@ var RelRangePicker = class extends LitElement15 {
6114
6407
  </button>
6115
6408
  `}
6116
6409
 
6117
- ${isPopup && this.open ? html15`<div part="overlay" class="backdrop" @click=${this.cancelPopover}></div>` : nothing7}
6410
+ ${isPopup && this.open ? html15`<div part="overlay" class="backdrop" popover="manual" @click=${this.cancelPopover}></div>` : nothing7}
6118
6411
 
6119
6412
  <div
6120
6413
  part="panel" class="popover ${this.open ? "open" : ""} ${isPopup ? "popup" : ""}"
6414
+ popover="manual"
6121
6415
  role="dialog"
6122
6416
  aria-label="Select date range"
6123
6417
  >
@@ -6328,6 +6622,17 @@ RelRangePicker.styles = css15`
6328
6622
 
6329
6623
  /* ── Popover shell ── */
6330
6624
  .popover {
6625
+ /* Reset native [popover] UA styles first (the shell is promoted to
6626
+ the top layer so transformed / overflow:hidden ancestors — e.g. a
6627
+ centered rel-modal panel — can't hijack or clip it). While in the
6628
+ top layer, JS overrides the anchored position below with inline
6629
+ fixed coordinates; the CSS anchoring is the no-Popover-API
6630
+ fallback. */
6631
+ margin: 0;
6632
+ padding: 0;
6633
+ width: auto;
6634
+ height: auto;
6635
+ color: inherit;
6331
6636
  position: absolute;
6332
6637
  top: calc(100% + 4px);
6333
6638
  left: 0;
@@ -6352,6 +6657,13 @@ RelRangePicker.styles = css15`
6352
6657
  .backdrop {
6353
6658
  position: fixed;
6354
6659
  inset: 0;
6660
+ /* Reset native [popover] UA styles — the backdrop is promoted to the
6661
+ top layer right before the panel so the panel stacks above it. */
6662
+ margin: 0;
6663
+ padding: 0;
6664
+ width: auto;
6665
+ height: auto;
6666
+ border: none;
6355
6667
  z-index: 2147483646;
6356
6668
  background: rgba(0,0,0,0.4);
6357
6669
  }
@@ -6725,6 +7037,12 @@ __decorateClass([
6725
7037
  __decorateClass([
6726
7038
  query4(".trigger-button")
6727
7039
  ], RelRangePicker.prototype, "triggerButton", 2);
7040
+ __decorateClass([
7041
+ query4(".popover")
7042
+ ], RelRangePicker.prototype, "popoverEl", 2);
7043
+ __decorateClass([
7044
+ query4(".backdrop")
7045
+ ], RelRangePicker.prototype, "backdropEl", 2);
6728
7046
  RelRangePicker = __decorateClass([
6729
7047
  customElement15("rel-range-picker")
6730
7048
  ], RelRangePicker);
@@ -8230,6 +8548,8 @@ var RelRadio = class extends LitElement23 {
8230
8548
  super(...arguments);
8231
8549
  this.options = [];
8232
8550
  this.name = "radio-group";
8551
+ this.orientation = "vertical";
8552
+ this.size = "medium";
8233
8553
  this.selectedValue = null;
8234
8554
  this.disabled = false;
8235
8555
  this.error = false;
@@ -8295,7 +8615,12 @@ var RelRadio = class extends LitElement23 {
8295
8615
  }
8296
8616
  render() {
8297
8617
  return html23`
8298
- <div part="base" class="rel-radio-group">
8618
+ <div
8619
+ part="base"
8620
+ class="rel-radio-group"
8621
+ role="radiogroup"
8622
+ aria-orientation="${this.orientation}"
8623
+ >
8299
8624
  ${this.options.map((o) => this.renderOption(o))}
8300
8625
  </div>
8301
8626
  ${this.error && this.errorMessage ? html23`<div class="error-message">${this.errorMessage}</div>` : ""}
@@ -8316,6 +8641,15 @@ RelRadio.styles = css23`
8316
8641
  gap: var(--rel-spacing-xs);
8317
8642
  }
8318
8643
 
8644
+ /* Horizontal orientation — options flow in a wrapping row. Wider column
8645
+ gap than the vertical rhythm so adjacent option labels read as separate
8646
+ choices; row gap keeps the vertical value for wrapped lines. */
8647
+ :host([orientation="horizontal"]) .rel-radio-group {
8648
+ flex-direction: row;
8649
+ flex-wrap: wrap;
8650
+ gap: var(--rel-spacing-xs) var(--rel-spacing-xl);
8651
+ }
8652
+
8319
8653
  .rel-radio-option {
8320
8654
  display: inline-flex;
8321
8655
  flex-direction: column;
@@ -8418,6 +8752,34 @@ RelRadio.styles = css23`
8418
8752
 
8419
8753
  }
8420
8754
 
8755
+ /* Size variant (medium is the base scale above). Supporting-text indent =
8756
+ circle width + the radio-main gap, so it stays aligned under the label. */
8757
+ :host([size="small"]) .radio-custom {
8758
+ width: 16px;
8759
+ height: 16px;
8760
+ }
8761
+
8762
+ :host([size="small"]) input:checked + .radio-custom::after {
8763
+ inset: 3px;
8764
+ }
8765
+
8766
+ :host([size="small"]) .radio-label {
8767
+ font-family: var(--rel-body-md-regular-font-family);
8768
+ font-weight: var(--rel-body-md-regular-font-weight);
8769
+ font-size: var(--rel-body-md-regular-font-size);
8770
+ line-height: var(--rel-body-md-regular-line-height);
8771
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
8772
+ }
8773
+
8774
+ :host([size="small"]) .radio-supporting {
8775
+ margin-left: 24px;
8776
+ font-family: var(--rel-body-xs-regular-font-family);
8777
+ font-weight: var(--rel-body-xs-regular-font-weight);
8778
+ font-size: var(--rel-body-xs-regular-font-size);
8779
+ line-height: var(--rel-body-xs-regular-line-height);
8780
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
8781
+ }
8782
+
8421
8783
  /* Error state (Figma State=Error, both Is Checked values) */
8422
8784
  .rel-radio-option.error-unchecked .radio-custom,
8423
8785
  .rel-radio-option.error-checked .radio-custom {
@@ -8461,6 +8823,12 @@ __decorateClass([
8461
8823
  __decorateClass([
8462
8824
  property22({ type: String })
8463
8825
  ], RelRadio.prototype, "name", 2);
8826
+ __decorateClass([
8827
+ property22({ type: String, reflect: true })
8828
+ ], RelRadio.prototype, "orientation", 2);
8829
+ __decorateClass([
8830
+ property22({ type: String, reflect: true })
8831
+ ], RelRadio.prototype, "size", 2);
8464
8832
  __decorateClass([
8465
8833
  property22({ type: String, reflect: true })
8466
8834
  ], RelRadio.prototype, "selectedValue", 2);
@@ -11208,7 +11576,7 @@ RelOtp = __decorateClass([
11208
11576
  ], RelOtp);
11209
11577
 
11210
11578
  // src/components/rel-pagination/rel-pagination.ts
11211
- import { LitElement as LitElement38, html as html38, css as css35 } from "lit";
11579
+ import { LitElement as LitElement38, html as html38, css as css35, nothing as nothing9 } from "lit";
11212
11580
  import { customElement as customElement38, property as property32, state as state14 } from "lit/decorators.js";
11213
11581
  var RelPagination = class extends LitElement38 {
11214
11582
  constructor() {
@@ -11279,6 +11647,14 @@ var RelPagination = class extends LitElement38 {
11279
11647
  }
11280
11648
  return pages;
11281
11649
  }
11650
+ /** Abbreviate large page numbers (368178 → "368k") so buttons stay compact.
11651
+ Exact below 10,000; floored to k up to 1M; then M with one decimal. */
11652
+ formatPageLabel(n) {
11653
+ if (n < 1e4) return String(n);
11654
+ if (n < 1e6) return `${Math.floor(n / 1e3)}k`;
11655
+ const m = Math.floor(n / 1e5) / 10;
11656
+ return `${Number.isInteger(m) ? m.toFixed(0) : m}M`;
11657
+ }
11282
11658
  handlePageChange(newPage) {
11283
11659
  const total = this.computedTotalPages;
11284
11660
  if (this.disabled || newPage < 1 || newPage > total || newPage === this.activePage) return;
@@ -11324,15 +11700,20 @@ var RelPagination = class extends LitElement38 {
11324
11700
  <div class="pag-stack">
11325
11701
  ${this.renderNavBtn("prev")}
11326
11702
  <div class="pag-numbers">
11327
- ${pages.map(
11328
- (p) => p === -1 ? html38`<span class="pag-ellipsis">...</span>` : html38`<button
11329
- type="button"
11330
- class="pag-page-btn ${Math.abs(p - current) <= 1 ? "near" : ""} ${p === 1 || p === total ? "edge" : ""}"
11331
- aria-label="Page ${p}"
11332
- aria-current="${p === current ? "page" : "false"}"
11333
- @click="${() => this.handlePageChange(p)}"
11334
- >${p}</button>`
11335
- )}
11703
+ ${pages.map((p) => {
11704
+ if (p === -1) return html38`<span class="pag-ellipsis">...</span>`;
11705
+ const isNear = Math.abs(p - current) <= 1;
11706
+ const isEdge = p === 1 || p === total;
11707
+ const label = isEdge && !isNear ? this.formatPageLabel(p) : String(p);
11708
+ return html38`<button
11709
+ type="button"
11710
+ class="pag-page-btn ${isNear ? "near" : ""} ${p === 1 || p === total ? "edge" : ""}"
11711
+ aria-label="Page ${p}"
11712
+ title="${label === String(p) ? nothing9 : `Page ${p}`}"
11713
+ aria-current="${p === current ? "page" : "false"}"
11714
+ @click="${() => this.handlePageChange(p)}"
11715
+ >${label}</button>`;
11716
+ })}
11336
11717
  </div>
11337
11718
  ${this.renderNavBtn("next")}
11338
11719
  </div>
@@ -11485,9 +11866,10 @@ RelPagination.styles = css35`
11485
11866
  display: inline-flex;
11486
11867
  align-items: center;
11487
11868
  justify-content: center;
11488
- width: 40px;
11869
+ width: auto;
11870
+ min-width: 40px;
11489
11871
  height: 40px;
11490
- padding: var(--rel-spacing-xs);
11872
+ padding: 0 var(--rel-spacing-xs);
11491
11873
  border-radius: var(--rel-border-radius-sm);
11492
11874
  border: 1px solid var(--rel-border-default);
11493
11875
  background: var(--rel-bg-neutral);
@@ -13020,7 +13402,7 @@ RelSegmentedButton = __decorateClass([
13020
13402
  ], RelSegmentedButton);
13021
13403
 
13022
13404
  // src/components/rel-select/rel-select.ts
13023
- import { LitElement as LitElement46, html as html46, css as css43, nothing as nothing9 } from "lit";
13405
+ import { LitElement as LitElement46, html as html46, css as css43, nothing as nothing10 } from "lit";
13024
13406
  import { customElement as customElement46, property as property40, state as state16, query as query8 } from "lit/decorators.js";
13025
13407
  import { repeat as repeat2 } from "lit/directives/repeat.js";
13026
13408
  import "@reliability-design/icons/rel-icon";
@@ -13060,6 +13442,7 @@ var RelSelect = class extends LitElement46 {
13060
13442
  this.internalState = "default";
13061
13443
  this.visibleChipCount = Infinity;
13062
13444
  this.uniqueId = `select-${Math.random().toString(36).slice(2)}`;
13445
+ this._warnedNoName = false;
13063
13446
  this.onOutsidePointerDown = (e) => {
13064
13447
  if (!this.open) return;
13065
13448
  const path = e.composedPath();
@@ -13089,6 +13472,14 @@ var RelSelect = class extends LitElement46 {
13089
13472
  if (left + t.width > window.innerWidth - 8) {
13090
13473
  left = Math.max(8, window.innerWidth - t.width - 8);
13091
13474
  }
13475
+ this.listboxEl.style.top = `${top}px`;
13476
+ this.listboxEl.style.left = `${left}px`;
13477
+ this.listboxEl.style.width = `${t.width}px`;
13478
+ const applied = this.listboxEl.getBoundingClientRect();
13479
+ top -= applied.top - top;
13480
+ left -= applied.left - left;
13481
+ this.listboxEl.style.top = `${top}px`;
13482
+ this.listboxEl.style.left = `${left}px`;
13092
13483
  this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
13093
13484
  };
13094
13485
  this.onSearchInput = (e) => {
@@ -13106,6 +13497,7 @@ var RelSelect = class extends LitElement46 {
13106
13497
  disconnectedCallback() {
13107
13498
  super.disconnectedCallback();
13108
13499
  this.removeGlobalListeners();
13500
+ this.hideNativePopover();
13109
13501
  this.chipResizeObserver?.disconnect();
13110
13502
  }
13111
13503
  firstUpdated() {
@@ -13118,6 +13510,13 @@ var RelSelect = class extends LitElement46 {
13118
13510
  }
13119
13511
  }
13120
13512
  updated(changed) {
13513
+ if (!this._warnedNoName && this.labelStyle === "none" && !this.fieldAriaLabel && !this.fieldAriaLabelledby && !this.getAttribute("aria-label")) {
13514
+ this._warnedNoName = true;
13515
+ console.warn(
13516
+ '<rel-select label-style="none"> has no accessible name. Set `field-aria-label` or `field-aria-labelledby` \u2014 a placeholder is not a valid naming mechanism.',
13517
+ this
13518
+ );
13519
+ }
13121
13520
  if (changed.has("selectedValues") || changed.has("options") || changed.has("multiple") || changed.has("size")) {
13122
13521
  requestAnimationFrame(() => this.computeVisibleChips());
13123
13522
  }
@@ -13215,6 +13614,7 @@ var RelSelect = class extends LitElement46 {
13215
13614
  this.highlightedIndex = -1;
13216
13615
  this.addGlobalListeners();
13217
13616
  requestAnimationFrame(() => {
13617
+ this.showNativePopover();
13218
13618
  this.updatePosition();
13219
13619
  if (!this.noSearch) {
13220
13620
  this.searchInputEl?.focus({ preventScroll: true });
@@ -13225,8 +13625,32 @@ var RelSelect = class extends LitElement46 {
13225
13625
  if (!this.open) return;
13226
13626
  this.open = false;
13227
13627
  this.removeGlobalListeners();
13628
+ this.hideNativePopover();
13228
13629
  this.triggerEl?.focus();
13229
13630
  }
13631
+ /* A transformed ancestor (e.g. rel-modal's centered panel with
13632
+ * translate(-50%,-50%)) becomes the containing block for position:fixed
13633
+ * descendants, breaking viewport-based coordinates and letting the
13634
+ * panel's overflow:hidden clip the listbox. Promoting the listbox to
13635
+ * the browser top layer sidesteps both — same fix as rel-popover /
13636
+ * rel-autocomplete. `manual` keeps light-dismiss and Escape handling
13637
+ * under this component's own control. */
13638
+ showNativePopover() {
13639
+ const p = this.listboxEl;
13640
+ if (!p || typeof p.showPopover !== "function") return;
13641
+ try {
13642
+ if (!p.matches(":popover-open")) p.showPopover();
13643
+ } catch {
13644
+ }
13645
+ }
13646
+ hideNativePopover() {
13647
+ const p = this.listboxEl;
13648
+ if (!p || typeof p.hidePopover !== "function") return;
13649
+ try {
13650
+ if (p.matches(":popover-open")) p.hidePopover();
13651
+ } catch {
13652
+ }
13653
+ }
13230
13654
  selectSingleOption(option) {
13231
13655
  this.selected = option.value;
13232
13656
  if (this.state === "error") this.state = "default";
@@ -13331,7 +13755,7 @@ var RelSelect = class extends LitElement46 {
13331
13755
  `;
13332
13756
  }
13333
13757
  )}
13334
- ${overflow > 0 ? html46`<rel-chip label="+${overflow}" size="sm" variant="fill" border="default"></rel-chip>` : nothing9}
13758
+ ${overflow > 0 ? html46`<rel-chip label="+${overflow}" size="sm" variant="fill" border="default"></rel-chip>` : nothing10}
13335
13759
  </span>
13336
13760
  `;
13337
13761
  }
@@ -13366,7 +13790,9 @@ var RelSelect = class extends LitElement46 {
13366
13790
  const opts = this.normalizedOptions;
13367
13791
  const filtered = this.filteredOptions;
13368
13792
  const isDisabled = this.state === "disabled";
13793
+ const labelHidden = this.labelStyle === "none";
13369
13794
  const showFloatingChrome = this.labelStyle === "floating" && !!this.label;
13795
+ const accessibleName = (labelHidden ? this.fieldAriaLabel : this.label) || "";
13370
13796
  const wrapperClasses = [
13371
13797
  "select-wrapper",
13372
13798
  this.size,
@@ -13382,7 +13808,7 @@ var RelSelect = class extends LitElement46 {
13382
13808
  const selectedSingle = opts.find((o) => o.value === this.selected);
13383
13809
  return html46`
13384
13810
  <div class="${wrapperClasses}">
13385
- ${!showFloatingChrome && this.label ? html46`<label part="label" class="select-label" for="${this.uniqueId}">${this.label}</label>` : ""}
13811
+ ${labelHidden ? html46`<div class="label-reserve" aria-hidden="true"></div>` : !showFloatingChrome && this.label ? html46`<label part="label" class="select-label" for="${this.uniqueId}">${this.label}</label>` : ""}
13386
13812
 
13387
13813
  <div class="select-container">
13388
13814
  ${showFloatingChrome ? html46`<span class="floating-label${this.leftIcon ? " has-left-icon" : ""}"
@@ -13399,7 +13825,8 @@ var RelSelect = class extends LitElement46 {
13399
13825
  aria-controls="${listboxId}"
13400
13826
  aria-activedescendant="${activeOptionId}"
13401
13827
  aria-invalid="${this.state === "error"}"
13402
- aria-label="${this.label || this.placeholder}"
13828
+ aria-label="${this.fieldAriaLabelledby ? nothing10 : accessibleName || this.placeholder}"
13829
+ aria-labelledby="${this.fieldAriaLabelledby ?? nothing10}"
13403
13830
  ?disabled="${isDisabled}"
13404
13831
  @click="${this.onTriggerClick}"
13405
13832
  @keydown="${this.onTriggerKeyDown}"
@@ -13423,7 +13850,7 @@ var RelSelect = class extends LitElement46 {
13423
13850
  ></rel-icon>
13424
13851
  </button>
13425
13852
 
13426
- ${this.multiple && this.selectedValues.length > 0 ? this.renderChipsMeasure() : nothing9}
13853
+ ${this.multiple && this.selectedValues.length > 0 ? this.renderChipsMeasure() : nothing10}
13427
13854
  </div>
13428
13855
 
13429
13856
  ${this.supportingText ? html46`<span part="helper" class="select-support ${this.state === "error" ? "error" : ""}">
@@ -13434,39 +13861,42 @@ var RelSelect = class extends LitElement46 {
13434
13861
  <div
13435
13862
  id="${listboxId}"
13436
13863
  part="panel list" class="select-listbox"
13864
+ popover="manual"
13437
13865
  role="listbox"
13438
13866
  aria-multiselectable="${this.multiple}"
13439
- aria-label="${this.label || "Options"}"
13867
+ aria-label="${accessibleName || "Options"}"
13440
13868
  style="${this.panelStyle}"
13441
13869
  ?data-open="${this.open}"
13442
13870
  >
13443
- ${!this.noSearch ? html46`<div class="select-search">
13444
- <div class="search-input-wrap">
13445
- <rel-icon name="search" size="${this.searchIconSize}" class="icon"></rel-icon>
13446
- <input
13447
- part="input" class="search-input"
13448
- type="text"
13449
- placeholder="${this.searchPlaceholder}"
13450
- .value="${this.searchValue}"
13451
- @input="${this.onSearchInput}"
13452
- @keydown="${(e) => this.onSearchKeyDown(e)}"
13453
- />
13454
- </div>
13871
+ ${!this.noSearch || this.multiple && filtered.length > 0 ? html46`<div class="select-header">
13872
+ ${!this.noSearch ? html46`<div class="select-search">
13873
+ <div class="search-input-wrap">
13874
+ <rel-icon name="search" size="${this.searchIconSize}" class="icon"></rel-icon>
13875
+ <input
13876
+ part="input" class="search-input"
13877
+ type="text"
13878
+ placeholder="${this.searchPlaceholder}"
13879
+ .value="${this.searchValue}"
13880
+ @input="${this.onSearchInput}"
13881
+ @keydown="${(e) => this.onSearchKeyDown(e)}"
13882
+ />
13883
+ </div>
13884
+ </div>` : ""}
13885
+ ${this.multiple && filtered.length > 0 ? html46`<div class="select-all-row">
13886
+ <rel-checkbox
13887
+ label="${this.selectAllLabel}"
13888
+ status="${this.selectAllStatus}"
13889
+ @rel-change=${(e) => {
13890
+ e.stopPropagation();
13891
+ this.toggleSelectAll();
13892
+ }}
13893
+ ></rel-checkbox>
13894
+ </div>` : ""}
13455
13895
  </div>` : ""}
13456
13896
 
13457
13897
  ${filtered.length === 0 ? html46`<div class="select-empty">
13458
13898
  ${opts.length === 0 ? "No options available" : "No matches found"}
13459
13899
  </div>` : this.multiple ? html46`
13460
- <div class="select-all-row">
13461
- <rel-checkbox
13462
- label="${this.selectAllLabel}"
13463
- status="${this.selectAllStatus}"
13464
- @rel-change=${(e) => {
13465
- e.stopPropagation();
13466
- this.toggleSelectAll();
13467
- }}
13468
- ></rel-checkbox>
13469
- </div>
13470
13900
  <div class="select-options">
13471
13901
  ${repeat2(
13472
13902
  filtered,
@@ -13513,7 +13943,7 @@ var RelSelect = class extends LitElement46 {
13513
13943
  name="${option.icon}"
13514
13944
  size="sm"
13515
13945
  class="select-option-icon"
13516
- ></rel-icon>` : nothing9}
13946
+ ></rel-icon>` : nothing10}
13517
13947
  <span class="select-option-label">${option.label}</span>
13518
13948
  </span>
13519
13949
  ${option.value === this.selected ? html46`<rel-icon name="check" size="sm" class="select-option-check"></rel-icon>` : ""}
@@ -13565,6 +13995,22 @@ RelSelect.styles = css43`
13565
13995
 
13566
13996
  }
13567
13997
 
13998
+ /* ── labelStyle="none" ──
13999
+ No label renders. An empty spacer holds the label row's full height so the
14000
+ trigger still bottom-aligns with labelled siblings in a form row.
14001
+ The reserve covers label line-height + the wrapper gap, and the negative
14002
+ margin cancels the flex gap that would otherwise sit below it — so
14003
+ --rel-field-label-reserve: 0 collapses the row to genuinely zero
14004
+ (toolbars, table cells), not just to the leftover gap. */
14005
+ .label-reserve {
14006
+ flex: none;
14007
+ height: var(
14008
+ --rel-field-label-reserve,
14009
+ calc(var(--rel-body-sm-regular-line-height) + var(--rel-spacing-2xs))
14010
+ );
14011
+ margin-bottom: calc(-1 * var(--rel-spacing-2xs));
14012
+ }
14013
+
13568
14014
  /* Error only colors the border/focus ring and the supporting text below
13569
14015
  the field — label, floating label, placeholder, and icons stay normal. */
13570
14016
 
@@ -13793,6 +14239,15 @@ RelSelect.styles = css43`
13793
14239
  position: fixed;
13794
14240
  z-index: 1001;
13795
14241
  display: none;
14242
+ /* Reset native [popover] UA styles (the listbox is promoted to the
14243
+ top layer so transformed ancestors — e.g. a centered rel-modal
14244
+ panel — can't hijack its containing block or clip it). */
14245
+ inset: auto;
14246
+ margin: 0;
14247
+ padding: 0;
14248
+ width: auto;
14249
+ height: auto;
14250
+ color: inherit;
13796
14251
  box-sizing: border-box;
13797
14252
  background: var(--rel-select-bg, var(--rel-bg-neutral));
13798
14253
  border: 1px solid var(--rel-select-menu-border, var(--rel-border-default));
@@ -13809,10 +14264,18 @@ RelSelect.styles = css43`
13809
14264
  display: block;
13810
14265
  }
13811
14266
 
13812
- .select-search {
14267
+ /* Search and Select All pin together while the options scroll beneath.
14268
+ z-index is load-bearing: multiselect rows contain rel-checkbox, whose
14269
+ .row wrapper is position:relative (Tab-scroll fix), so without it the
14270
+ positioned rows paint over the sticky header in DOM order. */
14271
+ .select-header {
13813
14272
  position: sticky;
13814
14273
  top: 0;
14274
+ z-index: 1;
13815
14275
  background: var(--rel-select-bg, var(--rel-bg-neutral));
14276
+ }
14277
+
14278
+ .select-search {
13816
14279
  padding: var(--rel-spacing-xs);
13817
14280
  border-bottom: 1px solid var(--rel-select-divider, var(--rel-border-subtle));
13818
14281
  }
@@ -13958,8 +14421,14 @@ __decorateClass([
13958
14421
  property40({ type: String })
13959
14422
  ], RelSelect.prototype, "label", 2);
13960
14423
  __decorateClass([
13961
- property40({ type: String, attribute: "label-style" })
14424
+ property40({ type: String, attribute: "label-style", reflect: true })
13962
14425
  ], RelSelect.prototype, "labelStyle", 2);
14426
+ __decorateClass([
14427
+ property40({ type: String, attribute: "field-aria-label" })
14428
+ ], RelSelect.prototype, "fieldAriaLabel", 2);
14429
+ __decorateClass([
14430
+ property40({ type: String, attribute: "field-aria-labelledby" })
14431
+ ], RelSelect.prototype, "fieldAriaLabelledby", 2);
13963
14432
  __decorateClass([
13964
14433
  property40({ type: Array })
13965
14434
  ], RelSelect.prototype, "options", 2);
@@ -16558,111 +17027,627 @@ RelTab = __decorateClass([
16558
17027
  customElement53("rel-tab")
16559
17028
  ], RelTab);
16560
17029
 
16561
- // src/components/rel-toast/rel-toast.ts
16562
- import { LitElement as LitElement54, html as html54, css as css51 } from "lit";
16563
- import { customElement as customElement54, property as property48, state as state21 } from "lit/decorators.js";
16564
- var RelToast = class extends LitElement54 {
17030
+ // src/components/rel-time-picker/rel-time-picker.ts
17031
+ import { LitElement as LitElement54, html as html54, css as css51, nothing as nothing11 } from "lit";
17032
+ import { customElement as customElement54, property as property48, state as state21, query as query10 } from "lit/decorators.js";
17033
+ import "@reliability-design/icons/rel-icon";
17034
+ var RelTimePicker = class extends LitElement54 {
16565
17035
  constructor() {
16566
17036
  super(...arguments);
16567
- this.message = "";
16568
- this.type = "default";
16569
- this.duration = 4e3;
16570
- this.show = false;
16571
- this.visible = false;
16572
- this._dismissTimer = null;
16573
- }
16574
- willUpdate(changed) {
16575
- if (!changed.has("show")) return;
16576
- if (this.show) {
16577
- this.visible = true;
16578
- if (this._dismissTimer) clearTimeout(this._dismissTimer);
16579
- if (this.duration > 0) {
16580
- this._dismissTimer = setTimeout(() => this.close(), this.duration);
16581
- }
16582
- } else {
16583
- this.visible = false;
16584
- if (this._dismissTimer) {
16585
- clearTimeout(this._dismissTimer);
16586
- this._dismissTimer = null;
17037
+ this.value = null;
17038
+ this.placeholder = "Select time";
17039
+ this.label = null;
17040
+ this.disabled = false;
17041
+ this.required = false;
17042
+ this.readonly = false;
17043
+ this.format = "12h";
17044
+ this.minuteStep = 1;
17045
+ this.minTime = null;
17046
+ this.maxTime = null;
17047
+ this.open = false;
17048
+ this.clearable = false;
17049
+ this.error = false;
17050
+ this.invalid = false;
17051
+ this.size = "medium";
17052
+ this.variant = "outlined";
17053
+ this.fullWidth = false;
17054
+ this.draftHour = null;
17055
+ this.draftMinute = null;
17056
+ this.handleClickOutside = (e) => {
17057
+ if (!this.open) return;
17058
+ const path = e.composedPath();
17059
+ if (!path.includes(this)) {
17060
+ this.cancelPopover();
16587
17061
  }
16588
- }
17062
+ };
17063
+ this.handleEscapeKey = (e) => {
17064
+ if (e.key === "Escape" && this.open) {
17065
+ this.cancelPopover();
17066
+ }
17067
+ };
17068
+ }
17069
+ connectedCallback() {
17070
+ super.connectedCallback();
17071
+ document.addEventListener("click", this.handleClickOutside);
17072
+ document.addEventListener("keydown", this.handleEscapeKey);
16589
17073
  }
16590
17074
  disconnectedCallback() {
16591
17075
  super.disconnectedCallback();
16592
- if (this._dismissTimer) clearTimeout(this._dismissTimer);
17076
+ document.removeEventListener("click", this.handleClickOutside);
17077
+ document.removeEventListener("keydown", this.handleEscapeKey);
16593
17078
  }
16594
- open() {
16595
- this.show = true;
17079
+ updated(changedProperties) {
17080
+ if (changedProperties.has("open") && this.open) {
17081
+ this.resetDraftFromValue();
17082
+ setTimeout(() => {
17083
+ this.scrollColumnsToSelection();
17084
+ const target = this.columnsElement?.querySelector(
17085
+ ".option-button.selected:not(:disabled)"
17086
+ ) ?? this.columnsElement?.querySelector(
17087
+ ".option-button:not(:disabled)"
17088
+ );
17089
+ target?.focus();
17090
+ }, 0);
17091
+ }
16596
17092
  }
16597
- close() {
16598
- this.show = false;
16599
- this.visible = false;
16600
- if (this._dismissTimer) {
16601
- clearTimeout(this._dismissTimer);
16602
- this._dismissTimer = null;
17093
+ // ── Parsing / formatting ──
17094
+ parseTime(time) {
17095
+ if (!time) return null;
17096
+ const match = time.match(/^(\d{1,2}):(\d{2})$/);
17097
+ if (!match) return null;
17098
+ const hour = parseInt(match[1], 10);
17099
+ const minute = parseInt(match[2], 10);
17100
+ if (hour > 23 || minute > 59) return null;
17101
+ return { hour, minute };
17102
+ }
17103
+ formatValue(hour, minute) {
17104
+ return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
17105
+ }
17106
+ formatDisplay(hour, minute) {
17107
+ const mm = String(minute).padStart(2, "0");
17108
+ if (this.format === "24h") {
17109
+ return `${String(hour).padStart(2, "0")}:${mm}`;
17110
+ }
17111
+ const period = hour < 12 ? "AM" : "PM";
17112
+ const hour12 = hour % 12 === 0 ? 12 : hour % 12;
17113
+ return `${hour12}:${mm} ${period}`;
17114
+ }
17115
+ toMinutes(hour, minute) {
17116
+ return hour * 60 + minute;
17117
+ }
17118
+ get minuteStepSafe() {
17119
+ const step = Math.floor(this.minuteStep);
17120
+ return step >= 1 && step <= 60 ? step : 1;
17121
+ }
17122
+ get minBoundMinutes() {
17123
+ const parsed = this.parseTime(this.minTime);
17124
+ return parsed ? this.toMinutes(parsed.hour, parsed.minute) : 0;
17125
+ }
17126
+ get maxBoundMinutes() {
17127
+ const parsed = this.parseTime(this.maxTime);
17128
+ return parsed ? this.toMinutes(parsed.hour, parsed.minute) : 23 * 60 + 59;
17129
+ }
17130
+ isTimeInRange(hour, minute) {
17131
+ const total = this.toMinutes(hour, minute);
17132
+ return total >= this.minBoundMinutes && total <= this.maxBoundMinutes;
17133
+ }
17134
+ /** Minutes shown in the minutes column (respecting minuteStep). */
17135
+ getMinuteOptions() {
17136
+ const minutes = [];
17137
+ for (let m = 0; m < 60; m += this.minuteStepSafe) {
17138
+ minutes.push(m);
17139
+ }
17140
+ return minutes;
17141
+ }
17142
+ /** An hour is enabled if at least one of its step minutes is in range. */
17143
+ isHourEnabled(hour) {
17144
+ return this.getMinuteOptions().some((m) => this.isTimeInRange(hour, m));
17145
+ }
17146
+ isPeriodEnabled(period) {
17147
+ const start = period === "AM" ? 0 : 12;
17148
+ for (let h = start; h < start + 12; h++) {
17149
+ if (this.isHourEnabled(h)) return true;
17150
+ }
17151
+ return false;
17152
+ }
17153
+ /** Hour column entries: display label + the 0–23 hour it maps to. */
17154
+ getHourOptions() {
17155
+ if (this.format === "24h") {
17156
+ return Array.from({ length: 24 }, (_, h) => ({
17157
+ label: String(h).padStart(2, "0"),
17158
+ hour24: h
17159
+ }));
17160
+ }
17161
+ const period = this.draftPeriod ?? "AM";
17162
+ const base = period === "AM" ? 0 : 12;
17163
+ return Array.from({ length: 12 }, (_, i) => ({
17164
+ label: i === 0 ? "12" : String(i).padStart(2, "0"),
17165
+ hour24: base + i
17166
+ }));
17167
+ }
17168
+ get draftPeriod() {
17169
+ if (this.draftHour === null) return null;
17170
+ return this.draftHour < 12 ? "AM" : "PM";
17171
+ }
17172
+ resetDraftFromValue() {
17173
+ const parsed = this.parseTime(this.value);
17174
+ this.draftHour = parsed ? parsed.hour : null;
17175
+ this.draftMinute = parsed ? parsed.minute : null;
17176
+ }
17177
+ /** Snap the draft minute to the nearest valid step minute for the hour. */
17178
+ clampDraftToRange() {
17179
+ if (this.draftHour === null || this.draftMinute === null) return;
17180
+ if (this.isTimeInRange(this.draftHour, this.draftMinute)) return;
17181
+ const hour = this.draftHour;
17182
+ const valid = this.getMinuteOptions().filter((m) => this.isTimeInRange(hour, m));
17183
+ if (valid.length === 0) {
17184
+ this.draftMinute = null;
17185
+ return;
16603
17186
  }
17187
+ const current = this.draftMinute;
17188
+ this.draftMinute = valid.reduce(
17189
+ (best, m) => Math.abs(m - current) < Math.abs(best - current) ? m : best
17190
+ );
17191
+ }
17192
+ // ── Open / close / commit ──
17193
+ toggleTimePopover() {
17194
+ if (this.disabled || this.readonly) return;
17195
+ this.open = !this.open;
16604
17196
  this.dispatchEvent(
16605
- new CustomEvent("dismissed", {
17197
+ new CustomEvent("rel-open-change", {
17198
+ detail: { open: this.open },
16606
17199
  bubbles: true,
16607
17200
  composed: true
16608
17201
  })
16609
17202
  );
16610
17203
  }
16611
- handleActionClick(event) {
16612
- event.stopPropagation();
17204
+ /** Close discarding the draft (Cancel / Escape / outside click). */
17205
+ cancelPopover() {
17206
+ this.open = false;
17207
+ this.triggerButton?.focus();
16613
17208
  this.dispatchEvent(
16614
- new CustomEvent("action", {
17209
+ new CustomEvent("rel-open-change", {
17210
+ detail: { open: false },
16615
17211
  bubbles: true,
16616
17212
  composed: true
16617
17213
  })
16618
17214
  );
16619
- this.close();
16620
17215
  }
17216
+ applySelection() {
17217
+ if (this.draftHour === null) return;
17218
+ const newValue = this.formatValue(this.draftHour, this.draftMinute ?? 0);
17219
+ this.value = newValue;
17220
+ this.dispatchEvent(
17221
+ new CustomEvent("rel-change", {
17222
+ detail: { value: newValue },
17223
+ bubbles: true,
17224
+ composed: true
17225
+ })
17226
+ );
17227
+ this.cancelPopover();
17228
+ }
17229
+ /** Set the draft to the current time, snapped to the minute step and range. */
17230
+ selectNow() {
17231
+ const now = /* @__PURE__ */ new Date();
17232
+ const step = this.minuteStepSafe;
17233
+ this.draftHour = now.getHours();
17234
+ this.draftMinute = Math.round(now.getMinutes() / step) * step;
17235
+ if (this.draftMinute >= 60) {
17236
+ this.draftMinute = 0;
17237
+ this.draftHour = (this.draftHour + 1) % 24;
17238
+ }
17239
+ if (!this.isHourEnabled(this.draftHour)) {
17240
+ const enabled = [];
17241
+ for (let h = 0; h < 24; h++) if (this.isHourEnabled(h)) enabled.push(h);
17242
+ if (enabled.length === 0) return;
17243
+ const currentHour = this.draftHour;
17244
+ this.draftHour = enabled.reduce(
17245
+ (best, h) => Math.abs(h - currentHour) < Math.abs(best - currentHour) ? h : best
17246
+ );
17247
+ }
17248
+ this.clampDraftToRange();
17249
+ this.scrollColumnsToSelection();
17250
+ }
17251
+ clearValue(e) {
17252
+ e.stopPropagation();
17253
+ this.value = null;
17254
+ this.draftHour = null;
17255
+ this.draftMinute = null;
17256
+ this.dispatchEvent(
17257
+ new CustomEvent("rel-change", {
17258
+ detail: { value: null },
17259
+ bubbles: true,
17260
+ composed: true
17261
+ })
17262
+ );
17263
+ }
17264
+ // ── Column selection ──
17265
+ selectHour(hour24) {
17266
+ if (!this.isHourEnabled(hour24)) return;
17267
+ this.draftHour = hour24;
17268
+ if (this.draftMinute === null) this.draftMinute = 0;
17269
+ this.clampDraftToRange();
17270
+ }
17271
+ selectMinute(minute) {
17272
+ if (this.draftHour === null) {
17273
+ for (let h = 0; h < 24; h++) {
17274
+ if (this.isHourEnabled(h)) {
17275
+ this.draftHour = h;
17276
+ break;
17277
+ }
17278
+ }
17279
+ if (this.draftHour === null) return;
17280
+ }
17281
+ if (!this.isTimeInRange(this.draftHour, minute)) return;
17282
+ this.draftMinute = minute;
17283
+ }
17284
+ selectPeriod(period) {
17285
+ if (!this.isPeriodEnabled(period)) return;
17286
+ const base = period === "AM" ? 0 : 12;
17287
+ const hour12 = this.draftHour === null ? 0 : this.draftHour % 12;
17288
+ this.draftHour = base + hour12;
17289
+ if (this.draftMinute === null) this.draftMinute = 0;
17290
+ if (!this.isHourEnabled(this.draftHour)) {
17291
+ const enabled = [];
17292
+ for (let h = base; h < base + 12; h++) if (this.isHourEnabled(h)) enabled.push(h);
17293
+ const currentHour = this.draftHour;
17294
+ this.draftHour = enabled.reduce(
17295
+ (best, h) => Math.abs(h - currentHour) < Math.abs(best - currentHour) ? h : best
17296
+ );
17297
+ }
17298
+ this.clampDraftToRange();
17299
+ this.scrollColumnsToSelection();
17300
+ }
17301
+ scrollColumnsToSelection() {
17302
+ this.updateComplete.then(() => {
17303
+ this.columnsElement?.querySelectorAll(".option-button.selected").forEach((btn) => btn.scrollIntoView({ block: "center" }));
17304
+ });
17305
+ }
17306
+ // ── Keyboard ──
17307
+ handleTriggerKeydown(e) {
17308
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
17309
+ e.preventDefault();
17310
+ if (!this.open) {
17311
+ this.toggleTimePopover();
17312
+ }
17313
+ }
17314
+ }
17315
+ /**
17316
+ * Column navigation: Up/Down move within the column (and select), Left/Right
17317
+ * jump between columns, Home/End go to the first/last option, Enter applies.
17318
+ */
17319
+ handleOptionKeydown(e, column) {
17320
+ const list = e.currentTarget.closest(".column-list");
17321
+ if (!list) return;
17322
+ const buttons = Array.from(
17323
+ list.querySelectorAll(".option-button:not(:disabled)")
17324
+ );
17325
+ const index = buttons.indexOf(e.currentTarget);
17326
+ let target;
17327
+ switch (e.key) {
17328
+ case "ArrowUp":
17329
+ e.preventDefault();
17330
+ target = buttons[Math.max(0, index - 1)];
17331
+ break;
17332
+ case "ArrowDown":
17333
+ e.preventDefault();
17334
+ target = buttons[Math.min(buttons.length - 1, index + 1)];
17335
+ break;
17336
+ case "Home":
17337
+ e.preventDefault();
17338
+ target = buttons[0];
17339
+ break;
17340
+ case "End":
17341
+ e.preventDefault();
17342
+ target = buttons[buttons.length - 1];
17343
+ break;
17344
+ case "ArrowLeft":
17345
+ case "ArrowRight": {
17346
+ e.preventDefault();
17347
+ this.focusAdjacentColumn(column, e.key === "ArrowRight" ? 1 : -1);
17348
+ return;
17349
+ }
17350
+ case "Enter":
17351
+ e.preventDefault();
17352
+ e.currentTarget.click();
17353
+ this.applySelection();
17354
+ return;
17355
+ default:
17356
+ return;
17357
+ }
17358
+ if (target && target !== buttons[index]) {
17359
+ target.click();
17360
+ target.focus();
17361
+ target.scrollIntoView({ block: "nearest" });
17362
+ }
17363
+ }
17364
+ focusAdjacentColumn(from, direction) {
17365
+ const order = this.format === "12h" ? ["hour", "minute", "period"] : ["hour", "minute"];
17366
+ const nextIndex = order.indexOf(from) + direction;
17367
+ if (nextIndex < 0 || nextIndex >= order.length) return;
17368
+ const lists = this.columnsElement?.querySelectorAll(".column-list");
17369
+ const list = lists?.[nextIndex];
17370
+ const target = list?.querySelector(".option-button.selected:not(:disabled)") ?? list?.querySelector(".option-button:not(:disabled)");
17371
+ target?.focus();
17372
+ target?.scrollIntoView({ block: "nearest" });
17373
+ }
17374
+ // ── Render ──
16621
17375
  render() {
17376
+ const isError = this.error || this.invalid;
17377
+ const belowText = isError && this.errorMessage ? this.errorMessage : this.helperText;
17378
+ const describedById = belowText ? "time-picker-desc" : void 0;
17379
+ const parsedValue = this.parseTime(this.value);
17380
+ const displayText = parsedValue ? this.formatDisplay(parsedValue.hour, parsedValue.minute) : "";
17381
+ const draftDisplay = this.draftHour !== null ? this.formatDisplay(this.draftHour, this.draftMinute ?? 0) : "";
17382
+ const hourOptions = this.getHourOptions();
17383
+ const minuteOptions = this.getMinuteOptions();
17384
+ const draftPeriod = this.draftPeriod;
17385
+ const wrapperClasses = [
17386
+ "time-picker-wrapper",
17387
+ this.size,
17388
+ isError ? "error" : "",
17389
+ this.readonly ? "readonly" : ""
17390
+ ].filter(Boolean).join(" ");
16622
17391
  return html54`
16623
- <div part="base" class="toast ${this.type} ${this.visible ? "" : "hidden"}" role="status" aria-live="polite">
16624
- <span part="label" class="toast-message">${this.message}</span>
17392
+ <div part="base" class="${wrapperClasses}">
17393
+ ${this.label ? html54`<label part="label" class="label" id="time-picker-label"
17394
+ >${this.label}${this.required ? html54`<span class="required-mark" aria-hidden="true">*</span>` : nothing11}</label
17395
+ >` : nothing11}
16625
17396
 
16626
- ${this.actionLabel ? html54`<button
16627
- part="action" class="toast-action"
16628
- @click="${this.handleActionClick}"
17397
+ ${this.name ? html54`<input type="hidden" name=${this.name} .value=${this.value ?? ""} />` : nothing11}
17398
+
17399
+ <button
17400
+ part="trigger"
17401
+ class="trigger-button ${isError ? "error" : ""}"
17402
+ type="button"
17403
+ ?disabled=${this.disabled}
17404
+ @click=${this.toggleTimePopover}
17405
+ @keydown=${this.handleTriggerKeydown}
17406
+ @focus=${() => this.dispatchEvent(
17407
+ new CustomEvent("rel-focus", { bubbles: true, composed: true })
17408
+ )}
17409
+ @blur=${() => this.dispatchEvent(
17410
+ new CustomEvent("rel-blur", { bubbles: true, composed: true })
17411
+ )}
17412
+ aria-label=${this.label || "Select time"}
17413
+ aria-haspopup="dialog"
17414
+ aria-expanded=${this.open}
17415
+ aria-required=${this.required}
17416
+ aria-invalid=${isError}
17417
+ aria-readonly=${this.readonly}
17418
+ aria-describedby=${describedById ?? nothing11}
17419
+ aria-labelledby=${this.label ? "time-picker-label" : nothing11}
17420
+ >
17421
+ <span part="value" class="trigger-content ${displayText ? "" : "placeholder"}">
17422
+ ${displayText || this.placeholder}
17423
+ </span>
17424
+ <span class="trigger-icons">
17425
+ ${this.clearable && this.value ? html54`
17426
+ <button
17427
+ class="icon-button"
17428
+ type="button"
17429
+ aria-label="Clear time"
17430
+ @click=${this.clearValue}
17431
+ >
17432
+ <svg viewBox="0 0 16 16" fill="currentColor">
17433
+ <path
17434
+ d="M4 4l8 8M12 4l-8 8"
17435
+ stroke="currentColor"
17436
+ stroke-width="2"
17437
+ stroke-linecap="round"
17438
+ />
17439
+ </svg>
17440
+ </button>
17441
+ ` : nothing11}
17442
+ <rel-icon part="icon" name="time-filled" size="sm"></rel-icon>
17443
+ </span>
17444
+ </button>
17445
+
17446
+ <div
17447
+ part="panel"
17448
+ class="popover ${this.open ? "open" : ""}"
17449
+ role="dialog"
17450
+ aria-label="Choose time"
17451
+ >
17452
+ <div class="time-readout">
17453
+ <span class="time-readout-label">Time</span>
17454
+ <span class="time-readout-value ${draftDisplay ? "" : "empty"}">
17455
+ ${draftDisplay || (this.format === "24h" ? "--:--" : "--:-- --")}
17456
+ </span>
17457
+ </div>
17458
+
17459
+ <div class="columns">
17460
+ <div class="column">
17461
+ <div class="column-header" aria-hidden="true">Hour</div>
17462
+ <div class="column-list" role="listbox" aria-label="Hours">
17463
+ ${hourOptions.map(
17464
+ (opt, i) => html54`
17465
+ <button
17466
+ part="option"
17467
+ class="option-button ${this.draftHour === opt.hour24 ? "selected" : ""}"
17468
+ type="button"
17469
+ role="option"
17470
+ aria-selected=${this.draftHour === opt.hour24}
17471
+ ?disabled=${!this.isHourEnabled(opt.hour24)}
17472
+ @click=${() => this.selectHour(opt.hour24)}
17473
+ @keydown=${(e) => this.handleOptionKeydown(e, "hour")}
17474
+ tabindex=${this.draftHour === opt.hour24 || this.draftHour === null && i === 0 ? 0 : -1}
17475
+ >
17476
+ ${opt.label}
17477
+ </button>
17478
+ `
17479
+ )}
17480
+ </div>
17481
+ </div>
17482
+
17483
+ <div class="column">
17484
+ <div class="column-header" aria-hidden="true">Min</div>
17485
+ <div class="column-list" role="listbox" aria-label="Minutes">
17486
+ ${minuteOptions.map(
17487
+ (m, i) => html54`
17488
+ <button
17489
+ part="option"
17490
+ class="option-button ${this.draftMinute === m ? "selected" : ""}"
17491
+ type="button"
17492
+ role="option"
17493
+ aria-selected=${this.draftMinute === m}
17494
+ ?disabled=${this.draftHour !== null && !this.isTimeInRange(this.draftHour, m)}
17495
+ @click=${() => this.selectMinute(m)}
17496
+ @keydown=${(e) => this.handleOptionKeydown(e, "minute")}
17497
+ tabindex=${this.draftMinute === m || this.draftMinute === null && i === 0 ? 0 : -1}
17498
+ >
17499
+ ${String(m).padStart(2, "0")}
17500
+ </button>
17501
+ `
17502
+ )}
17503
+ </div>
17504
+ </div>
17505
+
17506
+ ${this.format === "12h" ? html54`
17507
+ <div class="column">
17508
+ <div class="column-header" aria-hidden="true">AM/PM</div>
17509
+ <div class="column-list" role="listbox" aria-label="AM or PM">
17510
+ ${["AM", "PM"].map(
17511
+ (p) => html54`
17512
+ <button
17513
+ part="option"
17514
+ class="option-button ${draftPeriod === p ? "selected" : ""}"
17515
+ type="button"
17516
+ role="option"
17517
+ aria-selected=${draftPeriod === p}
17518
+ ?disabled=${!this.isPeriodEnabled(p)}
17519
+ @click=${() => this.selectPeriod(p)}
17520
+ @keydown=${(e) => this.handleOptionKeydown(e, "period")}
17521
+ tabindex=${draftPeriod === p || draftPeriod === null && p === "AM" ? 0 : -1}
17522
+ >
17523
+ ${p}
17524
+ </button>
17525
+ `
17526
+ )}
17527
+ </div>
17528
+ </div>
17529
+ ` : nothing11}
17530
+ </div>
17531
+
17532
+ <div part="footer" class="time-footer">
17533
+ <rel-button
17534
+ variant="secondary"
17535
+ size="small"
17536
+ border="corner-radius"
17537
+ @click=${() => this.selectNow()}
16629
17538
  >
16630
- ${this.actionLabel}
16631
- </button>` : ""}
17539
+ Now
17540
+ </rel-button>
17541
+ <div class="footer-right">
17542
+ <rel-button
17543
+ variant="secondary"
17544
+ size="small"
17545
+ border="corner-radius"
17546
+ @click=${() => this.cancelPopover()}
17547
+ >
17548
+ Cancel
17549
+ </rel-button>
17550
+ <rel-button
17551
+ variant="primary"
17552
+ tone="default"
17553
+ size="small"
17554
+ border="corner-radius"
17555
+ ?disabled=${this.draftHour === null}
17556
+ @click=${() => this.applySelection()}
17557
+ >
17558
+ Apply
17559
+ </rel-button>
17560
+ </div>
17561
+ </div>
17562
+ </div>
16632
17563
 
16633
- <button part="close-button" class="toast-close" @click="${this.close}" aria-label="Dismiss">✕</button>
17564
+ ${belowText ? html54`<div
17565
+ part="helper"
17566
+ class="time-picker-support ${isError ? "error" : ""}"
17567
+ id=${describedById}
17568
+ >
17569
+ ${belowText}
17570
+ </div>` : nothing11}
16634
17571
  </div>
16635
17572
  `;
16636
17573
  }
16637
17574
  };
16638
- RelToast.styles = css51`
17575
+ RelTimePicker.styles = css51`
16639
17576
  :host {
16640
17577
  color: var(--rel-text-default);
16641
- position: fixed;
16642
- bottom: 24px;
16643
- left: 50%;
16644
- transform: translateX(-50%);
16645
- display: flex;
16646
- justify-content: center;
16647
- width: 100%;
16648
- pointer-events: none;
16649
- z-index: var(--rel-z-toast, 9000);
17578
+ display: block;
16650
17579
  font-family: var(--rel-fonts-font-family);
17580
+ position: relative;
16651
17581
  }
16652
17582
 
16653
- /* Toast container */
16654
- .toast {
16655
- display: inline-flex;
16656
- align-items: center;
17583
+ /* Intrinsic width by default; set fullWidth to fill the parent. */
17584
+ :host(:not([fullwidth])) {
17585
+ display: inline-block;
17586
+ width: auto;
17587
+ }
17588
+
17589
+ :host([disabled]) {
17590
+ opacity: 0.6;
17591
+ cursor: not-allowed;
17592
+ }
17593
+
17594
+ :host([disabled]) * {
17595
+ pointer-events: none;
17596
+ }
17597
+
17598
+ :host([readonly]) .trigger-button {
17599
+ cursor: default;
17600
+ }
17601
+
17602
+ .time-picker-wrapper {
17603
+ display: flex;
17604
+ flex-direction: column;
16657
17605
  gap: var(--rel-spacing-xs);
16658
- background-color: var(--rel-bg-neutral-inverse-1);
16659
- color: var(--rel-text-inverse);
17606
+ }
16660
17607
 
16661
- border-radius: var(--rel-border-radius-sm);
17608
+ .label {
17609
+ color: var(--rel-text-label);
17610
+ font-family: var(--rel-body-sm-medium-font-family);
17611
+ font-weight: var(--rel-body-sm-medium-font-weight);
17612
+ font-size: var(--rel-body-sm-medium-font-size);
17613
+ line-height: var(--rel-body-sm-medium-line-height);
17614
+ letter-spacing: var(--rel-body-sm-medium-letter-spacing);
17615
+ }
17616
+
17617
+ .required-mark {
17618
+ color: var(--rel-text-danger);
17619
+ margin-left: 2px;
17620
+ }
17621
+
17622
+ /* Support text (helperText / errorMessage) */
17623
+ .time-picker-support {
17624
+ color: var(--rel-text-placeholder);
17625
+ font-family: var(--rel-body-xs-regular-font-family);
17626
+ font-weight: var(--rel-body-xs-regular-font-weight);
17627
+ font-size: var(--rel-body-xs-regular-font-size);
17628
+ line-height: var(--rel-body-xs-regular-line-height);
17629
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
17630
+ }
17631
+
17632
+ .time-picker-support.error {
17633
+ color: var(--rel-text-danger);
17634
+ }
17635
+
17636
+ .trigger-button {
17637
+ width: 100%;
17638
+ min-width: 160px;
17639
+ display: flex;
17640
+ align-items: center;
17641
+ justify-content: space-between;
17642
+ gap: var(--rel-spacing-xs);
16662
17643
  padding: var(--rel-spacing-sm) var(--rel-spacing-md);
16663
- box-shadow: var(--rel-shadows-shadow-300);
16664
- pointer-events: all;
16665
- animation: fadeInUp 0.25s ease;
17644
+ background: var(--rel-bg-surface);
17645
+ border: 1px solid var(--rel-border-default);
17646
+ border-radius: var(--rel-border-radius-sm);
17647
+ color: var(--rel-text-default);
17648
+ cursor: pointer;
17649
+ transition: all 0.15s ease;
17650
+ text-align: left;
16666
17651
  font-family: var(--rel-body-md-regular-font-family);
16667
17652
  font-weight: var(--rel-body-md-regular-font-weight);
16668
17653
  font-size: var(--rel-body-md-regular-font-size);
@@ -16670,276 +17655,1605 @@ RelToast.styles = css51`
16670
17655
  letter-spacing: var(--rel-body-md-regular-letter-spacing);
16671
17656
  }
16672
17657
 
16673
- .toast.error {
16674
- background-color: var(--rel-bg-danger);
16675
- color: var(--rel-text-static-light);
17658
+ .trigger-button:hover:not(:disabled) {
17659
+ border-color: var(--rel-border-strong);
17660
+ }
17661
+
17662
+ .trigger-button:focus-visible {
17663
+ outline: 2px solid var(--rel-border-focus);
17664
+ outline-offset: 2px;
17665
+ border-color: var(--rel-border-focus);
17666
+ }
17667
+
17668
+ .trigger-button:disabled {
17669
+ cursor: not-allowed;
17670
+ background: var(--rel-bg-surface-elevated);
17671
+ }
17672
+
17673
+ /* Error state */
17674
+ .trigger-button.error,
17675
+ .trigger-button.error:hover:not(:disabled) {
17676
+ border-color: var(--rel-border-danger);
17677
+ }
17678
+
17679
+ .trigger-button.error:focus-visible {
17680
+ outline-color: var(--rel-border-danger);
17681
+ border-color: var(--rel-border-danger);
17682
+ }
17683
+
17684
+ /* Size variants (medium is the base .trigger-button style) */
17685
+ .time-picker-wrapper.small .trigger-button {
17686
+ padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
17687
+ font-family: var(--rel-body-sm-regular-font-family);
17688
+ font-weight: var(--rel-body-sm-regular-font-weight);
17689
+ font-size: var(--rel-body-sm-regular-font-size);
17690
+ line-height: var(--rel-body-sm-regular-line-height);
17691
+ letter-spacing: var(--rel-body-sm-regular-letter-spacing);
17692
+ }
17693
+
17694
+ .time-picker-wrapper.large .trigger-button {
17695
+ padding: var(--rel-spacing-md) var(--rel-spacing-md);
17696
+ font-family: var(--rel-body-lg-regular-font-family);
17697
+ font-weight: var(--rel-body-lg-regular-font-weight);
17698
+ font-size: var(--rel-body-lg-regular-font-size);
17699
+ line-height: var(--rel-body-lg-regular-line-height);
17700
+ letter-spacing: var(--rel-body-lg-regular-letter-spacing);
17701
+ }
17702
+
17703
+ .trigger-content {
17704
+ flex: 1;
17705
+ color: var(--rel-text-default);
17706
+ }
17707
+
17708
+ .trigger-content.placeholder {
17709
+ color: var(--rel-text-placeholder);
17710
+ }
17711
+
17712
+ .trigger-icons {
17713
+ display: flex;
17714
+ align-items: center;
17715
+ gap: var(--rel-spacing-2xs);
17716
+ color: var(--rel-text-placeholder);
17717
+ }
17718
+
17719
+ .icon-button {
17720
+ background: none;
17721
+ border: none;
17722
+ padding: var(--rel-spacing-3xs);
17723
+ cursor: pointer;
17724
+ color: var(--rel-text-placeholder);
17725
+ display: flex;
17726
+ align-items: center;
17727
+ justify-content: center;
17728
+ border-radius: var(--rel-border-radius-xs);
17729
+ transition: all 0.15s ease;
17730
+ }
17731
+
17732
+ .icon-button:hover {
17733
+ background: var(--rel-bg-surface-elevated);
17734
+ color: var(--rel-text-default);
17735
+ }
17736
+
17737
+ .icon-button svg {
17738
+ width: 16px;
17739
+ height: 16px;
17740
+ }
17741
+
17742
+ .popover {
17743
+ position: absolute;
17744
+ top: calc(100% + 4px);
17745
+ left: 0;
17746
+ z-index: 1000;
17747
+ background: var(--rel-bg-surface);
17748
+ border: 1px solid var(--rel-border-default);
17749
+ border-radius: var(--rel-border-radius-sm);
17750
+ box-shadow: var(--rel-shadows-shadow-300);
17751
+ padding: var(--rel-spacing-md);
17752
+ display: none;
17753
+ }
17754
+
17755
+ .popover.open {
17756
+ display: block;
17757
+ }
17758
+
17759
+ /* Draft readout (mirrors the range-picker FROM/TO block) */
17760
+ .time-readout {
17761
+ display: flex;
17762
+ align-items: baseline;
17763
+ gap: var(--rel-spacing-sm);
17764
+ padding-bottom: var(--rel-spacing-sm);
17765
+ margin-bottom: var(--rel-spacing-sm);
17766
+ border-bottom: 1px solid var(--rel-border-subtle);
17767
+ }
17768
+
17769
+ .time-readout-label {
17770
+ color: var(--rel-text-placeholder);
17771
+ text-transform: uppercase;
17772
+ font-family: var(--rel-body-xs-semibold-font-family);
17773
+ font-weight: var(--rel-body-xs-semibold-font-weight);
17774
+ font-size: var(--rel-body-xs-semibold-font-size);
17775
+ line-height: var(--rel-body-xs-semibold-line-height);
17776
+ letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
17777
+ }
17778
+
17779
+ .time-readout-value {
17780
+ color: var(--rel-text-default);
17781
+ font-family: var(--rel-body-md-semibold-font-family);
17782
+ font-weight: var(--rel-body-md-semibold-font-weight);
17783
+ font-size: var(--rel-body-md-semibold-font-size);
17784
+ line-height: var(--rel-body-md-semibold-line-height);
17785
+ letter-spacing: var(--rel-body-md-semibold-letter-spacing);
17786
+ }
17787
+
17788
+ .time-readout-value.empty {
17789
+ color: var(--rel-text-placeholder);
17790
+ }
17791
+
17792
+ .columns {
17793
+ display: flex;
17794
+ gap: var(--rel-spacing-xs);
17795
+ }
17796
+
17797
+ /* Columns share the panel width equally, so the two-column 24h layout
17798
+ fills the same footprint the footer dictates instead of bunching left. */
17799
+ .column {
17800
+ display: flex;
17801
+ flex-direction: column;
17802
+ flex: 1;
17803
+ min-width: 64px;
17804
+ }
17805
+
17806
+ .column-header {
17807
+ text-align: center;
17808
+ color: var(--rel-text-placeholder);
17809
+ padding: var(--rel-spacing-xs);
17810
+ text-transform: uppercase;
17811
+ font-family: var(--rel-body-xs-semibold-font-family);
17812
+ font-weight: var(--rel-body-xs-semibold-font-weight);
17813
+ font-size: var(--rel-body-xs-semibold-font-size);
17814
+ line-height: var(--rel-body-xs-semibold-line-height);
17815
+ letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
17816
+ }
17817
+
17818
+ .column-list {
17819
+ display: flex;
17820
+ flex-direction: column;
17821
+ gap: var(--rel-spacing-3xs);
17822
+ max-height: 224px;
17823
+ overflow-y: auto;
17824
+ padding: var(--rel-spacing-3xs);
17825
+ scrollbar-width: thin;
17826
+ }
17827
+
17828
+ .option-button {
17829
+ border: none;
17830
+ background: transparent;
17831
+ border-radius: var(--rel-border-radius-sm);
17832
+ color: var(--rel-text-default);
17833
+ cursor: pointer;
17834
+ padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
17835
+ text-align: center;
17836
+ flex-shrink: 0;
17837
+ transition: all 0.15s ease;
17838
+ font-family: var(--rel-body-sm-medium-font-family);
17839
+ font-weight: var(--rel-body-sm-medium-font-weight);
17840
+ font-size: var(--rel-body-sm-medium-font-size);
17841
+ line-height: var(--rel-body-sm-medium-line-height);
17842
+ letter-spacing: var(--rel-body-sm-medium-letter-spacing);
17843
+ }
17844
+
17845
+ .option-button:hover:not(:disabled):not(.selected) {
17846
+ background: var(--rel-bg-surface-elevated);
17847
+ }
17848
+
17849
+ .option-button:focus-visible {
17850
+ outline: 2px solid var(--rel-border-focus);
17851
+ outline-offset: -2px;
17852
+ z-index: 1;
17853
+ }
17854
+
17855
+ .option-button.selected {
17856
+ background: var(--rel-action-brand-fill);
17857
+ color: var(--rel-action-brand-label-icons-primary);
17858
+ font-weight: var(--rel-body-md-semibold-font-weight);
17859
+ }
17860
+
17861
+ .option-button:disabled {
17862
+ color: var(--rel-text-disabled);
17863
+ cursor: not-allowed;
17864
+ opacity: 0.5;
17865
+ }
17866
+
17867
+ .option-button:disabled:hover {
17868
+ background: transparent;
17869
+ }
17870
+
17871
+ /* Footer (mirrors the range-picker calendar-footer) */
17872
+ .time-footer {
17873
+ display: flex;
17874
+ align-items: center;
17875
+ justify-content: space-between;
17876
+ gap: var(--rel-spacing-sm);
17877
+ padding-top: var(--rel-spacing-sm);
17878
+ margin-top: var(--rel-spacing-sm);
17879
+ border-top: 1px solid var(--rel-border-subtle);
17880
+ }
17881
+
17882
+ .footer-right {
17883
+ display: flex;
17884
+ align-items: center;
17885
+ gap: var(--rel-spacing-xs);
17886
+ }
17887
+
17888
+ @media (prefers-reduced-motion: reduce) {
17889
+ .trigger-button,
17890
+ .icon-button,
17891
+ .option-button {
17892
+ transition: none;
17893
+ }
17894
+ }
17895
+ `;
17896
+ __decorateClass([
17897
+ property48({ type: String, reflect: true })
17898
+ ], RelTimePicker.prototype, "value", 2);
17899
+ __decorateClass([
17900
+ property48({ type: String })
17901
+ ], RelTimePicker.prototype, "placeholder", 2);
17902
+ __decorateClass([
17903
+ property48({ type: String })
17904
+ ], RelTimePicker.prototype, "label", 2);
17905
+ __decorateClass([
17906
+ property48({ type: Boolean, reflect: true })
17907
+ ], RelTimePicker.prototype, "disabled", 2);
17908
+ __decorateClass([
17909
+ property48({ type: Boolean })
17910
+ ], RelTimePicker.prototype, "required", 2);
17911
+ __decorateClass([
17912
+ property48({ type: Boolean, reflect: true })
17913
+ ], RelTimePicker.prototype, "readonly", 2);
17914
+ __decorateClass([
17915
+ property48({ type: String })
17916
+ ], RelTimePicker.prototype, "format", 2);
17917
+ __decorateClass([
17918
+ property48({ type: Number, attribute: "minute-step" })
17919
+ ], RelTimePicker.prototype, "minuteStep", 2);
17920
+ __decorateClass([
17921
+ property48({ type: String, attribute: "min-time" })
17922
+ ], RelTimePicker.prototype, "minTime", 2);
17923
+ __decorateClass([
17924
+ property48({ type: String, attribute: "max-time" })
17925
+ ], RelTimePicker.prototype, "maxTime", 2);
17926
+ __decorateClass([
17927
+ property48({ type: Boolean, reflect: true })
17928
+ ], RelTimePicker.prototype, "open", 2);
17929
+ __decorateClass([
17930
+ property48({ type: Boolean })
17931
+ ], RelTimePicker.prototype, "clearable", 2);
17932
+ __decorateClass([
17933
+ property48({ type: Boolean })
17934
+ ], RelTimePicker.prototype, "error", 2);
17935
+ __decorateClass([
17936
+ property48({ type: Boolean })
17937
+ ], RelTimePicker.prototype, "invalid", 2);
17938
+ __decorateClass([
17939
+ property48({ type: String, attribute: "error-message" })
17940
+ ], RelTimePicker.prototype, "errorMessage", 2);
17941
+ __decorateClass([
17942
+ property48({ type: String, attribute: "helper-text" })
17943
+ ], RelTimePicker.prototype, "helperText", 2);
17944
+ __decorateClass([
17945
+ property48({ type: String })
17946
+ ], RelTimePicker.prototype, "name", 2);
17947
+ __decorateClass([
17948
+ property48({ type: String })
17949
+ ], RelTimePicker.prototype, "size", 2);
17950
+ __decorateClass([
17951
+ property48({ type: String, reflect: true })
17952
+ ], RelTimePicker.prototype, "variant", 2);
17953
+ __decorateClass([
17954
+ property48({ type: Boolean, reflect: true })
17955
+ ], RelTimePicker.prototype, "fullWidth", 2);
17956
+ __decorateClass([
17957
+ state21()
17958
+ ], RelTimePicker.prototype, "draftHour", 2);
17959
+ __decorateClass([
17960
+ state21()
17961
+ ], RelTimePicker.prototype, "draftMinute", 2);
17962
+ __decorateClass([
17963
+ query10(".trigger-button")
17964
+ ], RelTimePicker.prototype, "triggerButton", 2);
17965
+ __decorateClass([
17966
+ query10(".columns")
17967
+ ], RelTimePicker.prototype, "columnsElement", 2);
17968
+ RelTimePicker = __decorateClass([
17969
+ customElement54("rel-time-picker")
17970
+ ], RelTimePicker);
17971
+
17972
+ // src/components/rel-toast/rel-toast.ts
17973
+ import { LitElement as LitElement55, html as html55, css as css52 } from "lit";
17974
+ import { customElement as customElement55, property as property49, state as state22 } from "lit/decorators.js";
17975
+ var RelToast = class extends LitElement55 {
17976
+ constructor() {
17977
+ super(...arguments);
17978
+ this.message = "";
17979
+ this.type = "default";
17980
+ this.duration = 4e3;
17981
+ this.show = false;
17982
+ this.visible = false;
17983
+ this._dismissTimer = null;
17984
+ }
17985
+ willUpdate(changed) {
17986
+ if (!changed.has("show")) return;
17987
+ if (this.show) {
17988
+ this.visible = true;
17989
+ if (this._dismissTimer) clearTimeout(this._dismissTimer);
17990
+ if (this.duration > 0) {
17991
+ this._dismissTimer = setTimeout(() => this.close(), this.duration);
17992
+ }
17993
+ } else {
17994
+ this.visible = false;
17995
+ if (this._dismissTimer) {
17996
+ clearTimeout(this._dismissTimer);
17997
+ this._dismissTimer = null;
17998
+ }
17999
+ }
18000
+ }
18001
+ disconnectedCallback() {
18002
+ super.disconnectedCallback();
18003
+ if (this._dismissTimer) clearTimeout(this._dismissTimer);
18004
+ }
18005
+ open() {
18006
+ this.show = true;
18007
+ }
18008
+ close() {
18009
+ this.show = false;
18010
+ this.visible = false;
18011
+ if (this._dismissTimer) {
18012
+ clearTimeout(this._dismissTimer);
18013
+ this._dismissTimer = null;
18014
+ }
18015
+ this.dispatchEvent(
18016
+ new CustomEvent("dismissed", {
18017
+ bubbles: true,
18018
+ composed: true
18019
+ })
18020
+ );
18021
+ }
18022
+ handleActionClick(event) {
18023
+ event.stopPropagation();
18024
+ this.dispatchEvent(
18025
+ new CustomEvent("action", {
18026
+ bubbles: true,
18027
+ composed: true
18028
+ })
18029
+ );
18030
+ this.close();
18031
+ }
18032
+ render() {
18033
+ return html55`
18034
+ <div part="base" class="toast ${this.type} ${this.visible ? "" : "hidden"}" role="status" aria-live="polite">
18035
+ <span part="label" class="toast-message">${this.message}</span>
18036
+
18037
+ ${this.actionLabel ? html55`<button
18038
+ part="action" class="toast-action"
18039
+ @click="${this.handleActionClick}"
18040
+ >
18041
+ ${this.actionLabel}
18042
+ </button>` : ""}
18043
+
18044
+ <button part="close-button" class="toast-close" @click="${this.close}" aria-label="Dismiss">✕</button>
18045
+ </div>
18046
+ `;
18047
+ }
18048
+ };
18049
+ RelToast.styles = css52`
18050
+ :host {
18051
+ color: var(--rel-text-default);
18052
+ position: fixed;
18053
+ bottom: 24px;
18054
+ left: 50%;
18055
+ transform: translateX(-50%);
18056
+ display: flex;
18057
+ justify-content: center;
18058
+ width: 100%;
18059
+ pointer-events: none;
18060
+ z-index: var(--rel-z-toast, 9000);
18061
+ font-family: var(--rel-fonts-font-family);
18062
+ }
18063
+
18064
+ /* Toast container */
18065
+ .toast {
18066
+ display: inline-flex;
18067
+ align-items: center;
18068
+ gap: var(--rel-spacing-xs);
18069
+ background-color: var(--rel-bg-neutral-inverse-1);
18070
+ color: var(--rel-text-inverse);
18071
+
18072
+ border-radius: var(--rel-border-radius-sm);
18073
+ padding: var(--rel-spacing-sm) var(--rel-spacing-md);
18074
+ box-shadow: var(--rel-shadows-shadow-300);
18075
+ pointer-events: all;
18076
+ animation: fadeInUp 0.25s ease;
18077
+ font-family: var(--rel-body-md-regular-font-family);
18078
+ font-weight: var(--rel-body-md-regular-font-weight);
18079
+ font-size: var(--rel-body-md-regular-font-size);
18080
+ line-height: var(--rel-body-md-regular-line-height);
18081
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
18082
+ }
18083
+
18084
+ .toast.error {
18085
+ background-color: var(--rel-bg-danger);
18086
+ color: var(--rel-text-static-light);
18087
+ }
18088
+
18089
+ .toast.hidden {
18090
+ display: none;
18091
+ }
18092
+
18093
+ /* Message */
18094
+ .toast-message {
18095
+ flex: 1;
18096
+ }
18097
+
18098
+ /* Action button */
18099
+ .toast-action {
18100
+ background: transparent;
18101
+ border: none;
18102
+ color: var(--rel-action-neutral-label-icons-fill-inverse);
18103
+ font-family: var(--rel-action-md-font-family);
18104
+ font-weight: var(--rel-action-md-font-weight);
18105
+ font-size: var(--rel-action-md-font-size);
18106
+ line-height: var(--rel-action-md-line-height);
18107
+ letter-spacing: var(--rel-action-md-letter-spacing);
18108
+ cursor: pointer;
18109
+ transition: opacity 0.2s;
18110
+ }
18111
+
18112
+ .toast-action:hover {
18113
+ background: var(--rel-bg-neutral);
18114
+ color: var(--rel-text-default);
18115
+ opacity: 0.75;
18116
+ }
18117
+
18118
+ .toast-action:focus-visible {
18119
+ outline: 2px solid var(--rel-text-static-light);
18120
+ outline-offset: 2px;
18121
+ border-radius: var(--rel-border-radius-xs);
18122
+ }
18123
+
18124
+ /* Close button */
18125
+ .toast-close {
18126
+ background: transparent;
18127
+ color: var(--rel-action-neutral-label-icons-ghost);
18128
+ border: none;
18129
+ color: inherit;
18130
+ font-size: 1rem;
18131
+ cursor: pointer;
18132
+ line-height: 1;
18133
+ padding: var(--rel-spacing-2xs);
18134
+ margin-left: var(--rel-spacing-2xs);
18135
+ }
18136
+
18137
+ .toast-close:hover {
18138
+ opacity: 0.75;
18139
+ }
18140
+
18141
+ .toast-close:focus-visible {
18142
+ outline: 2px solid var(--rel-text-static-light);
18143
+ outline-offset: 2px;
18144
+ border-radius: var(--rel-border-radius-xs);
18145
+ }
18146
+
18147
+
18148
+ @media (prefers-reduced-motion: reduce) {
18149
+ .toast {
18150
+ animation: none;
18151
+ }
18152
+ }
18153
+
18154
+ /* Animations */
18155
+ @keyframes fadeInUp {
18156
+ from {
18157
+ opacity: 0;
18158
+ transform: translateY(20px);
18159
+ }
18160
+ to {
18161
+ opacity: 1;
18162
+ transform: translateY(0);
18163
+ }
18164
+ }
18165
+ `;
18166
+ __decorateClass([
18167
+ property49({ type: String })
18168
+ ], RelToast.prototype, "message", 2);
18169
+ __decorateClass([
18170
+ property49({ type: String })
18171
+ ], RelToast.prototype, "type", 2);
18172
+ __decorateClass([
18173
+ property49({ type: String })
18174
+ ], RelToast.prototype, "actionLabel", 2);
18175
+ __decorateClass([
18176
+ property49({ type: Number })
18177
+ ], RelToast.prototype, "duration", 2);
18178
+ __decorateClass([
18179
+ property49({ type: Boolean, reflect: true })
18180
+ ], RelToast.prototype, "show", 2);
18181
+ __decorateClass([
18182
+ state22()
18183
+ ], RelToast.prototype, "visible", 2);
18184
+ RelToast = __decorateClass([
18185
+ customElement55("rel-toast")
18186
+ ], RelToast);
18187
+
18188
+ // src/components/rel-tooltip/rel-tooltip.ts
18189
+ import { LitElement as LitElement56, html as html56, css as css53 } from "lit";
18190
+ import { customElement as customElement56, property as property50 } from "lit/decorators.js";
18191
+ var RelTooltip = class extends LitElement56 {
18192
+ constructor() {
18193
+ super(...arguments);
18194
+ this.text = "";
18195
+ this.position = "above";
18196
+ this.showTail = false;
18197
+ this.type = "single-line";
18198
+ // Stable id so aria-describedby can reference the bubble across renders.
18199
+ this.uniqueId = Math.random().toString(36).slice(2);
18200
+ }
18201
+ firstUpdated() {
18202
+ this.linkTrigger();
18203
+ }
18204
+ linkTrigger() {
18205
+ const slot = this.shadowRoot?.querySelector("slot");
18206
+ if (!slot) return;
18207
+ const trigger = slot.assignedElements()[0];
18208
+ if (trigger) {
18209
+ trigger.setAttribute("aria-describedby", `tooltip-${this.uniqueId}`);
18210
+ }
18211
+ }
18212
+ render() {
18213
+ return html56`
18214
+ <slot @slotchange="${this.linkTrigger}"></slot>
18215
+ <div
18216
+ part="panel" class="tooltip-bubble ${this.position}"
18217
+ id="tooltip-${this.uniqueId}"
18218
+ role="tooltip"
18219
+ >
18220
+ ${this.text}
18221
+ ${this.showTail ? html56`<span part="indicator" class="tooltip-tail"></span>` : ""}
18222
+ </div>
18223
+ `;
18224
+ }
18225
+ };
18226
+ RelTooltip.styles = css53`
18227
+ :host {
18228
+ color: var(--rel-text-default);
18229
+ display: inline-block;
18230
+ position: relative;
18231
+ }
18232
+
18233
+ /* Tooltip container */
18234
+ .tooltip-bubble {
18235
+ font-family: var(--rel-body-md-regular-font-family);
18236
+ font-weight: var(--rel-body-md-medium-font-weight);
18237
+ font-size: var(--rel-body-md-regular-font-size);
18238
+ line-height: var(--rel-body-md-regular-line-height);
18239
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
18240
+ position: absolute;
18241
+ background-color: var(--rel-bg-neutral-inverse-2);
18242
+ color: var(--rel-text-inverse);
18243
+ padding: var(--rel-spacing-xs, 8px)
18244
+ var(--rel-spacing-sm, 12px);
18245
+ border-radius: var(--rel-border-radius-sm, 8px);
18246
+ white-space: nowrap;
18247
+ z-index: 1000;
18248
+ opacity: 0;
18249
+ pointer-events: none;
18250
+ transition: opacity 0.2s ease;
18251
+ overflow: visible;
18252
+ }
18253
+
18254
+ /* Figma Type=Multi Line: wrap inside a bounded bubble */
18255
+ :host([type="multi-line"]) .tooltip-bubble {
18256
+ white-space: normal;
18257
+ max-width: 220px;
18258
+ width: max-content;
18259
+ }
18260
+
18261
+ :host(:hover) .tooltip-bubble,
18262
+ :host(:focus-within) .tooltip-bubble {
18263
+ opacity: 1;
18264
+ }
18265
+
18266
+ /* Positioning */
18267
+ .tooltip-bubble.above {
18268
+ bottom: calc(100% + 6px);
18269
+ left: 50%;
18270
+ transform: translateX(-50%);
18271
+ }
18272
+
18273
+ .tooltip-bubble.below {
18274
+ top: calc(100% + 6px);
18275
+ left: 50%;
18276
+ transform: translateX(-50%);
18277
+ }
18278
+
18279
+ .tooltip-bubble.left {
18280
+ right: calc(100% + 6px);
18281
+ top: 50%;
18282
+ transform: translateY(-50%);
18283
+ }
18284
+
18285
+ .tooltip-bubble.right {
18286
+ left: calc(100% + 6px);
18287
+ top: 50%;
18288
+ transform: translateY(-50%);
18289
+ }
18290
+
18291
+ /* Tail/Arrow */
18292
+ .tooltip-tail {
18293
+ position: absolute;
18294
+ width: 0;
18295
+ height: 0;
18296
+ display: block;
18297
+ }
18298
+
18299
+ .tooltip-bubble.above .tooltip-tail,
18300
+ .tooltip-bubble.below .tooltip-tail {
18301
+ left: 50%;
18302
+ transform: translateX(-50%);
18303
+ border-left: 6px solid transparent;
18304
+ border-right: 6px solid transparent;
18305
+ }
18306
+
18307
+ .tooltip-bubble.above .tooltip-tail {
18308
+ bottom: -6px;
18309
+ border-top: 6px solid var(--rel-bg-neutral-inverse-2);
18310
+ border-bottom: none;
18311
+ }
18312
+
18313
+ .tooltip-bubble.below .tooltip-tail {
18314
+ top: -6px;
18315
+ border-bottom: 6px solid var(--rel-bg-neutral-inverse-2);
18316
+ border-top: none;
18317
+ }
18318
+
18319
+ .tooltip-bubble.left .tooltip-tail,
18320
+ .tooltip-bubble.right .tooltip-tail {
18321
+ top: 50%;
18322
+ transform: translateY(-50%);
18323
+ border-top: 6px solid transparent;
18324
+ border-bottom: 6px solid transparent;
18325
+ }
18326
+
18327
+ .tooltip-bubble.left .tooltip-tail {
18328
+ right: -6px;
18329
+ border-left: 6px solid var(--rel-bg-neutral-inverse-2);
18330
+ border-right: none;
18331
+ }
18332
+
18333
+ .tooltip-bubble.right .tooltip-tail {
18334
+ left: -6px;
18335
+ border-right: 6px solid var(--rel-bg-neutral-inverse-2);
18336
+ border-left: none;
18337
+ }
18338
+ `;
18339
+ __decorateClass([
18340
+ property50({ type: String })
18341
+ ], RelTooltip.prototype, "text", 2);
18342
+ __decorateClass([
18343
+ property50({ type: String })
18344
+ ], RelTooltip.prototype, "position", 2);
18345
+ __decorateClass([
18346
+ property50({ type: Boolean, attribute: "show-tail" })
18347
+ ], RelTooltip.prototype, "showTail", 2);
18348
+ __decorateClass([
18349
+ property50({ type: String, reflect: true })
18350
+ ], RelTooltip.prototype, "type", 2);
18351
+ RelTooltip = __decorateClass([
18352
+ customElement56("rel-tooltip")
18353
+ ], RelTooltip);
18354
+
18355
+ // src/components/rel-upload/rel-upload.ts
18356
+ import { LitElement as LitElement57, html as html57, css as css54, nothing as nothing12 } from "lit";
18357
+ import { customElement as customElement57, property as property51, state as state23, query as query11 } from "lit/decorators.js";
18358
+ import "@reliability-design/icons/rel-icon";
18359
+ var uid = 0;
18360
+ var RelUpload = class extends LitElement57 {
18361
+ constructor() {
18362
+ super(...arguments);
18363
+ this.accept = "";
18364
+ this.maxSize = "";
18365
+ this.multiple = false;
18366
+ this.disabled = false;
18367
+ this.hint = "";
18368
+ this.dropzoneTitle = "Drag and drop your files here";
18369
+ this.browseLabel = "Browse files";
18370
+ this.files = [];
18371
+ this.concurrency = 3;
18372
+ this.delayThreshold = 15e3;
18373
+ this.dragOver = false;
18374
+ this.delayed = false;
18375
+ this.lastSignature = "";
18376
+ this.openPicker = () => {
18377
+ if (this.disabled) return;
18378
+ this.inputEl?.click();
18379
+ };
18380
+ this.onInputChange = (e) => {
18381
+ const input = e.target;
18382
+ this.ingest(input.files);
18383
+ input.value = "";
18384
+ };
18385
+ this.onDragOver = (e) => {
18386
+ if (this.disabled) return;
18387
+ e.preventDefault();
18388
+ this.dragOver = true;
18389
+ };
18390
+ this.onDragLeave = (e) => {
18391
+ e.preventDefault();
18392
+ this.dragOver = false;
18393
+ };
18394
+ this.onDrop = (e) => {
18395
+ e.preventDefault();
18396
+ this.dragOver = false;
18397
+ if (this.disabled) return;
18398
+ this.ingest(e.dataTransfer?.files ?? null);
18399
+ };
18400
+ }
18401
+ // ── derived ───────────────────────────────────────────────────────────────
18402
+ /**
18403
+ * Aggregate state. Read-only; corresponds to the Figma modal variants.
18404
+ *
18405
+ * `failed` means there is nothing left to send AND something errored — while
18406
+ * any row is still `ready` the primary action is Upload, not Retry.
18407
+ */
18408
+ get phase() {
18409
+ if (this.files.length === 0) return "empty";
18410
+ if (this.files.some((f) => f.status === "uploading")) {
18411
+ return this.delayed ? "delayed" : "uploading";
18412
+ }
18413
+ if (this.pending.length > 0) return "selected";
18414
+ if (this.files.some((f) => f.status === "error")) return "failed";
18415
+ return "selected";
18416
+ }
18417
+ /** Files still eligible to be sent. */
18418
+ get pending() {
18419
+ return this.files.filter((f) => f.status === "ready");
18420
+ }
18421
+ get failedFiles() {
18422
+ return this.files.filter((f) => f.status === "error");
18423
+ }
18424
+ /**
18425
+ * Failed rows worth sending again. A file rejected by `accept`/`max-size`
18426
+ * will fail validation every time, so retrying it is pointless — re-check
18427
+ * rather than re-queueing it blindly.
18428
+ */
18429
+ get retryable() {
18430
+ const maxBytes = this.parseMaxBytes();
18431
+ return this.failedFiles.filter(
18432
+ (f) => !!f.file && this.matchesAccept(f.file) && f.file.size <= maxBytes
18433
+ );
18434
+ }
18435
+ get canUpload() {
18436
+ if (this.disabled) return false;
18437
+ const phase = this.phase;
18438
+ if (phase === "uploading" || phase === "delayed") return false;
18439
+ if (phase === "failed") return this.retryable.length > 0;
18440
+ return this.pending.length > 0;
18441
+ }
18442
+ /** Footer button label — "Upload", "Upload 3 files" or "Retry upload". */
18443
+ get primaryLabel() {
18444
+ if (this.phase === "failed") return "Retry upload";
18445
+ const n = this.files.length;
18446
+ return n > 1 ? `Upload ${n} files` : "Upload";
18447
+ }
18448
+ /** The snapshot `<rel-upload-actions>` renders from. */
18449
+ get state() {
18450
+ return {
18451
+ phase: this.phase,
18452
+ total: this.files.length,
18453
+ failed: this.failedFiles.length,
18454
+ canUpload: this.canUpload,
18455
+ primaryLabel: this.primaryLabel
18456
+ };
18457
+ }
18458
+ get listHeading() {
18459
+ const n = this.files.length;
18460
+ switch (this.phase) {
18461
+ case "uploading":
18462
+ case "delayed":
18463
+ return `Uploading ${n} ${n === 1 ? "file" : "files"}`;
18464
+ case "failed":
18465
+ return `${n} ${n === 1 ? "file" : "files"} - ${this.failedFiles.length} failed`;
18466
+ default:
18467
+ return n === 1 ? "Selected file (1)" : `Selected files (${n})`;
18468
+ }
18469
+ }
18470
+ get hintText() {
18471
+ if (this.hint) return this.hint;
18472
+ const formats = this.acceptDisplay;
18473
+ const size = this.maxSize ? ` - up to ${this.maxSize} per file` : "";
18474
+ return `${formats}${size}`;
18475
+ }
18476
+ get acceptDisplay() {
18477
+ const tokens = this.accept.split(",").map((s) => s.trim()).filter(Boolean).map((tok) => {
18478
+ if (tok.startsWith(".")) return tok.slice(1).toUpperCase();
18479
+ if (tok.endsWith("/*")) return tok.slice(0, -2).toUpperCase();
18480
+ return (tok.split("/").pop() ?? tok).toUpperCase();
18481
+ });
18482
+ if (tokens.length === 0) return "Any file type";
18483
+ if (tokens.length === 1) return tokens[0];
18484
+ return `${tokens.slice(0, -1).join(", ")} or ${tokens[tokens.length - 1]}`;
18485
+ }
18486
+ get alertContent() {
18487
+ if (this.phase === "delayed") {
18488
+ return {
18489
+ type: "warning",
18490
+ message1: "This is taking longer than usual",
18491
+ message2: "Large files can take a few minutes on a slow connection. You can leave this open or come back later."
18492
+ };
18493
+ }
18494
+ const failed = this.failedFiles.length;
18495
+ if (failed > 0 && this.phase !== "uploading") {
18496
+ const limit = this.maxSize ? `under ${this.maxSize} and ` : "";
18497
+ return {
18498
+ type: "error",
18499
+ message1: failed === 1 && this.files.length === 1 ? "We couldn't upload your file" : `We couldn't upload ${failed} of your files`,
18500
+ message2: `Check that each file is ${limit}in a supported format, then try again.`
18501
+ };
18502
+ }
18503
+ return null;
18504
+ }
18505
+ // ── validation ────────────────────────────────────────────────────────────
18506
+ parseMaxBytes() {
18507
+ const s = this.maxSize.trim().toLowerCase();
18508
+ if (!s) return Infinity;
18509
+ const m = s.match(/^([\d.]+)\s*(b|kb|mb|gb)?$/);
18510
+ if (!m) return Infinity;
18511
+ const n = parseFloat(m[1]);
18512
+ const unit = m[2] || "b";
18513
+ const mult = unit === "gb" ? 1e9 : unit === "mb" ? 1e6 : unit === "kb" ? 1e3 : 1;
18514
+ return n * mult;
18515
+ }
18516
+ matchesAccept(file) {
18517
+ const raw = this.accept.trim();
18518
+ if (!raw) return true;
18519
+ const tokens = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
18520
+ const name = file.name.toLowerCase();
18521
+ const type = (file.type || "").toLowerCase();
18522
+ return tokens.some((tok) => {
18523
+ if (tok.startsWith(".")) return name.endsWith(tok);
18524
+ if (tok.endsWith("/*")) return type.startsWith(tok.slice(0, -1));
18525
+ return type === tok;
18526
+ });
18527
+ }
18528
+ static fmtSize(bytes) {
18529
+ if (bytes < 1e3) return `${bytes} B`;
18530
+ if (bytes < 1e6) return `${(bytes / 1e3).toFixed(0)} KB`;
18531
+ return `${(bytes / 1e6).toFixed(1)} MB`;
18532
+ }
18533
+ static ext(name) {
18534
+ const i = name.lastIndexOf(".");
18535
+ return i > -1 ? name.slice(i + 1).toUpperCase() : "FILE";
18536
+ }
18537
+ /**
18538
+ * Validate and append. Invalid files are kept as `error` rows rather than
18539
+ * dropped — that is how the Figma Failure state surfaces them.
18540
+ */
18541
+ ingest(list) {
18542
+ if (!list || list.length === 0) return;
18543
+ const maxBytes = this.parseMaxBytes();
18544
+ const incoming = this.multiple ? Array.from(list) : Array.from(list).slice(0, 1);
18545
+ const added = [];
18546
+ const rejected = [];
18547
+ for (const f of incoming) {
18548
+ const entry = {
18549
+ id: `rel-upload-${++uid}`,
18550
+ name: f.name,
18551
+ size: f.size,
18552
+ status: "ready",
18553
+ file: f
18554
+ };
18555
+ if (!this.matchesAccept(f)) {
18556
+ entry.status = "error";
18557
+ entry.error = "unsupported file type";
18558
+ rejected.push({ name: f.name, reason: "format", message: `${f.name}: unsupported file type` });
18559
+ } else if (f.size > maxBytes) {
18560
+ entry.status = "error";
18561
+ entry.error = `file exceeds the ${this.maxSize} limit`;
18562
+ rejected.push({
18563
+ name: f.name,
18564
+ reason: "size",
18565
+ message: `${f.name}: exceeds ${this.maxSize}`
18566
+ });
18567
+ }
18568
+ added.push(entry);
18569
+ }
18570
+ const names = new Set(added.map((f) => f.name));
18571
+ const kept = this.multiple ? this.files.filter((f) => !names.has(f.name)) : [];
18572
+ this.files = [...kept, ...added];
18573
+ if (rejected.length) this.emit("rel-invalid", { rejected });
18574
+ this.emit("rel-files-added", { added, rejected });
18575
+ this.emit("rel-change", { files: this.files });
18576
+ this.notify();
18577
+ }
18578
+ // ── upload orchestration (self-driving mode) ──────────────────────────────
18579
+ patch(id, partial) {
18580
+ this.files = this.files.map((f) => f.id === id ? { ...f, ...partial } : f);
18581
+ this.notify();
18582
+ }
18583
+ startDelayTimer() {
18584
+ this.stopDelayTimer();
18585
+ if (this.delayThreshold <= 0) return;
18586
+ this.delayTimer = setTimeout(() => {
18587
+ if (this.files.some((f) => f.status === "uploading")) {
18588
+ this.delayed = true;
18589
+ this.notify();
18590
+ }
18591
+ }, this.delayThreshold);
18592
+ }
18593
+ stopDelayTimer() {
18594
+ if (this.delayTimer) clearTimeout(this.delayTimer);
18595
+ this.delayTimer = void 0;
18596
+ }
18597
+ async runBatch(targets) {
18598
+ const uploader = this.uploader;
18599
+ if (!uploader || targets.length === 0) return;
18600
+ this.aborter = new AbortController();
18601
+ const signal = this.aborter.signal;
18602
+ this.startDelayTimer();
18603
+ const queue = [...targets];
18604
+ const workers = Array.from({ length: Math.max(1, this.concurrency) }, async () => {
18605
+ while (queue.length) {
18606
+ if (signal.aborted) return;
18607
+ const item = queue.shift();
18608
+ if (!item || !item.file) continue;
18609
+ this.patch(item.id, { status: "uploading", progress: 0, loaded: 0, error: void 0 });
18610
+ try {
18611
+ await uploader(item.file, {
18612
+ file: item,
18613
+ signal,
18614
+ onProgress: (loaded, total) => {
18615
+ const t = total ?? item.size;
18616
+ const progress = t > 0 ? Math.min(100, Math.round(loaded / t * 100)) : 0;
18617
+ this.patch(item.id, { loaded, progress });
18618
+ this.emit("rel-progress", { id: item.id, progress, loaded, total: t });
18619
+ }
18620
+ });
18621
+ if (signal.aborted) {
18622
+ this.patch(item.id, { status: "ready", progress: void 0, loaded: void 0 });
18623
+ return;
18624
+ }
18625
+ this.patch(item.id, { status: "success", progress: 100, loaded: item.size });
18626
+ this.emit("rel-file-complete", { id: item.id, file: item.file });
18627
+ } catch (err) {
18628
+ if (signal.aborted) {
18629
+ this.patch(item.id, { status: "ready", progress: void 0, loaded: void 0 });
18630
+ return;
18631
+ }
18632
+ const message = err instanceof Error ? err.message : String(err);
18633
+ this.patch(item.id, { status: "error", error: message, progress: void 0 });
18634
+ this.emit("rel-file-error", { id: item.id, file: item.file, error: message });
18635
+ }
18636
+ }
18637
+ });
18638
+ await Promise.all(workers);
18639
+ this.stopDelayTimer();
18640
+ this.delayed = false;
18641
+ this.aborter = void 0;
18642
+ if (!signal.aborted) {
18643
+ this.emit("rel-complete", {
18644
+ succeeded: this.files.filter((f) => f.status === "success"),
18645
+ failed: this.failedFiles
18646
+ });
18647
+ }
18648
+ this.notify();
18649
+ }
18650
+ // ── public API ────────────────────────────────────────────────────────────
18651
+ /** Start (or resume) the batch. Emits `rel-upload`; runs it when `uploader` is set. */
18652
+ upload() {
18653
+ if (!this.canUpload) return;
18654
+ const targets = this.pending;
18655
+ this.emit("rel-upload", { files: targets });
18656
+ if (this.uploader) void this.runBatch(targets);
18657
+ else this.notify();
18658
+ }
18659
+ /** Re-queue every retryable failed row and send again. */
18660
+ retry() {
18661
+ const targets = this.retryable;
18662
+ if (targets.length === 0) return;
18663
+ const ids = new Set(targets.map((f) => f.id));
18664
+ this.files = this.files.map(
18665
+ (f) => ids.has(f.id) ? { ...f, status: "ready", error: void 0, progress: void 0 } : f
18666
+ );
18667
+ const requeued = this.files.filter((f) => ids.has(f.id));
18668
+ this.emit("rel-retry", { files: requeued });
18669
+ if (this.uploader) void this.runBatch(requeued);
18670
+ else this.notify();
18671
+ }
18672
+ /** Abort anything in flight and emit `rel-cancel`. Does not clear the list. */
18673
+ cancel() {
18674
+ this.aborter?.abort();
18675
+ this.stopDelayTimer();
18676
+ this.delayed = false;
18677
+ this.emit("rel-cancel");
18678
+ this.notify();
18679
+ }
18680
+ /** Remove one row by id. */
18681
+ removeFile(id) {
18682
+ const target = this.files.find((f) => f.id === id);
18683
+ this.files = this.files.filter((f) => f.id !== id);
18684
+ this.emit("rel-remove", { id, file: target?.file });
18685
+ this.emit("rel-change", { files: this.files });
18686
+ this.notify();
18687
+ }
18688
+ /** Clear everything and abort in-flight transfers. */
18689
+ reset() {
18690
+ this.aborter?.abort();
18691
+ this.stopDelayTimer();
18692
+ this.files = [];
18693
+ this.delayed = false;
18694
+ if (this.inputEl) this.inputEl.value = "";
18695
+ this.emit("rel-change", { files: this.files });
18696
+ this.notify();
18697
+ }
18698
+ // ── events ────────────────────────────────────────────────────────────────
18699
+ emit(type, detail) {
18700
+ this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
18701
+ }
18702
+ /**
18703
+ * Emit `rel-state` only when the derived snapshot actually changes, so
18704
+ * progress ticks don't spam `<rel-upload-actions>`.
18705
+ */
18706
+ notify() {
18707
+ const s = this.state;
18708
+ const signature = `${s.phase}|${s.total}|${s.failed}|${s.canUpload}|${s.primaryLabel}`;
18709
+ if (signature === this.lastSignature) return;
18710
+ this.lastSignature = signature;
18711
+ this.emit("rel-state", s);
18712
+ }
18713
+ connectedCallback() {
18714
+ super.connectedCallback();
18715
+ queueMicrotask(() => this.emit("rel-state", this.state));
18716
+ }
18717
+ disconnectedCallback() {
18718
+ super.disconnectedCallback();
18719
+ this.stopDelayTimer();
18720
+ this.aborter?.abort();
18721
+ }
18722
+ updated() {
18723
+ this.notify();
18724
+ }
18725
+ // ── render ────────────────────────────────────────────────────────────────
18726
+ rowMeta(f) {
18727
+ switch (f.status) {
18728
+ case "uploading": {
18729
+ const pct = f.progress ?? 0;
18730
+ const loaded = RelUpload.fmtSize(f.loaded ?? 0);
18731
+ return `${pct}% - ${loaded} of ${RelUpload.fmtSize(f.size)}`;
18732
+ }
18733
+ case "success":
18734
+ return `${RelUpload.ext(f.name)} - ${RelUpload.fmtSize(f.size)} - Uploaded`;
18735
+ case "error":
18736
+ return `Upload failed - ${f.error ?? "please try again"}`;
18737
+ default: {
18738
+ const base = `${RelUpload.ext(f.name)} - ${RelUpload.fmtSize(f.size)}`;
18739
+ const busy = this.phase === "uploading" || this.phase === "delayed";
18740
+ return busy ? `${base} - Waiting` : base;
18741
+ }
18742
+ }
18743
+ }
18744
+ renderRow(f) {
18745
+ return html57`
18746
+ <div class="row ${f.status}">
18747
+ <rel-icon name="document" size="md"></rel-icon>
18748
+
18749
+ <div class="row-content">
18750
+ <span class="row-name" title=${f.name}>${f.name}</span>
18751
+ ${f.status === "uploading" ? html57`<rel-progress
18752
+ mode="linear-determinate"
18753
+ .value=${f.progress ?? 0}
18754
+ ></rel-progress>` : nothing12}
18755
+ <span class="row-meta">${this.rowMeta(f)}</span>
18756
+ </div>
18757
+
18758
+ <div class="row-trailing">
18759
+ ${f.status === "success" ? html57`<rel-icon
18760
+ class="status-success"
18761
+ name="check-circle"
18762
+ size="md"
18763
+ role="img"
18764
+ aria-label="Uploaded"
18765
+ ></rel-icon>` : nothing12}
18766
+ ${f.status === "error" ? html57`<rel-icon
18767
+ class="status-error"
18768
+ name="error"
18769
+ size="md"
18770
+ role="img"
18771
+ aria-label="Failed"
18772
+ ></rel-icon>` : nothing12}
18773
+ <button
18774
+ class="remove"
18775
+ aria-label="Remove ${f.name}"
18776
+ ?disabled=${this.disabled}
18777
+ @click=${() => this.removeFile(f.id)}
18778
+ >
18779
+ <rel-icon name="delete" size="md"></rel-icon>
18780
+ </button>
18781
+ </div>
18782
+ </div>
18783
+ `;
18784
+ }
18785
+ render() {
18786
+ const alert = this.alertContent;
18787
+ return html57`
18788
+ <div class="body">
18789
+ ${alert ? html57`<rel-alert
18790
+ type=${alert.type}
18791
+ appearance="subtle"
18792
+ message1=${alert.message1}
18793
+ message2=${alert.message2}
18794
+ ></rel-alert>` : nothing12}
18795
+
18796
+ <div
18797
+ class="dropzone ${this.dragOver ? "dragover" : ""}"
18798
+ @click=${this.openPicker}
18799
+ @dragover=${this.onDragOver}
18800
+ @dragleave=${this.onDragLeave}
18801
+ @drop=${this.onDrop}
18802
+ >
18803
+ <span class="icon-badge">
18804
+ <rel-icon name="upload" size="md"></rel-icon>
18805
+ </span>
18806
+
18807
+ <div class="dz-copy">
18808
+ <p class="dz-title">${this.dropzoneTitle}</p>
18809
+ <p class="dz-hint">${this.hintText}</p>
18810
+ </div>
18811
+
18812
+ <button
18813
+ class="browse"
18814
+ type="button"
18815
+ ?disabled=${this.disabled}
18816
+ @click=${(e) => {
18817
+ e.stopPropagation();
18818
+ this.openPicker();
18819
+ }}
18820
+ >
18821
+ ${this.browseLabel}
18822
+ </button>
18823
+ </div>
18824
+
18825
+ ${this.files.length ? html57`<div class="file-list">
18826
+ <p class="list-heading">${this.listHeading}</p>
18827
+ <div class="rows" role="list">${this.files.map((f) => this.renderRow(f))}</div>
18828
+ </div>` : nothing12}
18829
+
18830
+ <input
18831
+ class="file-input"
18832
+ type="file"
18833
+ ?multiple=${this.multiple}
18834
+ accept=${this.accept || nothing12}
18835
+ @change=${this.onInputChange}
18836
+ />
18837
+ </div>
18838
+ `;
18839
+ }
18840
+ };
18841
+ RelUpload.styles = css54`
18842
+ :host {
18843
+ display: block;
18844
+ box-sizing: border-box;
18845
+ color: var(--rel-text-default);
18846
+ font-family: var(--rel-fonts-font-family);
18847
+ }
18848
+
18849
+ :host([disabled]) {
18850
+ opacity: 0.6;
18851
+ }
18852
+
18853
+ :host([disabled]) .body {
18854
+ pointer-events: none;
18855
+ }
18856
+
18857
+ .body {
18858
+ display: flex;
18859
+ flex-direction: column;
18860
+ gap: var(--rel-spacing-lg, 20px);
18861
+ }
18862
+
18863
+ /* ── Dropzone ─────────────────────────────────────────────────────────── */
18864
+
18865
+ .dropzone {
18866
+ box-sizing: border-box;
18867
+ display: flex;
18868
+ flex-direction: column;
18869
+ align-items: center;
18870
+ justify-content: center;
18871
+ gap: var(--rel-spacing-md, 16px);
18872
+ padding: var(--rel-spacing-2xl, 32px) var(--rel-spacing-xl, 24px);
18873
+ background: var(--rel-bg-surface);
18874
+ border: 1px dashed var(--rel-border-accent);
18875
+ border-radius: var(--rel-border-radius-md, 16px);
18876
+ text-align: center;
18877
+ cursor: pointer;
18878
+ transition: border-color 0.15s ease, background 0.15s ease;
18879
+ }
18880
+
18881
+ /* Design gap: no drag-over frame exists in Figma. */
18882
+ .dropzone.dragover {
18883
+ border-style: solid;
18884
+ border-color: var(--rel-border-brand);
18885
+ background: color-mix(in srgb, var(--rel-bg-brand) 8%, var(--rel-bg-surface));
18886
+ }
18887
+
18888
+ .icon-badge {
18889
+ display: inline-flex;
18890
+ align-items: center;
18891
+ justify-content: center;
18892
+ padding: var(--rel-spacing-sm, 12px);
18893
+ background: var(--rel-bg-surface);
18894
+ border: 1px solid var(--rel-border-subtle);
18895
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
18896
+ }
18897
+
18898
+ .dz-copy {
18899
+ display: flex;
18900
+ flex-direction: column;
18901
+ align-items: center;
18902
+ gap: var(--rel-spacing-2xs, 4px);
18903
+ }
18904
+
18905
+ .dz-title {
18906
+ margin: 0;
18907
+ color: var(--rel-text-default);
18908
+ font-family: var(--rel-body-lg-medium-font-family);
18909
+ font-weight: var(--rel-body-lg-medium-font-weight);
18910
+ font-size: var(--rel-body-lg-medium-font-size);
18911
+ line-height: var(--rel-body-lg-medium-line-height);
18912
+ }
18913
+
18914
+ .dz-hint {
18915
+ margin: 0;
18916
+ color: var(--rel-text-caption);
18917
+ font-family: var(--rel-body-sm-regular-font-family);
18918
+ font-weight: var(--rel-body-sm-regular-font-weight);
18919
+ font-size: var(--rel-body-sm-regular-font-size);
18920
+ line-height: var(--rel-body-sm-regular-line-height);
18921
+ }
18922
+
18923
+ /* The dropzone is a mouse affordance only — the Browse button is the sole
18924
+ focusable control, so there are no nested interactive elements. */
18925
+ .browse {
18926
+ display: inline-flex;
18927
+ align-items: center;
18928
+ gap: var(--rel-spacing-xs, 8px);
18929
+ padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-md, 16px);
18930
+ background: transparent;
18931
+ color: var(--rel-text-default);
18932
+ border: 1px solid var(--rel-action-neutral-border-outline, var(--rel-border-accent));
18933
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
18934
+ cursor: pointer;
18935
+ font-family: var(--rel-body-md-semibold-font-family);
18936
+ font-weight: var(--rel-body-md-semibold-font-weight);
18937
+ font-size: var(--rel-body-md-semibold-font-size);
18938
+ line-height: var(--rel-body-md-semibold-line-height);
18939
+ }
18940
+
18941
+ .browse:hover:not(:disabled) {
18942
+ background: var(--rel-action-neutral-outline-hover, var(--rel-bg-surface-elevated));
18943
+ }
18944
+
18945
+ .browse:focus-visible {
18946
+ outline: 2px solid var(--rel-border-focus);
18947
+ outline-offset: 2px;
18948
+ }
18949
+
18950
+ .browse:disabled {
18951
+ cursor: not-allowed;
18952
+ }
18953
+
18954
+ /* ── File list ────────────────────────────────────────────────────────── */
18955
+
18956
+ .file-list {
18957
+ display: flex;
18958
+ flex-direction: column;
18959
+ gap: var(--rel-spacing-sm, 12px);
18960
+ }
18961
+
18962
+ .list-heading {
18963
+ margin: 0;
18964
+ color: var(--rel-text-default);
18965
+ font-family: var(--rel-body-md-semibold-font-family);
18966
+ font-weight: var(--rel-body-md-semibold-font-weight);
18967
+ font-size: var(--rel-body-md-semibold-font-size);
18968
+ line-height: var(--rel-body-md-semibold-line-height);
18969
+ }
18970
+
18971
+ .rows {
18972
+ display: flex;
18973
+ flex-direction: column;
18974
+ gap: var(--rel-spacing-xs, 8px);
18975
+ }
18976
+
18977
+ .row {
18978
+ display: flex;
18979
+ align-items: center;
18980
+ gap: var(--rel-spacing-sm, 12px);
18981
+ box-sizing: border-box;
18982
+ padding: var(--rel-spacing-sm, 12px) var(--rel-spacing-md, 16px);
18983
+ background: var(--rel-bg-surface);
18984
+ border: 1px solid var(--rel-border-default);
18985
+ border-radius: var(--rel-border-radius-sm, 8px);
18986
+ }
18987
+
18988
+ .row.error {
18989
+ background: var(--rel-bg-danger-light);
18990
+ border-color: var(--rel-border-danger);
18991
+ }
18992
+
18993
+ .row > rel-icon {
18994
+ flex-shrink: 0;
18995
+ }
18996
+
18997
+ .row-content {
18998
+ display: flex;
18999
+ flex-direction: column;
19000
+ gap: var(--rel-spacing-3xs, 2px);
19001
+ flex: 1;
19002
+ min-width: 0;
19003
+ }
19004
+
19005
+ .row.uploading .row-content {
19006
+ gap: var(--rel-spacing-2xs, 4px);
19007
+ }
19008
+
19009
+ .row-name {
19010
+ overflow: hidden;
19011
+ text-overflow: ellipsis;
19012
+ white-space: nowrap;
19013
+ color: var(--rel-text-default);
19014
+ font-family: var(--rel-body-md-regular-font-family);
19015
+ font-weight: var(--rel-body-md-regular-font-weight);
19016
+ font-size: var(--rel-body-md-regular-font-size);
19017
+ line-height: var(--rel-body-md-regular-line-height);
19018
+ }
19019
+
19020
+ .row-meta {
19021
+ color: var(--rel-text-caption);
19022
+ font-family: var(--rel-body-sm-regular-font-family);
19023
+ font-weight: var(--rel-body-sm-regular-font-weight);
19024
+ font-size: var(--rel-body-sm-regular-font-size);
19025
+ line-height: var(--rel-body-sm-regular-line-height);
19026
+ }
19027
+
19028
+ .row.success .row-meta {
19029
+ color: var(--rel-text-success);
19030
+ }
19031
+
19032
+ .row.error .row-meta {
19033
+ color: var(--rel-text-danger);
16676
19034
  }
16677
19035
 
16678
- .toast.hidden {
16679
- display: none;
19036
+ /* Figma's 4px rounded track; rel-progress ships a 6px square one. */
19037
+ rel-progress::part(track) {
19038
+ height: 4px;
19039
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
16680
19040
  }
16681
19041
 
16682
- /* Message */
16683
- .toast-message {
16684
- flex: 1;
19042
+ rel-progress::part(indicator) {
19043
+ background: var(--rel-bg-success);
19044
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
16685
19045
  }
16686
19046
 
16687
- /* Action button */
16688
- .toast-action {
16689
- background: transparent;
16690
- border: none;
16691
- color: var(--rel-action-neutral-label-icons-fill-inverse);
16692
- font-family: var(--rel-action-md-font-family);
16693
- font-weight: var(--rel-action-md-font-weight);
16694
- font-size: var(--rel-action-md-font-size);
16695
- line-height: var(--rel-action-md-line-height);
16696
- letter-spacing: var(--rel-action-md-letter-spacing);
16697
- cursor: pointer;
16698
- transition: opacity 0.2s;
19047
+ .row-trailing {
19048
+ display: flex;
19049
+ align-items: center;
19050
+ gap: var(--rel-spacing-sm, 12px);
19051
+ flex-shrink: 0;
16699
19052
  }
16700
19053
 
16701
- .toast-action:hover {
16702
- background: var(--rel-bg-neutral);
16703
- color: var(--rel-text-default);
16704
- opacity: 0.75;
19054
+ .status-success {
19055
+ color: var(--rel-text-success);
16705
19056
  }
16706
19057
 
16707
- .toast-action:focus-visible {
16708
- outline: 2px solid var(--rel-text-static-light);
16709
- outline-offset: 2px;
16710
- border-radius: var(--rel-border-radius-xs);
19058
+ .status-error {
19059
+ color: var(--rel-text-danger);
16711
19060
  }
16712
19061
 
16713
- /* Close button */
16714
- .toast-close {
16715
- background: transparent;
16716
- color: var(--rel-action-neutral-label-icons-ghost);
19062
+ .remove {
19063
+ display: inline-flex;
19064
+ align-items: center;
19065
+ justify-content: center;
19066
+ padding: 0;
19067
+ background: none;
16717
19068
  border: none;
16718
- color: inherit;
16719
- font-size: 1rem;
16720
19069
  cursor: pointer;
16721
- line-height: 1;
16722
- padding: var(--rel-spacing-2xs);
16723
- margin-left: var(--rel-spacing-2xs);
16724
- }
16725
-
16726
- .toast-close:hover {
16727
- opacity: 0.75;
19070
+ color: var(--rel-text-default);
16728
19071
  }
16729
19072
 
16730
- .toast-close:focus-visible {
16731
- outline: 2px solid var(--rel-text-static-light);
19073
+ .remove:focus-visible {
19074
+ outline: 2px solid var(--rel-border-focus);
16732
19075
  outline-offset: 2px;
16733
- border-radius: var(--rel-border-radius-xs);
19076
+ border-radius: var(--rel-border-radius-xs, 4px);
16734
19077
  }
16735
19078
 
16736
-
16737
- @media (prefers-reduced-motion: reduce) {
16738
- .toast {
16739
- animation: none;
16740
- }
19079
+ .file-input {
19080
+ display: none;
16741
19081
  }
16742
19082
 
16743
- /* Animations */
16744
- @keyframes fadeInUp {
16745
- from {
16746
- opacity: 0;
16747
- transform: translateY(20px);
16748
- }
16749
- to {
16750
- opacity: 1;
16751
- transform: translateY(0);
19083
+ @media (prefers-reduced-motion: reduce) {
19084
+ .dropzone {
19085
+ transition: none;
16752
19086
  }
16753
19087
  }
16754
19088
  `;
16755
19089
  __decorateClass([
16756
- property48({ type: String })
16757
- ], RelToast.prototype, "message", 2);
19090
+ property51({ type: String })
19091
+ ], RelUpload.prototype, "accept", 2);
16758
19092
  __decorateClass([
16759
- property48({ type: String })
16760
- ], RelToast.prototype, "type", 2);
19093
+ property51({ type: String, attribute: "max-size" })
19094
+ ], RelUpload.prototype, "maxSize", 2);
16761
19095
  __decorateClass([
16762
- property48({ type: String })
16763
- ], RelToast.prototype, "actionLabel", 2);
19096
+ property51({ type: Boolean })
19097
+ ], RelUpload.prototype, "multiple", 2);
16764
19098
  __decorateClass([
16765
- property48({ type: Number })
16766
- ], RelToast.prototype, "duration", 2);
19099
+ property51({ type: Boolean, reflect: true })
19100
+ ], RelUpload.prototype, "disabled", 2);
16767
19101
  __decorateClass([
16768
- property48({ type: Boolean, reflect: true })
16769
- ], RelToast.prototype, "show", 2);
19102
+ property51({ type: String })
19103
+ ], RelUpload.prototype, "hint", 2);
16770
19104
  __decorateClass([
16771
- state21()
16772
- ], RelToast.prototype, "visible", 2);
16773
- RelToast = __decorateClass([
16774
- customElement54("rel-toast")
16775
- ], RelToast);
19105
+ property51({ type: String, attribute: "dropzone-title" })
19106
+ ], RelUpload.prototype, "dropzoneTitle", 2);
19107
+ __decorateClass([
19108
+ property51({ type: String, attribute: "browse-label" })
19109
+ ], RelUpload.prototype, "browseLabel", 2);
19110
+ __decorateClass([
19111
+ property51({ attribute: false })
19112
+ ], RelUpload.prototype, "files", 2);
19113
+ __decorateClass([
19114
+ property51({ attribute: false })
19115
+ ], RelUpload.prototype, "uploader", 2);
19116
+ __decorateClass([
19117
+ property51({ type: Number })
19118
+ ], RelUpload.prototype, "concurrency", 2);
19119
+ __decorateClass([
19120
+ property51({ type: Number, attribute: "delay-threshold" })
19121
+ ], RelUpload.prototype, "delayThreshold", 2);
19122
+ __decorateClass([
19123
+ state23()
19124
+ ], RelUpload.prototype, "dragOver", 2);
19125
+ __decorateClass([
19126
+ state23()
19127
+ ], RelUpload.prototype, "delayed", 2);
19128
+ __decorateClass([
19129
+ query11(".file-input")
19130
+ ], RelUpload.prototype, "inputEl", 2);
19131
+ RelUpload = __decorateClass([
19132
+ customElement57("rel-upload")
19133
+ ], RelUpload);
16776
19134
 
16777
- // src/components/rel-tooltip/rel-tooltip.ts
16778
- import { LitElement as LitElement55, html as html55, css as css52 } from "lit";
16779
- import { customElement as customElement55, property as property49 } from "lit/decorators.js";
16780
- var RelTooltip = class extends LitElement55 {
19135
+ // src/components/rel-upload/rel-upload-actions.ts
19136
+ import { LitElement as LitElement58, html as html58, css as css55, nothing as nothing13 } from "lit";
19137
+ import { customElement as customElement58, property as property52, state as state24 } from "lit/decorators.js";
19138
+ import "@reliability-design/icons/rel-icon";
19139
+ var EMPTY_STATE = {
19140
+ phase: "empty",
19141
+ total: 0,
19142
+ failed: 0,
19143
+ canUpload: false,
19144
+ primaryLabel: "Upload"
19145
+ };
19146
+ var RelUploadActions = class extends LitElement58 {
16781
19147
  constructor() {
16782
19148
  super(...arguments);
16783
- this.text = "";
16784
- this.position = "above";
16785
- this.showTail = false;
16786
- this.type = "single-line";
16787
- // Stable id so aria-describedby can reference the bubble across renders.
16788
- this.uniqueId = Math.random().toString(36).slice(2);
19149
+ this.for = "";
19150
+ this.cancelLabel = "Cancel";
19151
+ this.noCancel = false;
19152
+ this.snapshot = EMPTY_STATE;
19153
+ this.target = null;
19154
+ this.onState = (e) => {
19155
+ if (!this.target) this.resolveTarget();
19156
+ if (e.target !== this.target) return;
19157
+ this.snapshot = e.detail;
19158
+ };
19159
+ this.onCancel = () => {
19160
+ this.target?.cancel();
19161
+ this.dispatchEvent(new CustomEvent("rel-cancel", { bubbles: true, composed: true }));
19162
+ };
19163
+ this.onPrimary = () => {
19164
+ if (!this.target) return;
19165
+ if (this.snapshot.phase === "failed") this.target.retry();
19166
+ else this.target.upload();
19167
+ };
16789
19168
  }
16790
- firstUpdated() {
16791
- this.linkTrigger();
19169
+ connectedCallback() {
19170
+ super.connectedCallback();
19171
+ queueMicrotask(() => this.resolveTarget());
19172
+ this.root.addEventListener("rel-state", this.onState);
16792
19173
  }
16793
- linkTrigger() {
16794
- const slot = this.shadowRoot?.querySelector("slot");
16795
- if (!slot) return;
16796
- const trigger = slot.assignedElements()[0];
16797
- if (trigger) {
16798
- trigger.setAttribute("aria-describedby", `tooltip-${this.uniqueId}`);
19174
+ disconnectedCallback() {
19175
+ super.disconnectedCallback();
19176
+ this.root.removeEventListener("rel-state", this.onState);
19177
+ this.target = null;
19178
+ }
19179
+ updated(changed) {
19180
+ if (changed.has("for")) this.resolveTarget();
19181
+ }
19182
+ /**
19183
+ * Listen on our own root rather than `document`: a shared root means no
19184
+ * shadow boundary sits between us and the sibling `<rel-upload>`, so
19185
+ * `event.target` is never retargeted away from it.
19186
+ */
19187
+ get root() {
19188
+ const node = this.getRootNode();
19189
+ return node instanceof ShadowRoot ? node : this.ownerDocument;
19190
+ }
19191
+ resolveTarget() {
19192
+ const root = this.getRootNode();
19193
+ let found = null;
19194
+ if (this.for) {
19195
+ found = root.querySelector(`#${CSS.escape(this.for)}`);
19196
+ } else {
19197
+ found = this.parentElement?.querySelector("rel-upload") ?? root.querySelector("rel-upload");
16799
19198
  }
19199
+ this.target = found;
19200
+ if (found) this.snapshot = found.state;
16800
19201
  }
16801
19202
  render() {
16802
- return html55`
16803
- <slot @slotchange="${this.linkTrigger}"></slot>
16804
- <div
16805
- part="panel" class="tooltip-bubble ${this.position}"
16806
- id="tooltip-${this.uniqueId}"
16807
- role="tooltip"
16808
- >
16809
- ${this.text}
16810
- ${this.showTail ? html55`<span part="indicator" class="tooltip-tail"></span>` : ""}
19203
+ const { phase, canUpload, primaryLabel } = this.snapshot;
19204
+ const busy = phase === "uploading" || phase === "delayed";
19205
+ return html58`
19206
+ <div class="actions" part="base">
19207
+ ${this.noCancel ? nothing13 : html58`<rel-button part="cancel" variant="secondary" @click=${this.onCancel}>
19208
+ ${this.cancelLabel}
19209
+ </rel-button>`}
19210
+
19211
+ <rel-button
19212
+ part="primary"
19213
+ variant="primary"
19214
+ ?loading=${busy}
19215
+ ?disabled=${!canUpload}
19216
+ aria-label=${busy ? "Uploading" : primaryLabel}
19217
+ @click=${this.onPrimary}
19218
+ >
19219
+ ${busy ? nothing13 : html58`${phase === "failed" ? html58`<rel-icon name="refresh" size="sm"></rel-icon>` : nothing13}${primaryLabel}`}
19220
+ </rel-button>
16811
19221
  </div>
16812
19222
  `;
16813
19223
  }
16814
19224
  };
16815
- RelTooltip.styles = css52`
19225
+ RelUploadActions.styles = css55`
16816
19226
  :host {
16817
- color: var(--rel-text-default);
16818
- display: inline-block;
16819
- position: relative;
16820
- }
16821
-
16822
- /* Tooltip container */
16823
- .tooltip-bubble {
16824
- font-family: var(--rel-body-md-regular-font-family);
16825
- font-weight: var(--rel-body-md-medium-font-weight);
16826
- font-size: var(--rel-body-md-regular-font-size);
16827
- line-height: var(--rel-body-md-regular-line-height);
16828
- letter-spacing: var(--rel-body-md-regular-letter-spacing);
16829
- position: absolute;
16830
- background-color: var(--rel-bg-neutral-inverse-2);
16831
- color: var(--rel-text-inverse);
16832
- padding: var(--rel-spacing-xs, 8px)
16833
- var(--rel-spacing-sm, 12px);
16834
- border-radius: var(--rel-border-radius-sm, 8px);
16835
- white-space: nowrap;
16836
- z-index: 1000;
16837
- opacity: 0;
16838
- pointer-events: none;
16839
- transition: opacity 0.2s ease;
16840
- overflow: visible;
16841
- }
16842
-
16843
- /* Figma Type=Multi Line: wrap inside a bounded bubble */
16844
- :host([type="multi-line"]) .tooltip-bubble {
16845
- white-space: normal;
16846
- max-width: 220px;
16847
- width: max-content;
16848
- }
16849
-
16850
- :host(:hover) .tooltip-bubble,
16851
- :host(:focus-within) .tooltip-bubble {
16852
- opacity: 1;
16853
- }
16854
-
16855
- /* Positioning */
16856
- .tooltip-bubble.above {
16857
- bottom: calc(100% + 6px);
16858
- left: 50%;
16859
- transform: translateX(-50%);
16860
- }
16861
-
16862
- .tooltip-bubble.below {
16863
- top: calc(100% + 6px);
16864
- left: 50%;
16865
- transform: translateX(-50%);
16866
- }
16867
-
16868
- .tooltip-bubble.left {
16869
- right: calc(100% + 6px);
16870
- top: 50%;
16871
- transform: translateY(-50%);
16872
- }
16873
-
16874
- .tooltip-bubble.right {
16875
- left: calc(100% + 6px);
16876
- top: 50%;
16877
- transform: translateY(-50%);
16878
- }
16879
-
16880
- /* Tail/Arrow */
16881
- .tooltip-tail {
16882
- position: absolute;
16883
- width: 0;
16884
- height: 0;
16885
19227
  display: block;
19228
+ font-family: var(--rel-fonts-font-family);
16886
19229
  }
16887
19230
 
16888
- .tooltip-bubble.above .tooltip-tail,
16889
- .tooltip-bubble.below .tooltip-tail {
16890
- left: 50%;
16891
- transform: translateX(-50%);
16892
- border-left: 6px solid transparent;
16893
- border-right: 6px solid transparent;
16894
- }
16895
-
16896
- .tooltip-bubble.above .tooltip-tail {
16897
- bottom: -6px;
16898
- border-top: 6px solid var(--rel-bg-neutral-inverse-2);
16899
- border-bottom: none;
16900
- }
16901
-
16902
- .tooltip-bubble.below .tooltip-tail {
16903
- top: -6px;
16904
- border-bottom: 6px solid var(--rel-bg-neutral-inverse-2);
16905
- border-top: none;
16906
- }
16907
-
16908
- .tooltip-bubble.left .tooltip-tail,
16909
- .tooltip-bubble.right .tooltip-tail {
16910
- top: 50%;
16911
- transform: translateY(-50%);
16912
- border-top: 6px solid transparent;
16913
- border-bottom: 6px solid transparent;
16914
- }
16915
-
16916
- .tooltip-bubble.left .tooltip-tail {
16917
- right: -6px;
16918
- border-left: 6px solid var(--rel-bg-neutral-inverse-2);
16919
- border-right: none;
19231
+ .actions {
19232
+ display: flex;
19233
+ align-items: center;
19234
+ justify-content: flex-end;
19235
+ gap: var(--rel-spacing-xs, 8px);
16920
19236
  }
16921
19237
 
16922
- .tooltip-bubble.right .tooltip-tail {
16923
- left: -6px;
16924
- border-right: 6px solid var(--rel-bg-neutral-inverse-2);
16925
- border-left: none;
19238
+ rel-button rel-icon {
19239
+ margin-inline-end: var(--rel-spacing-xs, 8px);
16926
19240
  }
16927
19241
  `;
16928
19242
  __decorateClass([
16929
- property49({ type: String })
16930
- ], RelTooltip.prototype, "text", 2);
19243
+ property52({ type: String })
19244
+ ], RelUploadActions.prototype, "for", 2);
16931
19245
  __decorateClass([
16932
- property49({ type: String })
16933
- ], RelTooltip.prototype, "position", 2);
19246
+ property52({ type: String, attribute: "cancel-label" })
19247
+ ], RelUploadActions.prototype, "cancelLabel", 2);
16934
19248
  __decorateClass([
16935
- property49({ type: Boolean, attribute: "show-tail" })
16936
- ], RelTooltip.prototype, "showTail", 2);
19249
+ property52({ type: Boolean, attribute: "no-cancel" })
19250
+ ], RelUploadActions.prototype, "noCancel", 2);
16937
19251
  __decorateClass([
16938
- property49({ type: String, reflect: true })
16939
- ], RelTooltip.prototype, "type", 2);
16940
- RelTooltip = __decorateClass([
16941
- customElement55("rel-tooltip")
16942
- ], RelTooltip);
19252
+ state24()
19253
+ ], RelUploadActions.prototype, "snapshot", 2);
19254
+ RelUploadActions = __decorateClass([
19255
+ customElement58("rel-upload-actions")
19256
+ ], RelUploadActions);
16943
19257
 
16944
19258
  // src/index.ts
16945
19259
  export * from "@reliability-design/icons";
@@ -16991,6 +19305,7 @@ export {
16991
19305
  RelTab,
16992
19306
  RelTabItem,
16993
19307
  RelTextfield,
19308
+ RelTimePicker,
16994
19309
  RelToast,
16995
19310
  RelTooltip,
16996
19311
  RelTopbar,
@@ -17001,6 +19316,8 @@ export {
17001
19316
  RelTopbarItem,
17002
19317
  RelTopbarMobilePanel,
17003
19318
  RelTopbarProfile,
17004
- RelTopbarUtilities
19319
+ RelTopbarUtilities,
19320
+ RelUpload,
19321
+ RelUploadActions
17005
19322
  };
17006
19323
  //# sourceMappingURL=index.mjs.map