@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.js CHANGED
@@ -76,6 +76,7 @@ __export(index_exports, {
76
76
  RelTab: () => RelTab,
77
77
  RelTabItem: () => RelTabItem,
78
78
  RelTextfield: () => RelTextfield,
79
+ RelTimePicker: () => RelTimePicker,
79
80
  RelToast: () => RelToast,
80
81
  RelTooltip: () => RelTooltip,
81
82
  RelTopbar: () => RelTopbar,
@@ -86,7 +87,9 @@ __export(index_exports, {
86
87
  RelTopbarItem: () => RelTopbarItem,
87
88
  RelTopbarMobilePanel: () => RelTopbarMobilePanel,
88
89
  RelTopbarProfile: () => RelTopbarProfile,
89
- RelTopbarUtilities: () => RelTopbarUtilities
90
+ RelTopbarUtilities: () => RelTopbarUtilities,
91
+ RelUpload: () => RelUpload,
92
+ RelUploadActions: () => RelUploadActions
90
93
  });
91
94
  module.exports = __toCommonJS(index_exports);
92
95
  var import_vars = require("./vars-XNMTSRVN.css");
@@ -760,6 +763,7 @@ var RelTextfield = class extends import_lit3.LitElement {
760
763
  this.internalState = "default";
761
764
  this._wasError = false;
762
765
  this.uniqueId = `textfield-${Math.random().toString(36).slice(2)}`;
766
+ this._warnedNoName = false;
763
767
  }
764
768
  get isDisabled() {
765
769
  return this.disabled || this.state === "disabled";
@@ -775,6 +779,15 @@ var RelTextfield = class extends import_lit3.LitElement {
775
779
  get helper() {
776
780
  return this.helperText ?? this.supportingText;
777
781
  }
782
+ /** labelStyle supersedes the deprecated floatingLabel boolean. */
783
+ get resolvedLabelStyle() {
784
+ if (this.labelStyle) return this.labelStyle;
785
+ return this.floatingLabel ? "floating" : "default";
786
+ }
787
+ /** True when no visible label renders and the input needs an ARIA name. */
788
+ get labelHidden() {
789
+ return this.resolvedLabelStyle === "none";
790
+ }
778
791
  connectedCallback() {
779
792
  super.connectedCallback();
780
793
  const leftIcon = this.getAttribute("lefticon") || this.getAttribute("leftIcon");
@@ -782,6 +795,20 @@ var RelTextfield = class extends import_lit3.LitElement {
782
795
  const rightIcon = this.getAttribute("righticon") || this.getAttribute("rightIcon");
783
796
  if (rightIcon) this.rightIcon = rightIcon;
784
797
  }
798
+ willUpdate(changed) {
799
+ if (changed.has("labelStyle") && this.labelStyle) {
800
+ this.floatingLabel = this.labelStyle === "floating";
801
+ }
802
+ }
803
+ updated() {
804
+ if (this._warnedNoName || !this.labelHidden) return;
805
+ if (this.fieldAriaLabel || this.fieldAriaLabelledby || this.getAttribute("aria-label")) return;
806
+ this._warnedNoName = true;
807
+ console.warn(
808
+ '<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.',
809
+ this
810
+ );
811
+ }
785
812
  /** Focuses the inner input/textarea (the host itself is not focusable). */
786
813
  focus(options) {
787
814
  this.renderRoot.querySelector("input, textarea")?.focus(options);
@@ -849,15 +876,20 @@ var RelTextfield = class extends import_lit3.LitElement {
849
876
  !this.multiline && leadingIcon ? "has-left-icon" : "",
850
877
  !this.multiline && this.rightIcon && this.variant !== "legend" ? "has-right-icon" : ""
851
878
  ].filter(Boolean).join(" ");
879
+ const labelStyle = this.resolvedLabelStyle;
880
+ const labelHidden = labelStyle === "none";
852
881
  const labelTemplate = (extraClass = "") => {
853
- if (!this.label) return "";
882
+ if (!this.label || labelHidden) return "";
854
883
  return import_lit3.html`<label
855
884
  part="label"
856
885
  class="textfield-label${extraClass}"
857
886
  for="${inputId}"
858
887
  >${this.label}${this.required ? import_lit3.html`<span class="required-mark" aria-hidden="true">*</span>` : ""}</label>`;
859
888
  };
860
- const ariaLabelledby = this.variant === "legend" && this.label ? legendId : void 0;
889
+ const legendLabelledby = this.variant === "legend" && this.label && !labelHidden ? legendId : void 0;
890
+ const ariaLabelledby = this.fieldAriaLabelledby ?? legendLabelledby;
891
+ const hasVisibleLabel = !labelHidden && !!this.label;
892
+ const ariaLabel = !hasVisibleLabel && !ariaLabelledby ? this.fieldAriaLabel : void 0;
861
893
  const inputTemplate = this.multiline ? import_lit3.html`<textarea part="input"
862
894
  id="${inputId}"
863
895
  class="${inputClasses}"
@@ -871,6 +903,7 @@ var RelTextfield = class extends import_lit3.LitElement {
871
903
  ?required="${this.required}"
872
904
  aria-invalid="${isError}"
873
905
  aria-required="${this.required}"
906
+ aria-label="${ariaLabel ?? import_lit3.nothing}"
874
907
  aria-labelledby="${ariaLabelledby ?? import_lit3.nothing}"
875
908
  aria-describedby="${describedById ?? import_lit3.nothing}"
876
909
  @focus="${this.handleFocus}"
@@ -889,6 +922,7 @@ var RelTextfield = class extends import_lit3.LitElement {
889
922
  ?required="${this.required}"
890
923
  aria-invalid="${isError}"
891
924
  aria-required="${this.required}"
925
+ aria-label="${ariaLabel ?? import_lit3.nothing}"
892
926
  aria-labelledby="${ariaLabelledby ?? import_lit3.nothing}"
893
927
  aria-describedby="${describedById ?? import_lit3.nothing}"
894
928
  @focus="${this.handleFocus}"
@@ -910,7 +944,7 @@ var RelTextfield = class extends import_lit3.LitElement {
910
944
  const fieldsetClass = ["field-fieldset", this.multiline ? "multiline" : ""].filter(Boolean).join(" ");
911
945
  return import_lit3.html`
912
946
  <div class="${wrapperClasses}">
913
- <fieldset part="control" class="${fieldsetClass}"><legend class="field-legend" id="${legendId}">${this.label ?? ""}${this.required ? import_lit3.html`<span class="required-mark" aria-hidden="true">*</span>` : ""}</legend>
947
+ <fieldset part="control" class="${fieldsetClass}"><legend class="field-legend" id="${legendId}">${labelHidden ? "" : this.label ?? ""}${this.required && !labelHidden ? import_lit3.html`<span class="required-mark" aria-hidden="true">*</span>` : ""}</legend>
914
948
  ${!this.multiline ? leadingIconTemplate : ""}
915
949
  ${inputTemplate}
916
950
  </fieldset>
@@ -920,10 +954,10 @@ var RelTextfield = class extends import_lit3.LitElement {
920
954
  }
921
955
  return import_lit3.html`
922
956
  <div class="${wrapperClasses}">
923
- ${!this.floatingLabel ? labelTemplate() : ""}
957
+ ${labelHidden ? import_lit3.html`<div class="label-reserve" aria-hidden="true"></div>` : labelStyle !== "floating" ? labelTemplate() : ""}
924
958
 
925
959
  <div part="control" class="${["textfield-container", this.isDisabled ? "disabled" : "", this.multiline ? "multiline" : ""].filter(Boolean).join(" ")}">
926
- ${this.floatingLabel ? labelTemplate(leadingIcon && !this.multiline ? " has-left-icon" : "") : ""}
960
+ ${labelStyle === "floating" ? labelTemplate(leadingIcon && !this.multiline ? " has-left-icon" : "") : ""}
927
961
  ${!this.multiline ? leadingIconTemplate : ""}
928
962
 
929
963
  ${inputTemplate}
@@ -982,6 +1016,22 @@ RelTextfield.styles = import_lit3.css`
982
1016
  margin-left: 2px;
983
1017
  }
984
1018
 
1019
+ /* ── labelStyle="none" ──
1020
+ No label renders. An empty spacer holds the label row's full height so the
1021
+ input box still bottom-aligns with labelled siblings in a form row.
1022
+ The reserve covers label line-height + the wrapper gap, and the negative
1023
+ margin cancels the flex gap that would otherwise sit below it — so
1024
+ --rel-field-label-reserve: 0 collapses the row to genuinely zero
1025
+ (toolbars, table cells), not just to the leftover gap. */
1026
+ .label-reserve {
1027
+ flex: none;
1028
+ height: var(
1029
+ --rel-field-label-reserve,
1030
+ calc(var(--rel-body-sm-medium-line-height) + var(--rel-spacing-2xs))
1031
+ );
1032
+ margin-bottom: calc(-1 * var(--rel-spacing-2xs));
1033
+ }
1034
+
985
1035
  /* Floating label variant */
986
1036
  :host([floatinglabel]) .textfield-wrapper {
987
1037
  gap: 0;
@@ -1358,6 +1408,12 @@ RelTextfield.styles = import_lit3.css`
1358
1408
  box-sizing: border-box;
1359
1409
  }
1360
1410
 
1411
+ /* labelStyle="none" + legend: nothing to seat in the notch, so drop the
1412
+ legend entirely and let the top border run continuous. */
1413
+ :host([label-style="none"][variant="legend"]) .field-legend {
1414
+ display: none;
1415
+ }
1416
+
1361
1417
  /* Prefix icon (legend): vertically centered; input inset via has-left-icon */
1362
1418
  :host([variant="legend"]) .icon.left {
1363
1419
  top: 50%;
@@ -1473,9 +1529,18 @@ __decorateClass([
1473
1529
  __decorateClass([
1474
1530
  (0, import_decorators3.property)({ type: String })
1475
1531
  ], RelTextfield.prototype, "size", 2);
1532
+ __decorateClass([
1533
+ (0, import_decorators3.property)({ type: String, attribute: "label-style", reflect: true })
1534
+ ], RelTextfield.prototype, "labelStyle", 2);
1476
1535
  __decorateClass([
1477
1536
  (0, import_decorators3.property)({ type: Boolean, reflect: true })
1478
1537
  ], RelTextfield.prototype, "floatingLabel", 2);
1538
+ __decorateClass([
1539
+ (0, import_decorators3.property)({ type: String, attribute: "field-aria-label" })
1540
+ ], RelTextfield.prototype, "fieldAriaLabel", 2);
1541
+ __decorateClass([
1542
+ (0, import_decorators3.property)({ type: String, attribute: "field-aria-labelledby" })
1543
+ ], RelTextfield.prototype, "fieldAriaLabelledby", 2);
1479
1544
  __decorateClass([
1480
1545
  (0, import_decorators3.property)({ type: String, reflect: true })
1481
1546
  ], RelTextfield.prototype, "variant", 2);
@@ -1585,12 +1650,26 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1585
1650
  if (left + t.width > window.innerWidth - 8) {
1586
1651
  left = Math.max(8, window.innerWidth - t.width - 8);
1587
1652
  }
1653
+ this.panelEl.style.top = `${top}px`;
1654
+ this.panelEl.style.left = `${left}px`;
1655
+ this.panelEl.style.width = `${t.width}px`;
1656
+ const applied = this.panelEl.getBoundingClientRect();
1657
+ top -= applied.top - top;
1658
+ left -= applied.left - left;
1659
+ this.panelEl.style.top = `${top}px`;
1660
+ this.panelEl.style.left = `${left}px`;
1588
1661
  this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
1589
1662
  };
1590
1663
  }
1664
+ /** Name used for the suggestion panel — the visible label, or fieldAriaLabel
1665
+ * once labelStyle="none" hides it. Mirrors rel-textfield's own resolution. */
1666
+ get accessibleName() {
1667
+ return (this.labelStyle === "none" ? this.fieldAriaLabel : this.label) || "";
1668
+ }
1591
1669
  disconnectedCallback() {
1592
1670
  super.disconnectedCallback();
1593
1671
  this.removeGlobalListeners();
1672
+ this.hideNativePopover();
1594
1673
  clearTimeout(this.debounceTimer);
1595
1674
  }
1596
1675
  willUpdate(changed) {
@@ -1650,13 +1729,40 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1650
1729
  this.open = true;
1651
1730
  this.highlightedIndex = -1;
1652
1731
  this.addGlobalListeners();
1653
- requestAnimationFrame(() => this.updatePosition());
1732
+ requestAnimationFrame(() => {
1733
+ this.showNativePopover();
1734
+ this.updatePosition();
1735
+ });
1654
1736
  }
1655
1737
  closePanel() {
1656
1738
  if (!this.open) return;
1657
1739
  this.open = false;
1658
1740
  this.highlightedIndex = -1;
1659
1741
  this.removeGlobalListeners();
1742
+ this.hideNativePopover();
1743
+ }
1744
+ /* A transformed ancestor (e.g. rel-modal's centered panel with
1745
+ * translate(-50%,-50%)) becomes the containing block for position:fixed
1746
+ * descendants, breaking viewport-based coordinates and letting the
1747
+ * panel's overflow:hidden clip the suggestions. Promoting the panel to
1748
+ * the browser top layer sidesteps both — same fix as rel-popover.
1749
+ * `manual` keeps light-dismiss and Escape handling under this
1750
+ * component's own control. */
1751
+ showNativePopover() {
1752
+ const p = this.panelEl;
1753
+ if (!p || typeof p.showPopover !== "function") return;
1754
+ try {
1755
+ if (!p.matches(":popover-open")) p.showPopover();
1756
+ } catch {
1757
+ }
1758
+ }
1759
+ hideNativePopover() {
1760
+ const p = this.panelEl;
1761
+ if (!p || typeof p.hidePopover !== "function") return;
1762
+ try {
1763
+ if (p.matches(":popover-open")) p.hidePopover();
1764
+ } catch {
1765
+ }
1660
1766
  }
1661
1767
  /** Re-evaluates whether the panel should be showing for the current state. */
1662
1768
  syncPanel() {
@@ -1692,17 +1798,17 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1692
1798
  this.syncPanel();
1693
1799
  }
1694
1800
  }
1695
- async runSearch(query10) {
1801
+ async runSearch(query12) {
1696
1802
  const seq = ++this.searchSeq;
1697
1803
  try {
1698
- const results = await this.searchFn(query10);
1804
+ const results = await this.searchFn(query12);
1699
1805
  if (seq !== this.searchSeq) return;
1700
1806
  this.remoteItems = results ?? [];
1701
1807
  } catch (err) {
1702
1808
  if (seq !== this.searchSeq) return;
1703
1809
  this.remoteItems = [];
1704
1810
  this.dispatchEvent(
1705
- new CustomEvent("rel-search-error", { detail: { query: query10, error: err }, bubbles: true, composed: true })
1811
+ new CustomEvent("rel-search-error", { detail: { query: query12, error: err }, bubbles: true, composed: true })
1706
1812
  );
1707
1813
  } finally {
1708
1814
  if (seq === this.searchSeq) {
@@ -1848,7 +1954,10 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1848
1954
  .value="${this.inputText}"
1849
1955
  .size="${this.size}"
1850
1956
  .variant="${this.variant}"
1957
+ .labelStyle="${this.labelStyle}"
1851
1958
  .floatingLabel="${this.floatingLabel}"
1959
+ .fieldAriaLabel="${this.fieldAriaLabel}"
1960
+ .fieldAriaLabelledby="${this.fieldAriaLabelledby}"
1852
1961
  .prefixIcon="${this.prefixIcon}"
1853
1962
  .disabled="${this.disabled}"
1854
1963
  .readonly="${this.readonly}"
@@ -1869,8 +1978,9 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1869
1978
  id="${panelId}"
1870
1979
  part="panel list"
1871
1980
  class="ac-panel"
1981
+ popover="manual"
1872
1982
  role="listbox"
1873
- aria-label="${this.label || "Suggestions"}"
1983
+ aria-label="${this.accessibleName || "Suggestions"}"
1874
1984
  style="${this.panelStyle}"
1875
1985
  ?data-open="${this.open}"
1876
1986
  >
@@ -1923,6 +2033,15 @@ RelAutocomplete.styles = import_lit4.css`
1923
2033
  position: fixed;
1924
2034
  z-index: 1001;
1925
2035
  display: none;
2036
+ /* Reset native [popover] UA styles (the panel is promoted to the
2037
+ top layer so transformed ancestors — e.g. a centered rel-modal
2038
+ panel — can't hijack its containing block or clip it). */
2039
+ inset: auto;
2040
+ margin: 0;
2041
+ padding: 0;
2042
+ width: auto;
2043
+ height: auto;
2044
+ color: inherit;
1926
2045
  box-sizing: border-box;
1927
2046
  background: var(--rel-autocomplete-bg, var(--rel-bg-neutral));
1928
2047
  border: 1px solid var(--rel-autocomplete-menu-border, var(--rel-border-default));
@@ -2049,9 +2168,18 @@ __decorateClass([
2049
2168
  __decorateClass([
2050
2169
  (0, import_decorators4.property)({ type: String })
2051
2170
  ], RelAutocomplete.prototype, "variant", 2);
2171
+ __decorateClass([
2172
+ (0, import_decorators4.property)({ type: String, attribute: "label-style", reflect: true })
2173
+ ], RelAutocomplete.prototype, "labelStyle", 2);
2052
2174
  __decorateClass([
2053
2175
  (0, import_decorators4.property)({ type: Boolean, attribute: "floating-label" })
2054
2176
  ], RelAutocomplete.prototype, "floatingLabel", 2);
2177
+ __decorateClass([
2178
+ (0, import_decorators4.property)({ type: String, attribute: "field-aria-label" })
2179
+ ], RelAutocomplete.prototype, "fieldAriaLabel", 2);
2180
+ __decorateClass([
2181
+ (0, import_decorators4.property)({ type: String, attribute: "field-aria-labelledby" })
2182
+ ], RelAutocomplete.prototype, "fieldAriaLabelledby", 2);
2055
2183
  __decorateClass([
2056
2184
  (0, import_decorators4.property)({ type: String, attribute: "prefix-icon" })
2057
2185
  ], RelAutocomplete.prototype, "prefixIcon", 2);
@@ -2763,8 +2891,8 @@ RelButton.styles = import_lit8.css`
2763
2891
  pointer-events:none on the HOST would make it transparent to the
2764
2892
  pointer and suppress the cursor entirely. */
2765
2893
  cursor: not-allowed;
2766
- /* The disabled tokens are full-contrast pairs (light: #0B0B0B on
2767
- #C2C2C2, dark: #FFF on #404040), so on their own they render a
2894
+ /* The disabled tokens are full-contrast pairs (light: near-black on
2895
+ gray-300, dark: white on gray-700), so on their own they render a
2768
2896
  crisp button that reads as enabled. Dulling happens here instead —
2769
2897
  same opacity mechanism the rest of the library uses. */
2770
2898
  opacity: 0.5;
@@ -4387,6 +4515,9 @@ var RelCheckbox = class extends import_lit13.LitElement {
4387
4515
  this.state = "default";
4388
4516
  this.status = "unchecked";
4389
4517
  this.value = "";
4518
+ this.size = "medium";
4519
+ this.density = "compact";
4520
+ this.alignment = "leading";
4390
4521
  this._onChange = () => {
4391
4522
  };
4392
4523
  this._onTouched = () => {
@@ -4398,6 +4529,17 @@ var RelCheckbox = class extends import_lit13.LitElement {
4398
4529
  set checked(val) {
4399
4530
  this.status = val ? "checked" : "unchecked";
4400
4531
  }
4532
+ willUpdate(changed) {
4533
+ if ((changed.has("alignment") || changed.has("density")) && this.alignment === "trailing" && this.density === "compact") {
4534
+ console.warn(
4535
+ '[rel-checkbox] density="compact" has no trailing layout; rendering comfortable density.'
4536
+ );
4537
+ }
4538
+ }
4539
+ /** Trailing has no compact layout in the spec — trailing wins. */
4540
+ get effectiveDensity() {
4541
+ return this.alignment === "trailing" ? "comfortable" : this.density;
4542
+ }
4401
4543
  toggle() {
4402
4544
  if (this.state === "disabled") return;
4403
4545
  this.status = this.status === "checked" ? "unchecked" : "checked";
@@ -4445,12 +4587,22 @@ var RelCheckbox = class extends import_lit13.LitElement {
4445
4587
  </svg>
4446
4588
  </span>
4447
4589
 
4448
- ${this.icon ? import_lit13.html`<rel-icon name="${this.icon}" size="md" class="icon-prefix"></rel-icon>` : null}
4590
+ <span class="content">
4591
+ <span class="label-line">
4592
+ ${this.icon ? import_lit13.html`<rel-icon
4593
+ name="${this.icon}"
4594
+ size="${this.size === "small" ? "sm" : "md"}"
4595
+ class="icon-prefix"
4596
+ ></rel-icon>` : null}
4449
4597
 
4450
- <span part="label" class="label">${this.label}</span>
4598
+ <span part="label" class="label">${this.label}</span>
4599
+ </span>
4600
+
4601
+ ${this.effectiveDensity === "comfortable" && this.supportingtext ? import_lit13.html`<span part="helper" class="text">${this.supportingtext}</span>` : null}
4602
+ </span>
4451
4603
  </label>
4452
4604
 
4453
- ${this.supportingtext ? import_lit13.html`<span part="helper" class="text">${this.supportingtext}</span>` : null}
4605
+ ${this.effectiveDensity === "compact" && this.supportingtext ? import_lit13.html`<span part="helper" class="text text-below">${this.supportingtext}</span>` : null}
4454
4606
  </div>
4455
4607
  `;
4456
4608
  }
@@ -4483,6 +4635,29 @@ RelCheckbox.styles = import_lit13.css`
4483
4635
  cursor: pointer;
4484
4636
  }
4485
4637
 
4638
+ /* Trailing alignment — box moves to the far edge, label stack to the
4639
+ start. row-reverse keeps the box first in DOM order (input + .box
4640
+ adjacency drives the focus ring) while space-between stretches the
4641
+ row to its container. */
4642
+ :host([alignment="trailing"]) .row {
4643
+ flex-direction: row-reverse;
4644
+ justify-content: space-between;
4645
+ }
4646
+
4647
+ /* Comfortable density — label and supporting text stack beside the box. */
4648
+ .content {
4649
+ display: flex;
4650
+ flex-direction: column;
4651
+ gap: var(--rel-spacing-3xs);
4652
+ min-width: 0;
4653
+ }
4654
+
4655
+ .label-line {
4656
+ display: flex;
4657
+ align-items: center;
4658
+ gap: var(--rel-spacing-xs);
4659
+ }
4660
+
4486
4661
  input {
4487
4662
  position: absolute;
4488
4663
  inset: 0;
@@ -4625,7 +4800,6 @@ RelCheckbox.styles = import_lit13.css`
4625
4800
 
4626
4801
  .text {
4627
4802
  color: var(--rel-text-caption);
4628
- padding-left: 28px;
4629
4803
 
4630
4804
  font-family: var(--rel-body-sm-regular-font-family);
4631
4805
 
@@ -4638,6 +4812,44 @@ RelCheckbox.styles = import_lit13.css`
4638
4812
  letter-spacing: var(--rel-body-sm-regular-letter-spacing);
4639
4813
 
4640
4814
  }
4815
+
4816
+ /* Compact density — supporting text sits under the whole row, indented
4817
+ past the box (box width + row gap, matching the pre-existing 28px). */
4818
+ .text-below {
4819
+ padding-left: 28px;
4820
+ }
4821
+
4822
+ /* Size variant (medium is the base scale above). */
4823
+ :host([size="small"]) .box {
4824
+ width: 14px;
4825
+ height: 14px;
4826
+ border-radius: 3px;
4827
+ }
4828
+
4829
+ :host([size="small"]) svg {
4830
+ width: 10px;
4831
+ height: 10px;
4832
+ }
4833
+
4834
+ :host([size="small"]) .label {
4835
+ font-family: var(--rel-body-md-regular-font-family);
4836
+ font-weight: var(--rel-body-md-regular-font-weight);
4837
+ font-size: var(--rel-body-md-regular-font-size);
4838
+ line-height: var(--rel-body-md-regular-line-height);
4839
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
4840
+ }
4841
+
4842
+ :host([size="small"]) .text {
4843
+ font-family: var(--rel-body-xs-regular-font-family);
4844
+ font-weight: var(--rel-body-xs-regular-font-weight);
4845
+ font-size: var(--rel-body-xs-regular-font-size);
4846
+ line-height: var(--rel-body-xs-regular-line-height);
4847
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
4848
+ }
4849
+
4850
+ :host([size="small"]) .text-below {
4851
+ padding-left: 24px;
4852
+ }
4641
4853
  `;
4642
4854
  __decorateClass([
4643
4855
  (0, import_decorators13.property)({ type: String })
@@ -4657,6 +4869,15 @@ __decorateClass([
4657
4869
  __decorateClass([
4658
4870
  (0, import_decorators13.property)({ type: String })
4659
4871
  ], RelCheckbox.prototype, "value", 2);
4872
+ __decorateClass([
4873
+ (0, import_decorators13.property)({ type: String, reflect: true })
4874
+ ], RelCheckbox.prototype, "size", 2);
4875
+ __decorateClass([
4876
+ (0, import_decorators13.property)({ type: String, reflect: true })
4877
+ ], RelCheckbox.prototype, "density", 2);
4878
+ __decorateClass([
4879
+ (0, import_decorators13.property)({ type: String, reflect: true })
4880
+ ], RelCheckbox.prototype, "alignment", 2);
4660
4881
  RelCheckbox = __decorateClass([
4661
4882
  (0, import_decorators13.customElement)("rel-checkbox")
4662
4883
  ], RelCheckbox);
@@ -5788,6 +6009,31 @@ var RelRangePicker = class extends import_lit15.LitElement {
5788
6009
  this.viewMonth = (/* @__PURE__ */ new Date()).getMonth();
5789
6010
  this.isSelectingRange = false;
5790
6011
  this.hoverDate = null;
6012
+ this.onReposition = () => {
6013
+ if (this.open && this.mode === "popover") this.updatePopoverPosition();
6014
+ };
6015
+ this.updatePopoverPosition = () => {
6016
+ const panel = this.popoverEl;
6017
+ if (!panel || !this.open || this.mode !== "popover") return;
6018
+ const t = this.getBoundingClientRect();
6019
+ const c = panel.getBoundingClientRect();
6020
+ let top = t.bottom + 4;
6021
+ if (top + c.height > window.innerHeight - 8) {
6022
+ top = Math.max(8, t.top - c.height - 4);
6023
+ }
6024
+ let left = t.left;
6025
+ if (left + c.width > window.innerWidth - 8) {
6026
+ left = Math.max(8, window.innerWidth - c.width - 8);
6027
+ }
6028
+ panel.style.position = "fixed";
6029
+ panel.style.top = `${top}px`;
6030
+ panel.style.left = `${left}px`;
6031
+ const applied = panel.getBoundingClientRect();
6032
+ top -= applied.top - top;
6033
+ left -= applied.left - left;
6034
+ panel.style.top = `${top}px`;
6035
+ panel.style.left = `${left}px`;
6036
+ };
5791
6037
  // ── Open / close ──
5792
6038
  this.handleClickOutside = (e) => {
5793
6039
  if (!this.open) return;
@@ -5830,14 +6076,64 @@ var RelRangePicker = class extends import_lit15.LitElement {
5830
6076
  super.disconnectedCallback();
5831
6077
  document.removeEventListener("click", this.handleClickOutside);
5832
6078
  document.removeEventListener("keydown", this.handleGlobalKeydown);
6079
+ window.removeEventListener("resize", this.onReposition);
6080
+ window.removeEventListener("scroll", this.onReposition, true);
6081
+ this.hideTopLayer();
5833
6082
  }
5834
- updated() {
6083
+ updated(changed) {
5835
6084
  this.style.width = this.width;
5836
6085
  this.style.minWidth = this.minWidth;
5837
6086
  this.style.maxWidth = this.maxWidth;
5838
6087
  this.style.height = this.height;
5839
6088
  this.style.minHeight = this.minHeight;
5840
6089
  this.style.maxHeight = this.maxHeight;
6090
+ if (changed.has("open")) {
6091
+ if (this.open) {
6092
+ this.showTopLayer();
6093
+ window.addEventListener("resize", this.onReposition);
6094
+ window.addEventListener("scroll", this.onReposition, true);
6095
+ } else {
6096
+ window.removeEventListener("resize", this.onReposition);
6097
+ window.removeEventListener("scroll", this.onReposition, true);
6098
+ this.hideTopLayer();
6099
+ }
6100
+ }
6101
+ }
6102
+ /* A transformed ancestor (e.g. rel-modal's centered panel with
6103
+ * translate(-50%,-50%)) becomes the containing block for position:fixed
6104
+ * descendants, and its overflow:hidden clips position:absolute ones —
6105
+ * either way the picker shell breaks inside a modal. Promoting shell
6106
+ * (and popup-mode backdrop) to the browser top layer sidesteps both —
6107
+ * same fix as rel-popover / rel-autocomplete / rel-select. `manual`
6108
+ * keeps outside-click and Escape handling under this component's own
6109
+ * control. While in the top layer the CSS anchoring (absolute, below
6110
+ * the trigger) is meaningless, so popover mode gets inline fixed
6111
+ * coordinates from updatePopoverPosition(). */
6112
+ showTopLayer() {
6113
+ for (const el of [this.mode === "popup" ? this.backdropEl : void 0, this.popoverEl]) {
6114
+ if (!el || typeof el.showPopover !== "function") continue;
6115
+ try {
6116
+ if (!el.matches(":popover-open")) el.showPopover();
6117
+ } catch {
6118
+ }
6119
+ }
6120
+ if (this.mode === "popover") {
6121
+ requestAnimationFrame(() => this.updatePopoverPosition());
6122
+ }
6123
+ }
6124
+ hideTopLayer() {
6125
+ for (const el of [this.popoverEl, this.backdropEl]) {
6126
+ if (!el || typeof el.hidePopover !== "function") continue;
6127
+ try {
6128
+ if (el.matches(":popover-open")) el.hidePopover();
6129
+ } catch {
6130
+ }
6131
+ }
6132
+ if (this.popoverEl) {
6133
+ this.popoverEl.style.position = "";
6134
+ this.popoverEl.style.top = "";
6135
+ this.popoverEl.style.left = "";
6136
+ }
5841
6137
  }
5842
6138
  toggleOpen() {
5843
6139
  if (this.disabled) return;
@@ -6193,10 +6489,11 @@ var RelRangePicker = class extends import_lit15.LitElement {
6193
6489
  </button>
6194
6490
  `}
6195
6491
 
6196
- ${isPopup && this.open ? import_lit15.html`<div part="overlay" class="backdrop" @click=${this.cancelPopover}></div>` : import_lit15.nothing}
6492
+ ${isPopup && this.open ? import_lit15.html`<div part="overlay" class="backdrop" popover="manual" @click=${this.cancelPopover}></div>` : import_lit15.nothing}
6197
6493
 
6198
6494
  <div
6199
6495
  part="panel" class="popover ${this.open ? "open" : ""} ${isPopup ? "popup" : ""}"
6496
+ popover="manual"
6200
6497
  role="dialog"
6201
6498
  aria-label="Select date range"
6202
6499
  >
@@ -6407,6 +6704,17 @@ RelRangePicker.styles = import_lit15.css`
6407
6704
 
6408
6705
  /* ── Popover shell ── */
6409
6706
  .popover {
6707
+ /* Reset native [popover] UA styles first (the shell is promoted to
6708
+ the top layer so transformed / overflow:hidden ancestors — e.g. a
6709
+ centered rel-modal panel — can't hijack or clip it). While in the
6710
+ top layer, JS overrides the anchored position below with inline
6711
+ fixed coordinates; the CSS anchoring is the no-Popover-API
6712
+ fallback. */
6713
+ margin: 0;
6714
+ padding: 0;
6715
+ width: auto;
6716
+ height: auto;
6717
+ color: inherit;
6410
6718
  position: absolute;
6411
6719
  top: calc(100% + 4px);
6412
6720
  left: 0;
@@ -6431,6 +6739,13 @@ RelRangePicker.styles = import_lit15.css`
6431
6739
  .backdrop {
6432
6740
  position: fixed;
6433
6741
  inset: 0;
6742
+ /* Reset native [popover] UA styles — the backdrop is promoted to the
6743
+ top layer right before the panel so the panel stacks above it. */
6744
+ margin: 0;
6745
+ padding: 0;
6746
+ width: auto;
6747
+ height: auto;
6748
+ border: none;
6434
6749
  z-index: 2147483646;
6435
6750
  background: rgba(0,0,0,0.4);
6436
6751
  }
@@ -6804,6 +7119,12 @@ __decorateClass([
6804
7119
  __decorateClass([
6805
7120
  (0, import_decorators15.query)(".trigger-button")
6806
7121
  ], RelRangePicker.prototype, "triggerButton", 2);
7122
+ __decorateClass([
7123
+ (0, import_decorators15.query)(".popover")
7124
+ ], RelRangePicker.prototype, "popoverEl", 2);
7125
+ __decorateClass([
7126
+ (0, import_decorators15.query)(".backdrop")
7127
+ ], RelRangePicker.prototype, "backdropEl", 2);
6807
7128
  RelRangePicker = __decorateClass([
6808
7129
  (0, import_decorators15.customElement)("rel-range-picker")
6809
7130
  ], RelRangePicker);
@@ -8309,6 +8630,8 @@ var RelRadio = class extends import_lit23.LitElement {
8309
8630
  super(...arguments);
8310
8631
  this.options = [];
8311
8632
  this.name = "radio-group";
8633
+ this.orientation = "vertical";
8634
+ this.size = "medium";
8312
8635
  this.selectedValue = null;
8313
8636
  this.disabled = false;
8314
8637
  this.error = false;
@@ -8374,7 +8697,12 @@ var RelRadio = class extends import_lit23.LitElement {
8374
8697
  }
8375
8698
  render() {
8376
8699
  return import_lit23.html`
8377
- <div part="base" class="rel-radio-group">
8700
+ <div
8701
+ part="base"
8702
+ class="rel-radio-group"
8703
+ role="radiogroup"
8704
+ aria-orientation="${this.orientation}"
8705
+ >
8378
8706
  ${this.options.map((o) => this.renderOption(o))}
8379
8707
  </div>
8380
8708
  ${this.error && this.errorMessage ? import_lit23.html`<div class="error-message">${this.errorMessage}</div>` : ""}
@@ -8395,6 +8723,15 @@ RelRadio.styles = import_lit23.css`
8395
8723
  gap: var(--rel-spacing-xs);
8396
8724
  }
8397
8725
 
8726
+ /* Horizontal orientation — options flow in a wrapping row. Wider column
8727
+ gap than the vertical rhythm so adjacent option labels read as separate
8728
+ choices; row gap keeps the vertical value for wrapped lines. */
8729
+ :host([orientation="horizontal"]) .rel-radio-group {
8730
+ flex-direction: row;
8731
+ flex-wrap: wrap;
8732
+ gap: var(--rel-spacing-xs) var(--rel-spacing-xl);
8733
+ }
8734
+
8398
8735
  .rel-radio-option {
8399
8736
  display: inline-flex;
8400
8737
  flex-direction: column;
@@ -8497,6 +8834,34 @@ RelRadio.styles = import_lit23.css`
8497
8834
 
8498
8835
  }
8499
8836
 
8837
+ /* Size variant (medium is the base scale above). Supporting-text indent =
8838
+ circle width + the radio-main gap, so it stays aligned under the label. */
8839
+ :host([size="small"]) .radio-custom {
8840
+ width: 16px;
8841
+ height: 16px;
8842
+ }
8843
+
8844
+ :host([size="small"]) input:checked + .radio-custom::after {
8845
+ inset: 3px;
8846
+ }
8847
+
8848
+ :host([size="small"]) .radio-label {
8849
+ font-family: var(--rel-body-md-regular-font-family);
8850
+ font-weight: var(--rel-body-md-regular-font-weight);
8851
+ font-size: var(--rel-body-md-regular-font-size);
8852
+ line-height: var(--rel-body-md-regular-line-height);
8853
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
8854
+ }
8855
+
8856
+ :host([size="small"]) .radio-supporting {
8857
+ margin-left: 24px;
8858
+ font-family: var(--rel-body-xs-regular-font-family);
8859
+ font-weight: var(--rel-body-xs-regular-font-weight);
8860
+ font-size: var(--rel-body-xs-regular-font-size);
8861
+ line-height: var(--rel-body-xs-regular-line-height);
8862
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
8863
+ }
8864
+
8500
8865
  /* Error state (Figma State=Error, both Is Checked values) */
8501
8866
  .rel-radio-option.error-unchecked .radio-custom,
8502
8867
  .rel-radio-option.error-checked .radio-custom {
@@ -8540,6 +8905,12 @@ __decorateClass([
8540
8905
  __decorateClass([
8541
8906
  (0, import_decorators23.property)({ type: String })
8542
8907
  ], RelRadio.prototype, "name", 2);
8908
+ __decorateClass([
8909
+ (0, import_decorators23.property)({ type: String, reflect: true })
8910
+ ], RelRadio.prototype, "orientation", 2);
8911
+ __decorateClass([
8912
+ (0, import_decorators23.property)({ type: String, reflect: true })
8913
+ ], RelRadio.prototype, "size", 2);
8543
8914
  __decorateClass([
8544
8915
  (0, import_decorators23.property)({ type: String, reflect: true })
8545
8916
  ], RelRadio.prototype, "selectedValue", 2);
@@ -11358,6 +11729,14 @@ var RelPagination = class extends import_lit38.LitElement {
11358
11729
  }
11359
11730
  return pages;
11360
11731
  }
11732
+ /** Abbreviate large page numbers (368178 → "368k") so buttons stay compact.
11733
+ Exact below 10,000; floored to k up to 1M; then M with one decimal. */
11734
+ formatPageLabel(n) {
11735
+ if (n < 1e4) return String(n);
11736
+ if (n < 1e6) return `${Math.floor(n / 1e3)}k`;
11737
+ const m = Math.floor(n / 1e5) / 10;
11738
+ return `${Number.isInteger(m) ? m.toFixed(0) : m}M`;
11739
+ }
11361
11740
  handlePageChange(newPage) {
11362
11741
  const total = this.computedTotalPages;
11363
11742
  if (this.disabled || newPage < 1 || newPage > total || newPage === this.activePage) return;
@@ -11403,15 +11782,20 @@ var RelPagination = class extends import_lit38.LitElement {
11403
11782
  <div class="pag-stack">
11404
11783
  ${this.renderNavBtn("prev")}
11405
11784
  <div class="pag-numbers">
11406
- ${pages.map(
11407
- (p) => p === -1 ? import_lit38.html`<span class="pag-ellipsis">...</span>` : import_lit38.html`<button
11408
- type="button"
11409
- class="pag-page-btn ${Math.abs(p - current) <= 1 ? "near" : ""} ${p === 1 || p === total ? "edge" : ""}"
11410
- aria-label="Page ${p}"
11411
- aria-current="${p === current ? "page" : "false"}"
11412
- @click="${() => this.handlePageChange(p)}"
11413
- >${p}</button>`
11414
- )}
11785
+ ${pages.map((p) => {
11786
+ if (p === -1) return import_lit38.html`<span class="pag-ellipsis">...</span>`;
11787
+ const isNear = Math.abs(p - current) <= 1;
11788
+ const isEdge = p === 1 || p === total;
11789
+ const label = isEdge && !isNear ? this.formatPageLabel(p) : String(p);
11790
+ return import_lit38.html`<button
11791
+ type="button"
11792
+ class="pag-page-btn ${isNear ? "near" : ""} ${p === 1 || p === total ? "edge" : ""}"
11793
+ aria-label="Page ${p}"
11794
+ title="${label === String(p) ? import_lit38.nothing : `Page ${p}`}"
11795
+ aria-current="${p === current ? "page" : "false"}"
11796
+ @click="${() => this.handlePageChange(p)}"
11797
+ >${label}</button>`;
11798
+ })}
11415
11799
  </div>
11416
11800
  ${this.renderNavBtn("next")}
11417
11801
  </div>
@@ -11564,9 +11948,10 @@ RelPagination.styles = import_lit38.css`
11564
11948
  display: inline-flex;
11565
11949
  align-items: center;
11566
11950
  justify-content: center;
11567
- width: 40px;
11951
+ width: auto;
11952
+ min-width: 40px;
11568
11953
  height: 40px;
11569
- padding: var(--rel-spacing-xs);
11954
+ padding: 0 var(--rel-spacing-xs);
11570
11955
  border-radius: var(--rel-border-radius-sm);
11571
11956
  border: 1px solid var(--rel-border-default);
11572
11957
  background: var(--rel-bg-neutral);
@@ -13139,6 +13524,7 @@ var RelSelect = class extends import_lit46.LitElement {
13139
13524
  this.internalState = "default";
13140
13525
  this.visibleChipCount = Infinity;
13141
13526
  this.uniqueId = `select-${Math.random().toString(36).slice(2)}`;
13527
+ this._warnedNoName = false;
13142
13528
  this.onOutsidePointerDown = (e) => {
13143
13529
  if (!this.open) return;
13144
13530
  const path = e.composedPath();
@@ -13168,6 +13554,14 @@ var RelSelect = class extends import_lit46.LitElement {
13168
13554
  if (left + t.width > window.innerWidth - 8) {
13169
13555
  left = Math.max(8, window.innerWidth - t.width - 8);
13170
13556
  }
13557
+ this.listboxEl.style.top = `${top}px`;
13558
+ this.listboxEl.style.left = `${left}px`;
13559
+ this.listboxEl.style.width = `${t.width}px`;
13560
+ const applied = this.listboxEl.getBoundingClientRect();
13561
+ top -= applied.top - top;
13562
+ left -= applied.left - left;
13563
+ this.listboxEl.style.top = `${top}px`;
13564
+ this.listboxEl.style.left = `${left}px`;
13171
13565
  this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
13172
13566
  };
13173
13567
  this.onSearchInput = (e) => {
@@ -13185,6 +13579,7 @@ var RelSelect = class extends import_lit46.LitElement {
13185
13579
  disconnectedCallback() {
13186
13580
  super.disconnectedCallback();
13187
13581
  this.removeGlobalListeners();
13582
+ this.hideNativePopover();
13188
13583
  this.chipResizeObserver?.disconnect();
13189
13584
  }
13190
13585
  firstUpdated() {
@@ -13197,6 +13592,13 @@ var RelSelect = class extends import_lit46.LitElement {
13197
13592
  }
13198
13593
  }
13199
13594
  updated(changed) {
13595
+ if (!this._warnedNoName && this.labelStyle === "none" && !this.fieldAriaLabel && !this.fieldAriaLabelledby && !this.getAttribute("aria-label")) {
13596
+ this._warnedNoName = true;
13597
+ console.warn(
13598
+ '<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.',
13599
+ this
13600
+ );
13601
+ }
13200
13602
  if (changed.has("selectedValues") || changed.has("options") || changed.has("multiple") || changed.has("size")) {
13201
13603
  requestAnimationFrame(() => this.computeVisibleChips());
13202
13604
  }
@@ -13294,6 +13696,7 @@ var RelSelect = class extends import_lit46.LitElement {
13294
13696
  this.highlightedIndex = -1;
13295
13697
  this.addGlobalListeners();
13296
13698
  requestAnimationFrame(() => {
13699
+ this.showNativePopover();
13297
13700
  this.updatePosition();
13298
13701
  if (!this.noSearch) {
13299
13702
  this.searchInputEl?.focus({ preventScroll: true });
@@ -13304,8 +13707,32 @@ var RelSelect = class extends import_lit46.LitElement {
13304
13707
  if (!this.open) return;
13305
13708
  this.open = false;
13306
13709
  this.removeGlobalListeners();
13710
+ this.hideNativePopover();
13307
13711
  this.triggerEl?.focus();
13308
13712
  }
13713
+ /* A transformed ancestor (e.g. rel-modal's centered panel with
13714
+ * translate(-50%,-50%)) becomes the containing block for position:fixed
13715
+ * descendants, breaking viewport-based coordinates and letting the
13716
+ * panel's overflow:hidden clip the listbox. Promoting the listbox to
13717
+ * the browser top layer sidesteps both — same fix as rel-popover /
13718
+ * rel-autocomplete. `manual` keeps light-dismiss and Escape handling
13719
+ * under this component's own control. */
13720
+ showNativePopover() {
13721
+ const p = this.listboxEl;
13722
+ if (!p || typeof p.showPopover !== "function") return;
13723
+ try {
13724
+ if (!p.matches(":popover-open")) p.showPopover();
13725
+ } catch {
13726
+ }
13727
+ }
13728
+ hideNativePopover() {
13729
+ const p = this.listboxEl;
13730
+ if (!p || typeof p.hidePopover !== "function") return;
13731
+ try {
13732
+ if (p.matches(":popover-open")) p.hidePopover();
13733
+ } catch {
13734
+ }
13735
+ }
13309
13736
  selectSingleOption(option) {
13310
13737
  this.selected = option.value;
13311
13738
  if (this.state === "error") this.state = "default";
@@ -13445,7 +13872,9 @@ var RelSelect = class extends import_lit46.LitElement {
13445
13872
  const opts = this.normalizedOptions;
13446
13873
  const filtered = this.filteredOptions;
13447
13874
  const isDisabled = this.state === "disabled";
13875
+ const labelHidden = this.labelStyle === "none";
13448
13876
  const showFloatingChrome = this.labelStyle === "floating" && !!this.label;
13877
+ const accessibleName = (labelHidden ? this.fieldAriaLabel : this.label) || "";
13449
13878
  const wrapperClasses = [
13450
13879
  "select-wrapper",
13451
13880
  this.size,
@@ -13461,7 +13890,7 @@ var RelSelect = class extends import_lit46.LitElement {
13461
13890
  const selectedSingle = opts.find((o) => o.value === this.selected);
13462
13891
  return import_lit46.html`
13463
13892
  <div class="${wrapperClasses}">
13464
- ${!showFloatingChrome && this.label ? import_lit46.html`<label part="label" class="select-label" for="${this.uniqueId}">${this.label}</label>` : ""}
13893
+ ${labelHidden ? import_lit46.html`<div class="label-reserve" aria-hidden="true"></div>` : !showFloatingChrome && this.label ? import_lit46.html`<label part="label" class="select-label" for="${this.uniqueId}">${this.label}</label>` : ""}
13465
13894
 
13466
13895
  <div class="select-container">
13467
13896
  ${showFloatingChrome ? import_lit46.html`<span class="floating-label${this.leftIcon ? " has-left-icon" : ""}"
@@ -13478,7 +13907,8 @@ var RelSelect = class extends import_lit46.LitElement {
13478
13907
  aria-controls="${listboxId}"
13479
13908
  aria-activedescendant="${activeOptionId}"
13480
13909
  aria-invalid="${this.state === "error"}"
13481
- aria-label="${this.label || this.placeholder}"
13910
+ aria-label="${this.fieldAriaLabelledby ? import_lit46.nothing : accessibleName || this.placeholder}"
13911
+ aria-labelledby="${this.fieldAriaLabelledby ?? import_lit46.nothing}"
13482
13912
  ?disabled="${isDisabled}"
13483
13913
  @click="${this.onTriggerClick}"
13484
13914
  @keydown="${this.onTriggerKeyDown}"
@@ -13513,39 +13943,42 @@ var RelSelect = class extends import_lit46.LitElement {
13513
13943
  <div
13514
13944
  id="${listboxId}"
13515
13945
  part="panel list" class="select-listbox"
13946
+ popover="manual"
13516
13947
  role="listbox"
13517
13948
  aria-multiselectable="${this.multiple}"
13518
- aria-label="${this.label || "Options"}"
13949
+ aria-label="${accessibleName || "Options"}"
13519
13950
  style="${this.panelStyle}"
13520
13951
  ?data-open="${this.open}"
13521
13952
  >
13522
- ${!this.noSearch ? import_lit46.html`<div class="select-search">
13523
- <div class="search-input-wrap">
13524
- <rel-icon name="search" size="${this.searchIconSize}" class="icon"></rel-icon>
13525
- <input
13526
- part="input" class="search-input"
13527
- type="text"
13528
- placeholder="${this.searchPlaceholder}"
13529
- .value="${this.searchValue}"
13530
- @input="${this.onSearchInput}"
13531
- @keydown="${(e) => this.onSearchKeyDown(e)}"
13532
- />
13533
- </div>
13953
+ ${!this.noSearch || this.multiple && filtered.length > 0 ? import_lit46.html`<div class="select-header">
13954
+ ${!this.noSearch ? import_lit46.html`<div class="select-search">
13955
+ <div class="search-input-wrap">
13956
+ <rel-icon name="search" size="${this.searchIconSize}" class="icon"></rel-icon>
13957
+ <input
13958
+ part="input" class="search-input"
13959
+ type="text"
13960
+ placeholder="${this.searchPlaceholder}"
13961
+ .value="${this.searchValue}"
13962
+ @input="${this.onSearchInput}"
13963
+ @keydown="${(e) => this.onSearchKeyDown(e)}"
13964
+ />
13965
+ </div>
13966
+ </div>` : ""}
13967
+ ${this.multiple && filtered.length > 0 ? import_lit46.html`<div class="select-all-row">
13968
+ <rel-checkbox
13969
+ label="${this.selectAllLabel}"
13970
+ status="${this.selectAllStatus}"
13971
+ @rel-change=${(e) => {
13972
+ e.stopPropagation();
13973
+ this.toggleSelectAll();
13974
+ }}
13975
+ ></rel-checkbox>
13976
+ </div>` : ""}
13534
13977
  </div>` : ""}
13535
13978
 
13536
13979
  ${filtered.length === 0 ? import_lit46.html`<div class="select-empty">
13537
13980
  ${opts.length === 0 ? "No options available" : "No matches found"}
13538
13981
  </div>` : this.multiple ? import_lit46.html`
13539
- <div class="select-all-row">
13540
- <rel-checkbox
13541
- label="${this.selectAllLabel}"
13542
- status="${this.selectAllStatus}"
13543
- @rel-change=${(e) => {
13544
- e.stopPropagation();
13545
- this.toggleSelectAll();
13546
- }}
13547
- ></rel-checkbox>
13548
- </div>
13549
13982
  <div class="select-options">
13550
13983
  ${(0, import_repeat2.repeat)(
13551
13984
  filtered,
@@ -13644,6 +14077,22 @@ RelSelect.styles = import_lit46.css`
13644
14077
 
13645
14078
  }
13646
14079
 
14080
+ /* ── labelStyle="none" ──
14081
+ No label renders. An empty spacer holds the label row's full height so the
14082
+ trigger still bottom-aligns with labelled siblings in a form row.
14083
+ The reserve covers label line-height + the wrapper gap, and the negative
14084
+ margin cancels the flex gap that would otherwise sit below it — so
14085
+ --rel-field-label-reserve: 0 collapses the row to genuinely zero
14086
+ (toolbars, table cells), not just to the leftover gap. */
14087
+ .label-reserve {
14088
+ flex: none;
14089
+ height: var(
14090
+ --rel-field-label-reserve,
14091
+ calc(var(--rel-body-sm-regular-line-height) + var(--rel-spacing-2xs))
14092
+ );
14093
+ margin-bottom: calc(-1 * var(--rel-spacing-2xs));
14094
+ }
14095
+
13647
14096
  /* Error only colors the border/focus ring and the supporting text below
13648
14097
  the field — label, floating label, placeholder, and icons stay normal. */
13649
14098
 
@@ -13872,6 +14321,15 @@ RelSelect.styles = import_lit46.css`
13872
14321
  position: fixed;
13873
14322
  z-index: 1001;
13874
14323
  display: none;
14324
+ /* Reset native [popover] UA styles (the listbox is promoted to the
14325
+ top layer so transformed ancestors — e.g. a centered rel-modal
14326
+ panel — can't hijack its containing block or clip it). */
14327
+ inset: auto;
14328
+ margin: 0;
14329
+ padding: 0;
14330
+ width: auto;
14331
+ height: auto;
14332
+ color: inherit;
13875
14333
  box-sizing: border-box;
13876
14334
  background: var(--rel-select-bg, var(--rel-bg-neutral));
13877
14335
  border: 1px solid var(--rel-select-menu-border, var(--rel-border-default));
@@ -13888,10 +14346,18 @@ RelSelect.styles = import_lit46.css`
13888
14346
  display: block;
13889
14347
  }
13890
14348
 
13891
- .select-search {
14349
+ /* Search and Select All pin together while the options scroll beneath.
14350
+ z-index is load-bearing: multiselect rows contain rel-checkbox, whose
14351
+ .row wrapper is position:relative (Tab-scroll fix), so without it the
14352
+ positioned rows paint over the sticky header in DOM order. */
14353
+ .select-header {
13892
14354
  position: sticky;
13893
14355
  top: 0;
14356
+ z-index: 1;
13894
14357
  background: var(--rel-select-bg, var(--rel-bg-neutral));
14358
+ }
14359
+
14360
+ .select-search {
13895
14361
  padding: var(--rel-spacing-xs);
13896
14362
  border-bottom: 1px solid var(--rel-select-divider, var(--rel-border-subtle));
13897
14363
  }
@@ -14037,8 +14503,14 @@ __decorateClass([
14037
14503
  (0, import_decorators46.property)({ type: String })
14038
14504
  ], RelSelect.prototype, "label", 2);
14039
14505
  __decorateClass([
14040
- (0, import_decorators46.property)({ type: String, attribute: "label-style" })
14506
+ (0, import_decorators46.property)({ type: String, attribute: "label-style", reflect: true })
14041
14507
  ], RelSelect.prototype, "labelStyle", 2);
14508
+ __decorateClass([
14509
+ (0, import_decorators46.property)({ type: String, attribute: "field-aria-label" })
14510
+ ], RelSelect.prototype, "fieldAriaLabel", 2);
14511
+ __decorateClass([
14512
+ (0, import_decorators46.property)({ type: String, attribute: "field-aria-labelledby" })
14513
+ ], RelSelect.prototype, "fieldAriaLabelledby", 2);
14042
14514
  __decorateClass([
14043
14515
  (0, import_decorators46.property)({ type: Array })
14044
14516
  ], RelSelect.prototype, "options", 2);
@@ -16637,111 +17109,627 @@ RelTab = __decorateClass([
16637
17109
  (0, import_decorators53.customElement)("rel-tab")
16638
17110
  ], RelTab);
16639
17111
 
16640
- // src/components/rel-toast/rel-toast.ts
17112
+ // src/components/rel-time-picker/rel-time-picker.ts
16641
17113
  var import_lit54 = require("lit");
16642
17114
  var import_decorators54 = require("lit/decorators.js");
16643
- var RelToast = class extends import_lit54.LitElement {
17115
+ var import_rel_icon16 = require("@reliability-design/icons/rel-icon");
17116
+ var RelTimePicker = class extends import_lit54.LitElement {
16644
17117
  constructor() {
16645
17118
  super(...arguments);
16646
- this.message = "";
16647
- this.type = "default";
16648
- this.duration = 4e3;
16649
- this.show = false;
16650
- this.visible = false;
16651
- this._dismissTimer = null;
16652
- }
16653
- willUpdate(changed) {
16654
- if (!changed.has("show")) return;
16655
- if (this.show) {
16656
- this.visible = true;
16657
- if (this._dismissTimer) clearTimeout(this._dismissTimer);
16658
- if (this.duration > 0) {
16659
- this._dismissTimer = setTimeout(() => this.close(), this.duration);
16660
- }
16661
- } else {
16662
- this.visible = false;
16663
- if (this._dismissTimer) {
16664
- clearTimeout(this._dismissTimer);
16665
- this._dismissTimer = null;
17119
+ this.value = null;
17120
+ this.placeholder = "Select time";
17121
+ this.label = null;
17122
+ this.disabled = false;
17123
+ this.required = false;
17124
+ this.readonly = false;
17125
+ this.format = "12h";
17126
+ this.minuteStep = 1;
17127
+ this.minTime = null;
17128
+ this.maxTime = null;
17129
+ this.open = false;
17130
+ this.clearable = false;
17131
+ this.error = false;
17132
+ this.invalid = false;
17133
+ this.size = "medium";
17134
+ this.variant = "outlined";
17135
+ this.fullWidth = false;
17136
+ this.draftHour = null;
17137
+ this.draftMinute = null;
17138
+ this.handleClickOutside = (e) => {
17139
+ if (!this.open) return;
17140
+ const path = e.composedPath();
17141
+ if (!path.includes(this)) {
17142
+ this.cancelPopover();
16666
17143
  }
16667
- }
17144
+ };
17145
+ this.handleEscapeKey = (e) => {
17146
+ if (e.key === "Escape" && this.open) {
17147
+ this.cancelPopover();
17148
+ }
17149
+ };
17150
+ }
17151
+ connectedCallback() {
17152
+ super.connectedCallback();
17153
+ document.addEventListener("click", this.handleClickOutside);
17154
+ document.addEventListener("keydown", this.handleEscapeKey);
16668
17155
  }
16669
17156
  disconnectedCallback() {
16670
17157
  super.disconnectedCallback();
16671
- if (this._dismissTimer) clearTimeout(this._dismissTimer);
17158
+ document.removeEventListener("click", this.handleClickOutside);
17159
+ document.removeEventListener("keydown", this.handleEscapeKey);
16672
17160
  }
16673
- open() {
16674
- this.show = true;
17161
+ updated(changedProperties) {
17162
+ if (changedProperties.has("open") && this.open) {
17163
+ this.resetDraftFromValue();
17164
+ setTimeout(() => {
17165
+ this.scrollColumnsToSelection();
17166
+ const target = this.columnsElement?.querySelector(
17167
+ ".option-button.selected:not(:disabled)"
17168
+ ) ?? this.columnsElement?.querySelector(
17169
+ ".option-button:not(:disabled)"
17170
+ );
17171
+ target?.focus();
17172
+ }, 0);
17173
+ }
16675
17174
  }
16676
- close() {
16677
- this.show = false;
16678
- this.visible = false;
16679
- if (this._dismissTimer) {
16680
- clearTimeout(this._dismissTimer);
16681
- this._dismissTimer = null;
17175
+ // ── Parsing / formatting ──
17176
+ parseTime(time) {
17177
+ if (!time) return null;
17178
+ const match = time.match(/^(\d{1,2}):(\d{2})$/);
17179
+ if (!match) return null;
17180
+ const hour = parseInt(match[1], 10);
17181
+ const minute = parseInt(match[2], 10);
17182
+ if (hour > 23 || minute > 59) return null;
17183
+ return { hour, minute };
17184
+ }
17185
+ formatValue(hour, minute) {
17186
+ return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
17187
+ }
17188
+ formatDisplay(hour, minute) {
17189
+ const mm = String(minute).padStart(2, "0");
17190
+ if (this.format === "24h") {
17191
+ return `${String(hour).padStart(2, "0")}:${mm}`;
17192
+ }
17193
+ const period = hour < 12 ? "AM" : "PM";
17194
+ const hour12 = hour % 12 === 0 ? 12 : hour % 12;
17195
+ return `${hour12}:${mm} ${period}`;
17196
+ }
17197
+ toMinutes(hour, minute) {
17198
+ return hour * 60 + minute;
17199
+ }
17200
+ get minuteStepSafe() {
17201
+ const step = Math.floor(this.minuteStep);
17202
+ return step >= 1 && step <= 60 ? step : 1;
17203
+ }
17204
+ get minBoundMinutes() {
17205
+ const parsed = this.parseTime(this.minTime);
17206
+ return parsed ? this.toMinutes(parsed.hour, parsed.minute) : 0;
17207
+ }
17208
+ get maxBoundMinutes() {
17209
+ const parsed = this.parseTime(this.maxTime);
17210
+ return parsed ? this.toMinutes(parsed.hour, parsed.minute) : 23 * 60 + 59;
17211
+ }
17212
+ isTimeInRange(hour, minute) {
17213
+ const total = this.toMinutes(hour, minute);
17214
+ return total >= this.minBoundMinutes && total <= this.maxBoundMinutes;
17215
+ }
17216
+ /** Minutes shown in the minutes column (respecting minuteStep). */
17217
+ getMinuteOptions() {
17218
+ const minutes = [];
17219
+ for (let m = 0; m < 60; m += this.minuteStepSafe) {
17220
+ minutes.push(m);
17221
+ }
17222
+ return minutes;
17223
+ }
17224
+ /** An hour is enabled if at least one of its step minutes is in range. */
17225
+ isHourEnabled(hour) {
17226
+ return this.getMinuteOptions().some((m) => this.isTimeInRange(hour, m));
17227
+ }
17228
+ isPeriodEnabled(period) {
17229
+ const start = period === "AM" ? 0 : 12;
17230
+ for (let h = start; h < start + 12; h++) {
17231
+ if (this.isHourEnabled(h)) return true;
17232
+ }
17233
+ return false;
17234
+ }
17235
+ /** Hour column entries: display label + the 0–23 hour it maps to. */
17236
+ getHourOptions() {
17237
+ if (this.format === "24h") {
17238
+ return Array.from({ length: 24 }, (_, h) => ({
17239
+ label: String(h).padStart(2, "0"),
17240
+ hour24: h
17241
+ }));
17242
+ }
17243
+ const period = this.draftPeriod ?? "AM";
17244
+ const base = period === "AM" ? 0 : 12;
17245
+ return Array.from({ length: 12 }, (_, i) => ({
17246
+ label: i === 0 ? "12" : String(i).padStart(2, "0"),
17247
+ hour24: base + i
17248
+ }));
17249
+ }
17250
+ get draftPeriod() {
17251
+ if (this.draftHour === null) return null;
17252
+ return this.draftHour < 12 ? "AM" : "PM";
17253
+ }
17254
+ resetDraftFromValue() {
17255
+ const parsed = this.parseTime(this.value);
17256
+ this.draftHour = parsed ? parsed.hour : null;
17257
+ this.draftMinute = parsed ? parsed.minute : null;
17258
+ }
17259
+ /** Snap the draft minute to the nearest valid step minute for the hour. */
17260
+ clampDraftToRange() {
17261
+ if (this.draftHour === null || this.draftMinute === null) return;
17262
+ if (this.isTimeInRange(this.draftHour, this.draftMinute)) return;
17263
+ const hour = this.draftHour;
17264
+ const valid = this.getMinuteOptions().filter((m) => this.isTimeInRange(hour, m));
17265
+ if (valid.length === 0) {
17266
+ this.draftMinute = null;
17267
+ return;
16682
17268
  }
17269
+ const current = this.draftMinute;
17270
+ this.draftMinute = valid.reduce(
17271
+ (best, m) => Math.abs(m - current) < Math.abs(best - current) ? m : best
17272
+ );
17273
+ }
17274
+ // ── Open / close / commit ──
17275
+ toggleTimePopover() {
17276
+ if (this.disabled || this.readonly) return;
17277
+ this.open = !this.open;
16683
17278
  this.dispatchEvent(
16684
- new CustomEvent("dismissed", {
17279
+ new CustomEvent("rel-open-change", {
17280
+ detail: { open: this.open },
16685
17281
  bubbles: true,
16686
17282
  composed: true
16687
17283
  })
16688
17284
  );
16689
17285
  }
16690
- handleActionClick(event) {
16691
- event.stopPropagation();
17286
+ /** Close discarding the draft (Cancel / Escape / outside click). */
17287
+ cancelPopover() {
17288
+ this.open = false;
17289
+ this.triggerButton?.focus();
16692
17290
  this.dispatchEvent(
16693
- new CustomEvent("action", {
17291
+ new CustomEvent("rel-open-change", {
17292
+ detail: { open: false },
16694
17293
  bubbles: true,
16695
17294
  composed: true
16696
17295
  })
16697
17296
  );
16698
- this.close();
16699
17297
  }
17298
+ applySelection() {
17299
+ if (this.draftHour === null) return;
17300
+ const newValue = this.formatValue(this.draftHour, this.draftMinute ?? 0);
17301
+ this.value = newValue;
17302
+ this.dispatchEvent(
17303
+ new CustomEvent("rel-change", {
17304
+ detail: { value: newValue },
17305
+ bubbles: true,
17306
+ composed: true
17307
+ })
17308
+ );
17309
+ this.cancelPopover();
17310
+ }
17311
+ /** Set the draft to the current time, snapped to the minute step and range. */
17312
+ selectNow() {
17313
+ const now = /* @__PURE__ */ new Date();
17314
+ const step = this.minuteStepSafe;
17315
+ this.draftHour = now.getHours();
17316
+ this.draftMinute = Math.round(now.getMinutes() / step) * step;
17317
+ if (this.draftMinute >= 60) {
17318
+ this.draftMinute = 0;
17319
+ this.draftHour = (this.draftHour + 1) % 24;
17320
+ }
17321
+ if (!this.isHourEnabled(this.draftHour)) {
17322
+ const enabled = [];
17323
+ for (let h = 0; h < 24; h++) if (this.isHourEnabled(h)) enabled.push(h);
17324
+ if (enabled.length === 0) return;
17325
+ const currentHour = this.draftHour;
17326
+ this.draftHour = enabled.reduce(
17327
+ (best, h) => Math.abs(h - currentHour) < Math.abs(best - currentHour) ? h : best
17328
+ );
17329
+ }
17330
+ this.clampDraftToRange();
17331
+ this.scrollColumnsToSelection();
17332
+ }
17333
+ clearValue(e) {
17334
+ e.stopPropagation();
17335
+ this.value = null;
17336
+ this.draftHour = null;
17337
+ this.draftMinute = null;
17338
+ this.dispatchEvent(
17339
+ new CustomEvent("rel-change", {
17340
+ detail: { value: null },
17341
+ bubbles: true,
17342
+ composed: true
17343
+ })
17344
+ );
17345
+ }
17346
+ // ── Column selection ──
17347
+ selectHour(hour24) {
17348
+ if (!this.isHourEnabled(hour24)) return;
17349
+ this.draftHour = hour24;
17350
+ if (this.draftMinute === null) this.draftMinute = 0;
17351
+ this.clampDraftToRange();
17352
+ }
17353
+ selectMinute(minute) {
17354
+ if (this.draftHour === null) {
17355
+ for (let h = 0; h < 24; h++) {
17356
+ if (this.isHourEnabled(h)) {
17357
+ this.draftHour = h;
17358
+ break;
17359
+ }
17360
+ }
17361
+ if (this.draftHour === null) return;
17362
+ }
17363
+ if (!this.isTimeInRange(this.draftHour, minute)) return;
17364
+ this.draftMinute = minute;
17365
+ }
17366
+ selectPeriod(period) {
17367
+ if (!this.isPeriodEnabled(period)) return;
17368
+ const base = period === "AM" ? 0 : 12;
17369
+ const hour12 = this.draftHour === null ? 0 : this.draftHour % 12;
17370
+ this.draftHour = base + hour12;
17371
+ if (this.draftMinute === null) this.draftMinute = 0;
17372
+ if (!this.isHourEnabled(this.draftHour)) {
17373
+ const enabled = [];
17374
+ for (let h = base; h < base + 12; h++) if (this.isHourEnabled(h)) enabled.push(h);
17375
+ const currentHour = this.draftHour;
17376
+ this.draftHour = enabled.reduce(
17377
+ (best, h) => Math.abs(h - currentHour) < Math.abs(best - currentHour) ? h : best
17378
+ );
17379
+ }
17380
+ this.clampDraftToRange();
17381
+ this.scrollColumnsToSelection();
17382
+ }
17383
+ scrollColumnsToSelection() {
17384
+ this.updateComplete.then(() => {
17385
+ this.columnsElement?.querySelectorAll(".option-button.selected").forEach((btn) => btn.scrollIntoView({ block: "center" }));
17386
+ });
17387
+ }
17388
+ // ── Keyboard ──
17389
+ handleTriggerKeydown(e) {
17390
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
17391
+ e.preventDefault();
17392
+ if (!this.open) {
17393
+ this.toggleTimePopover();
17394
+ }
17395
+ }
17396
+ }
17397
+ /**
17398
+ * Column navigation: Up/Down move within the column (and select), Left/Right
17399
+ * jump between columns, Home/End go to the first/last option, Enter applies.
17400
+ */
17401
+ handleOptionKeydown(e, column) {
17402
+ const list = e.currentTarget.closest(".column-list");
17403
+ if (!list) return;
17404
+ const buttons = Array.from(
17405
+ list.querySelectorAll(".option-button:not(:disabled)")
17406
+ );
17407
+ const index = buttons.indexOf(e.currentTarget);
17408
+ let target;
17409
+ switch (e.key) {
17410
+ case "ArrowUp":
17411
+ e.preventDefault();
17412
+ target = buttons[Math.max(0, index - 1)];
17413
+ break;
17414
+ case "ArrowDown":
17415
+ e.preventDefault();
17416
+ target = buttons[Math.min(buttons.length - 1, index + 1)];
17417
+ break;
17418
+ case "Home":
17419
+ e.preventDefault();
17420
+ target = buttons[0];
17421
+ break;
17422
+ case "End":
17423
+ e.preventDefault();
17424
+ target = buttons[buttons.length - 1];
17425
+ break;
17426
+ case "ArrowLeft":
17427
+ case "ArrowRight": {
17428
+ e.preventDefault();
17429
+ this.focusAdjacentColumn(column, e.key === "ArrowRight" ? 1 : -1);
17430
+ return;
17431
+ }
17432
+ case "Enter":
17433
+ e.preventDefault();
17434
+ e.currentTarget.click();
17435
+ this.applySelection();
17436
+ return;
17437
+ default:
17438
+ return;
17439
+ }
17440
+ if (target && target !== buttons[index]) {
17441
+ target.click();
17442
+ target.focus();
17443
+ target.scrollIntoView({ block: "nearest" });
17444
+ }
17445
+ }
17446
+ focusAdjacentColumn(from, direction) {
17447
+ const order = this.format === "12h" ? ["hour", "minute", "period"] : ["hour", "minute"];
17448
+ const nextIndex = order.indexOf(from) + direction;
17449
+ if (nextIndex < 0 || nextIndex >= order.length) return;
17450
+ const lists = this.columnsElement?.querySelectorAll(".column-list");
17451
+ const list = lists?.[nextIndex];
17452
+ const target = list?.querySelector(".option-button.selected:not(:disabled)") ?? list?.querySelector(".option-button:not(:disabled)");
17453
+ target?.focus();
17454
+ target?.scrollIntoView({ block: "nearest" });
17455
+ }
17456
+ // ── Render ──
16700
17457
  render() {
17458
+ const isError = this.error || this.invalid;
17459
+ const belowText = isError && this.errorMessage ? this.errorMessage : this.helperText;
17460
+ const describedById = belowText ? "time-picker-desc" : void 0;
17461
+ const parsedValue = this.parseTime(this.value);
17462
+ const displayText = parsedValue ? this.formatDisplay(parsedValue.hour, parsedValue.minute) : "";
17463
+ const draftDisplay = this.draftHour !== null ? this.formatDisplay(this.draftHour, this.draftMinute ?? 0) : "";
17464
+ const hourOptions = this.getHourOptions();
17465
+ const minuteOptions = this.getMinuteOptions();
17466
+ const draftPeriod = this.draftPeriod;
17467
+ const wrapperClasses = [
17468
+ "time-picker-wrapper",
17469
+ this.size,
17470
+ isError ? "error" : "",
17471
+ this.readonly ? "readonly" : ""
17472
+ ].filter(Boolean).join(" ");
16701
17473
  return import_lit54.html`
16702
- <div part="base" class="toast ${this.type} ${this.visible ? "" : "hidden"}" role="status" aria-live="polite">
16703
- <span part="label" class="toast-message">${this.message}</span>
17474
+ <div part="base" class="${wrapperClasses}">
17475
+ ${this.label ? import_lit54.html`<label part="label" class="label" id="time-picker-label"
17476
+ >${this.label}${this.required ? import_lit54.html`<span class="required-mark" aria-hidden="true">*</span>` : import_lit54.nothing}</label
17477
+ >` : import_lit54.nothing}
16704
17478
 
16705
- ${this.actionLabel ? import_lit54.html`<button
16706
- part="action" class="toast-action"
16707
- @click="${this.handleActionClick}"
17479
+ ${this.name ? import_lit54.html`<input type="hidden" name=${this.name} .value=${this.value ?? ""} />` : import_lit54.nothing}
17480
+
17481
+ <button
17482
+ part="trigger"
17483
+ class="trigger-button ${isError ? "error" : ""}"
17484
+ type="button"
17485
+ ?disabled=${this.disabled}
17486
+ @click=${this.toggleTimePopover}
17487
+ @keydown=${this.handleTriggerKeydown}
17488
+ @focus=${() => this.dispatchEvent(
17489
+ new CustomEvent("rel-focus", { bubbles: true, composed: true })
17490
+ )}
17491
+ @blur=${() => this.dispatchEvent(
17492
+ new CustomEvent("rel-blur", { bubbles: true, composed: true })
17493
+ )}
17494
+ aria-label=${this.label || "Select time"}
17495
+ aria-haspopup="dialog"
17496
+ aria-expanded=${this.open}
17497
+ aria-required=${this.required}
17498
+ aria-invalid=${isError}
17499
+ aria-readonly=${this.readonly}
17500
+ aria-describedby=${describedById ?? import_lit54.nothing}
17501
+ aria-labelledby=${this.label ? "time-picker-label" : import_lit54.nothing}
17502
+ >
17503
+ <span part="value" class="trigger-content ${displayText ? "" : "placeholder"}">
17504
+ ${displayText || this.placeholder}
17505
+ </span>
17506
+ <span class="trigger-icons">
17507
+ ${this.clearable && this.value ? import_lit54.html`
17508
+ <button
17509
+ class="icon-button"
17510
+ type="button"
17511
+ aria-label="Clear time"
17512
+ @click=${this.clearValue}
17513
+ >
17514
+ <svg viewBox="0 0 16 16" fill="currentColor">
17515
+ <path
17516
+ d="M4 4l8 8M12 4l-8 8"
17517
+ stroke="currentColor"
17518
+ stroke-width="2"
17519
+ stroke-linecap="round"
17520
+ />
17521
+ </svg>
17522
+ </button>
17523
+ ` : import_lit54.nothing}
17524
+ <rel-icon part="icon" name="time-filled" size="sm"></rel-icon>
17525
+ </span>
17526
+ </button>
17527
+
17528
+ <div
17529
+ part="panel"
17530
+ class="popover ${this.open ? "open" : ""}"
17531
+ role="dialog"
17532
+ aria-label="Choose time"
17533
+ >
17534
+ <div class="time-readout">
17535
+ <span class="time-readout-label">Time</span>
17536
+ <span class="time-readout-value ${draftDisplay ? "" : "empty"}">
17537
+ ${draftDisplay || (this.format === "24h" ? "--:--" : "--:-- --")}
17538
+ </span>
17539
+ </div>
17540
+
17541
+ <div class="columns">
17542
+ <div class="column">
17543
+ <div class="column-header" aria-hidden="true">Hour</div>
17544
+ <div class="column-list" role="listbox" aria-label="Hours">
17545
+ ${hourOptions.map(
17546
+ (opt, i) => import_lit54.html`
17547
+ <button
17548
+ part="option"
17549
+ class="option-button ${this.draftHour === opt.hour24 ? "selected" : ""}"
17550
+ type="button"
17551
+ role="option"
17552
+ aria-selected=${this.draftHour === opt.hour24}
17553
+ ?disabled=${!this.isHourEnabled(opt.hour24)}
17554
+ @click=${() => this.selectHour(opt.hour24)}
17555
+ @keydown=${(e) => this.handleOptionKeydown(e, "hour")}
17556
+ tabindex=${this.draftHour === opt.hour24 || this.draftHour === null && i === 0 ? 0 : -1}
17557
+ >
17558
+ ${opt.label}
17559
+ </button>
17560
+ `
17561
+ )}
17562
+ </div>
17563
+ </div>
17564
+
17565
+ <div class="column">
17566
+ <div class="column-header" aria-hidden="true">Min</div>
17567
+ <div class="column-list" role="listbox" aria-label="Minutes">
17568
+ ${minuteOptions.map(
17569
+ (m, i) => import_lit54.html`
17570
+ <button
17571
+ part="option"
17572
+ class="option-button ${this.draftMinute === m ? "selected" : ""}"
17573
+ type="button"
17574
+ role="option"
17575
+ aria-selected=${this.draftMinute === m}
17576
+ ?disabled=${this.draftHour !== null && !this.isTimeInRange(this.draftHour, m)}
17577
+ @click=${() => this.selectMinute(m)}
17578
+ @keydown=${(e) => this.handleOptionKeydown(e, "minute")}
17579
+ tabindex=${this.draftMinute === m || this.draftMinute === null && i === 0 ? 0 : -1}
17580
+ >
17581
+ ${String(m).padStart(2, "0")}
17582
+ </button>
17583
+ `
17584
+ )}
17585
+ </div>
17586
+ </div>
17587
+
17588
+ ${this.format === "12h" ? import_lit54.html`
17589
+ <div class="column">
17590
+ <div class="column-header" aria-hidden="true">AM/PM</div>
17591
+ <div class="column-list" role="listbox" aria-label="AM or PM">
17592
+ ${["AM", "PM"].map(
17593
+ (p) => import_lit54.html`
17594
+ <button
17595
+ part="option"
17596
+ class="option-button ${draftPeriod === p ? "selected" : ""}"
17597
+ type="button"
17598
+ role="option"
17599
+ aria-selected=${draftPeriod === p}
17600
+ ?disabled=${!this.isPeriodEnabled(p)}
17601
+ @click=${() => this.selectPeriod(p)}
17602
+ @keydown=${(e) => this.handleOptionKeydown(e, "period")}
17603
+ tabindex=${draftPeriod === p || draftPeriod === null && p === "AM" ? 0 : -1}
17604
+ >
17605
+ ${p}
17606
+ </button>
17607
+ `
17608
+ )}
17609
+ </div>
17610
+ </div>
17611
+ ` : import_lit54.nothing}
17612
+ </div>
17613
+
17614
+ <div part="footer" class="time-footer">
17615
+ <rel-button
17616
+ variant="secondary"
17617
+ size="small"
17618
+ border="corner-radius"
17619
+ @click=${() => this.selectNow()}
16708
17620
  >
16709
- ${this.actionLabel}
16710
- </button>` : ""}
17621
+ Now
17622
+ </rel-button>
17623
+ <div class="footer-right">
17624
+ <rel-button
17625
+ variant="secondary"
17626
+ size="small"
17627
+ border="corner-radius"
17628
+ @click=${() => this.cancelPopover()}
17629
+ >
17630
+ Cancel
17631
+ </rel-button>
17632
+ <rel-button
17633
+ variant="primary"
17634
+ tone="default"
17635
+ size="small"
17636
+ border="corner-radius"
17637
+ ?disabled=${this.draftHour === null}
17638
+ @click=${() => this.applySelection()}
17639
+ >
17640
+ Apply
17641
+ </rel-button>
17642
+ </div>
17643
+ </div>
17644
+ </div>
16711
17645
 
16712
- <button part="close-button" class="toast-close" @click="${this.close}" aria-label="Dismiss">✕</button>
17646
+ ${belowText ? import_lit54.html`<div
17647
+ part="helper"
17648
+ class="time-picker-support ${isError ? "error" : ""}"
17649
+ id=${describedById}
17650
+ >
17651
+ ${belowText}
17652
+ </div>` : import_lit54.nothing}
16713
17653
  </div>
16714
17654
  `;
16715
17655
  }
16716
17656
  };
16717
- RelToast.styles = import_lit54.css`
17657
+ RelTimePicker.styles = import_lit54.css`
16718
17658
  :host {
16719
17659
  color: var(--rel-text-default);
16720
- position: fixed;
16721
- bottom: 24px;
16722
- left: 50%;
16723
- transform: translateX(-50%);
16724
- display: flex;
16725
- justify-content: center;
16726
- width: 100%;
16727
- pointer-events: none;
16728
- z-index: var(--rel-z-toast, 9000);
17660
+ display: block;
16729
17661
  font-family: var(--rel-fonts-font-family);
17662
+ position: relative;
16730
17663
  }
16731
17664
 
16732
- /* Toast container */
16733
- .toast {
16734
- display: inline-flex;
16735
- align-items: center;
17665
+ /* Intrinsic width by default; set fullWidth to fill the parent. */
17666
+ :host(:not([fullwidth])) {
17667
+ display: inline-block;
17668
+ width: auto;
17669
+ }
17670
+
17671
+ :host([disabled]) {
17672
+ opacity: 0.6;
17673
+ cursor: not-allowed;
17674
+ }
17675
+
17676
+ :host([disabled]) * {
17677
+ pointer-events: none;
17678
+ }
17679
+
17680
+ :host([readonly]) .trigger-button {
17681
+ cursor: default;
17682
+ }
17683
+
17684
+ .time-picker-wrapper {
17685
+ display: flex;
17686
+ flex-direction: column;
16736
17687
  gap: var(--rel-spacing-xs);
16737
- background-color: var(--rel-bg-neutral-inverse-1);
16738
- color: var(--rel-text-inverse);
17688
+ }
16739
17689
 
16740
- border-radius: var(--rel-border-radius-sm);
17690
+ .label {
17691
+ color: var(--rel-text-label);
17692
+ font-family: var(--rel-body-sm-medium-font-family);
17693
+ font-weight: var(--rel-body-sm-medium-font-weight);
17694
+ font-size: var(--rel-body-sm-medium-font-size);
17695
+ line-height: var(--rel-body-sm-medium-line-height);
17696
+ letter-spacing: var(--rel-body-sm-medium-letter-spacing);
17697
+ }
17698
+
17699
+ .required-mark {
17700
+ color: var(--rel-text-danger);
17701
+ margin-left: 2px;
17702
+ }
17703
+
17704
+ /* Support text (helperText / errorMessage) */
17705
+ .time-picker-support {
17706
+ color: var(--rel-text-placeholder);
17707
+ font-family: var(--rel-body-xs-regular-font-family);
17708
+ font-weight: var(--rel-body-xs-regular-font-weight);
17709
+ font-size: var(--rel-body-xs-regular-font-size);
17710
+ line-height: var(--rel-body-xs-regular-line-height);
17711
+ letter-spacing: var(--rel-body-xs-regular-letter-spacing);
17712
+ }
17713
+
17714
+ .time-picker-support.error {
17715
+ color: var(--rel-text-danger);
17716
+ }
17717
+
17718
+ .trigger-button {
17719
+ width: 100%;
17720
+ min-width: 160px;
17721
+ display: flex;
17722
+ align-items: center;
17723
+ justify-content: space-between;
17724
+ gap: var(--rel-spacing-xs);
16741
17725
  padding: var(--rel-spacing-sm) var(--rel-spacing-md);
16742
- box-shadow: var(--rel-shadows-shadow-300);
16743
- pointer-events: all;
16744
- animation: fadeInUp 0.25s ease;
17726
+ background: var(--rel-bg-surface);
17727
+ border: 1px solid var(--rel-border-default);
17728
+ border-radius: var(--rel-border-radius-sm);
17729
+ color: var(--rel-text-default);
17730
+ cursor: pointer;
17731
+ transition: all 0.15s ease;
17732
+ text-align: left;
16745
17733
  font-family: var(--rel-body-md-regular-font-family);
16746
17734
  font-weight: var(--rel-body-md-regular-font-weight);
16747
17735
  font-size: var(--rel-body-md-regular-font-size);
@@ -16749,276 +17737,1605 @@ RelToast.styles = import_lit54.css`
16749
17737
  letter-spacing: var(--rel-body-md-regular-letter-spacing);
16750
17738
  }
16751
17739
 
16752
- .toast.error {
16753
- background-color: var(--rel-bg-danger);
16754
- color: var(--rel-text-static-light);
17740
+ .trigger-button:hover:not(:disabled) {
17741
+ border-color: var(--rel-border-strong);
17742
+ }
17743
+
17744
+ .trigger-button:focus-visible {
17745
+ outline: 2px solid var(--rel-border-focus);
17746
+ outline-offset: 2px;
17747
+ border-color: var(--rel-border-focus);
17748
+ }
17749
+
17750
+ .trigger-button:disabled {
17751
+ cursor: not-allowed;
17752
+ background: var(--rel-bg-surface-elevated);
17753
+ }
17754
+
17755
+ /* Error state */
17756
+ .trigger-button.error,
17757
+ .trigger-button.error:hover:not(:disabled) {
17758
+ border-color: var(--rel-border-danger);
17759
+ }
17760
+
17761
+ .trigger-button.error:focus-visible {
17762
+ outline-color: var(--rel-border-danger);
17763
+ border-color: var(--rel-border-danger);
17764
+ }
17765
+
17766
+ /* Size variants (medium is the base .trigger-button style) */
17767
+ .time-picker-wrapper.small .trigger-button {
17768
+ padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
17769
+ font-family: var(--rel-body-sm-regular-font-family);
17770
+ font-weight: var(--rel-body-sm-regular-font-weight);
17771
+ font-size: var(--rel-body-sm-regular-font-size);
17772
+ line-height: var(--rel-body-sm-regular-line-height);
17773
+ letter-spacing: var(--rel-body-sm-regular-letter-spacing);
17774
+ }
17775
+
17776
+ .time-picker-wrapper.large .trigger-button {
17777
+ padding: var(--rel-spacing-md) var(--rel-spacing-md);
17778
+ font-family: var(--rel-body-lg-regular-font-family);
17779
+ font-weight: var(--rel-body-lg-regular-font-weight);
17780
+ font-size: var(--rel-body-lg-regular-font-size);
17781
+ line-height: var(--rel-body-lg-regular-line-height);
17782
+ letter-spacing: var(--rel-body-lg-regular-letter-spacing);
17783
+ }
17784
+
17785
+ .trigger-content {
17786
+ flex: 1;
17787
+ color: var(--rel-text-default);
17788
+ }
17789
+
17790
+ .trigger-content.placeholder {
17791
+ color: var(--rel-text-placeholder);
17792
+ }
17793
+
17794
+ .trigger-icons {
17795
+ display: flex;
17796
+ align-items: center;
17797
+ gap: var(--rel-spacing-2xs);
17798
+ color: var(--rel-text-placeholder);
17799
+ }
17800
+
17801
+ .icon-button {
17802
+ background: none;
17803
+ border: none;
17804
+ padding: var(--rel-spacing-3xs);
17805
+ cursor: pointer;
17806
+ color: var(--rel-text-placeholder);
17807
+ display: flex;
17808
+ align-items: center;
17809
+ justify-content: center;
17810
+ border-radius: var(--rel-border-radius-xs);
17811
+ transition: all 0.15s ease;
17812
+ }
17813
+
17814
+ .icon-button:hover {
17815
+ background: var(--rel-bg-surface-elevated);
17816
+ color: var(--rel-text-default);
17817
+ }
17818
+
17819
+ .icon-button svg {
17820
+ width: 16px;
17821
+ height: 16px;
17822
+ }
17823
+
17824
+ .popover {
17825
+ position: absolute;
17826
+ top: calc(100% + 4px);
17827
+ left: 0;
17828
+ z-index: 1000;
17829
+ background: var(--rel-bg-surface);
17830
+ border: 1px solid var(--rel-border-default);
17831
+ border-radius: var(--rel-border-radius-sm);
17832
+ box-shadow: var(--rel-shadows-shadow-300);
17833
+ padding: var(--rel-spacing-md);
17834
+ display: none;
17835
+ }
17836
+
17837
+ .popover.open {
17838
+ display: block;
17839
+ }
17840
+
17841
+ /* Draft readout (mirrors the range-picker FROM/TO block) */
17842
+ .time-readout {
17843
+ display: flex;
17844
+ align-items: baseline;
17845
+ gap: var(--rel-spacing-sm);
17846
+ padding-bottom: var(--rel-spacing-sm);
17847
+ margin-bottom: var(--rel-spacing-sm);
17848
+ border-bottom: 1px solid var(--rel-border-subtle);
17849
+ }
17850
+
17851
+ .time-readout-label {
17852
+ color: var(--rel-text-placeholder);
17853
+ text-transform: uppercase;
17854
+ font-family: var(--rel-body-xs-semibold-font-family);
17855
+ font-weight: var(--rel-body-xs-semibold-font-weight);
17856
+ font-size: var(--rel-body-xs-semibold-font-size);
17857
+ line-height: var(--rel-body-xs-semibold-line-height);
17858
+ letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
17859
+ }
17860
+
17861
+ .time-readout-value {
17862
+ color: var(--rel-text-default);
17863
+ font-family: var(--rel-body-md-semibold-font-family);
17864
+ font-weight: var(--rel-body-md-semibold-font-weight);
17865
+ font-size: var(--rel-body-md-semibold-font-size);
17866
+ line-height: var(--rel-body-md-semibold-line-height);
17867
+ letter-spacing: var(--rel-body-md-semibold-letter-spacing);
17868
+ }
17869
+
17870
+ .time-readout-value.empty {
17871
+ color: var(--rel-text-placeholder);
17872
+ }
17873
+
17874
+ .columns {
17875
+ display: flex;
17876
+ gap: var(--rel-spacing-xs);
17877
+ }
17878
+
17879
+ /* Columns share the panel width equally, so the two-column 24h layout
17880
+ fills the same footprint the footer dictates instead of bunching left. */
17881
+ .column {
17882
+ display: flex;
17883
+ flex-direction: column;
17884
+ flex: 1;
17885
+ min-width: 64px;
17886
+ }
17887
+
17888
+ .column-header {
17889
+ text-align: center;
17890
+ color: var(--rel-text-placeholder);
17891
+ padding: var(--rel-spacing-xs);
17892
+ text-transform: uppercase;
17893
+ font-family: var(--rel-body-xs-semibold-font-family);
17894
+ font-weight: var(--rel-body-xs-semibold-font-weight);
17895
+ font-size: var(--rel-body-xs-semibold-font-size);
17896
+ line-height: var(--rel-body-xs-semibold-line-height);
17897
+ letter-spacing: var(--rel-body-xs-semibold-letter-spacing);
17898
+ }
17899
+
17900
+ .column-list {
17901
+ display: flex;
17902
+ flex-direction: column;
17903
+ gap: var(--rel-spacing-3xs);
17904
+ max-height: 224px;
17905
+ overflow-y: auto;
17906
+ padding: var(--rel-spacing-3xs);
17907
+ scrollbar-width: thin;
17908
+ }
17909
+
17910
+ .option-button {
17911
+ border: none;
17912
+ background: transparent;
17913
+ border-radius: var(--rel-border-radius-sm);
17914
+ color: var(--rel-text-default);
17915
+ cursor: pointer;
17916
+ padding: var(--rel-spacing-xs) var(--rel-spacing-sm);
17917
+ text-align: center;
17918
+ flex-shrink: 0;
17919
+ transition: all 0.15s ease;
17920
+ font-family: var(--rel-body-sm-medium-font-family);
17921
+ font-weight: var(--rel-body-sm-medium-font-weight);
17922
+ font-size: var(--rel-body-sm-medium-font-size);
17923
+ line-height: var(--rel-body-sm-medium-line-height);
17924
+ letter-spacing: var(--rel-body-sm-medium-letter-spacing);
17925
+ }
17926
+
17927
+ .option-button:hover:not(:disabled):not(.selected) {
17928
+ background: var(--rel-bg-surface-elevated);
17929
+ }
17930
+
17931
+ .option-button:focus-visible {
17932
+ outline: 2px solid var(--rel-border-focus);
17933
+ outline-offset: -2px;
17934
+ z-index: 1;
17935
+ }
17936
+
17937
+ .option-button.selected {
17938
+ background: var(--rel-action-brand-fill);
17939
+ color: var(--rel-action-brand-label-icons-primary);
17940
+ font-weight: var(--rel-body-md-semibold-font-weight);
17941
+ }
17942
+
17943
+ .option-button:disabled {
17944
+ color: var(--rel-text-disabled);
17945
+ cursor: not-allowed;
17946
+ opacity: 0.5;
17947
+ }
17948
+
17949
+ .option-button:disabled:hover {
17950
+ background: transparent;
17951
+ }
17952
+
17953
+ /* Footer (mirrors the range-picker calendar-footer) */
17954
+ .time-footer {
17955
+ display: flex;
17956
+ align-items: center;
17957
+ justify-content: space-between;
17958
+ gap: var(--rel-spacing-sm);
17959
+ padding-top: var(--rel-spacing-sm);
17960
+ margin-top: var(--rel-spacing-sm);
17961
+ border-top: 1px solid var(--rel-border-subtle);
17962
+ }
17963
+
17964
+ .footer-right {
17965
+ display: flex;
17966
+ align-items: center;
17967
+ gap: var(--rel-spacing-xs);
17968
+ }
17969
+
17970
+ @media (prefers-reduced-motion: reduce) {
17971
+ .trigger-button,
17972
+ .icon-button,
17973
+ .option-button {
17974
+ transition: none;
17975
+ }
17976
+ }
17977
+ `;
17978
+ __decorateClass([
17979
+ (0, import_decorators54.property)({ type: String, reflect: true })
17980
+ ], RelTimePicker.prototype, "value", 2);
17981
+ __decorateClass([
17982
+ (0, import_decorators54.property)({ type: String })
17983
+ ], RelTimePicker.prototype, "placeholder", 2);
17984
+ __decorateClass([
17985
+ (0, import_decorators54.property)({ type: String })
17986
+ ], RelTimePicker.prototype, "label", 2);
17987
+ __decorateClass([
17988
+ (0, import_decorators54.property)({ type: Boolean, reflect: true })
17989
+ ], RelTimePicker.prototype, "disabled", 2);
17990
+ __decorateClass([
17991
+ (0, import_decorators54.property)({ type: Boolean })
17992
+ ], RelTimePicker.prototype, "required", 2);
17993
+ __decorateClass([
17994
+ (0, import_decorators54.property)({ type: Boolean, reflect: true })
17995
+ ], RelTimePicker.prototype, "readonly", 2);
17996
+ __decorateClass([
17997
+ (0, import_decorators54.property)({ type: String })
17998
+ ], RelTimePicker.prototype, "format", 2);
17999
+ __decorateClass([
18000
+ (0, import_decorators54.property)({ type: Number, attribute: "minute-step" })
18001
+ ], RelTimePicker.prototype, "minuteStep", 2);
18002
+ __decorateClass([
18003
+ (0, import_decorators54.property)({ type: String, attribute: "min-time" })
18004
+ ], RelTimePicker.prototype, "minTime", 2);
18005
+ __decorateClass([
18006
+ (0, import_decorators54.property)({ type: String, attribute: "max-time" })
18007
+ ], RelTimePicker.prototype, "maxTime", 2);
18008
+ __decorateClass([
18009
+ (0, import_decorators54.property)({ type: Boolean, reflect: true })
18010
+ ], RelTimePicker.prototype, "open", 2);
18011
+ __decorateClass([
18012
+ (0, import_decorators54.property)({ type: Boolean })
18013
+ ], RelTimePicker.prototype, "clearable", 2);
18014
+ __decorateClass([
18015
+ (0, import_decorators54.property)({ type: Boolean })
18016
+ ], RelTimePicker.prototype, "error", 2);
18017
+ __decorateClass([
18018
+ (0, import_decorators54.property)({ type: Boolean })
18019
+ ], RelTimePicker.prototype, "invalid", 2);
18020
+ __decorateClass([
18021
+ (0, import_decorators54.property)({ type: String, attribute: "error-message" })
18022
+ ], RelTimePicker.prototype, "errorMessage", 2);
18023
+ __decorateClass([
18024
+ (0, import_decorators54.property)({ type: String, attribute: "helper-text" })
18025
+ ], RelTimePicker.prototype, "helperText", 2);
18026
+ __decorateClass([
18027
+ (0, import_decorators54.property)({ type: String })
18028
+ ], RelTimePicker.prototype, "name", 2);
18029
+ __decorateClass([
18030
+ (0, import_decorators54.property)({ type: String })
18031
+ ], RelTimePicker.prototype, "size", 2);
18032
+ __decorateClass([
18033
+ (0, import_decorators54.property)({ type: String, reflect: true })
18034
+ ], RelTimePicker.prototype, "variant", 2);
18035
+ __decorateClass([
18036
+ (0, import_decorators54.property)({ type: Boolean, reflect: true })
18037
+ ], RelTimePicker.prototype, "fullWidth", 2);
18038
+ __decorateClass([
18039
+ (0, import_decorators54.state)()
18040
+ ], RelTimePicker.prototype, "draftHour", 2);
18041
+ __decorateClass([
18042
+ (0, import_decorators54.state)()
18043
+ ], RelTimePicker.prototype, "draftMinute", 2);
18044
+ __decorateClass([
18045
+ (0, import_decorators54.query)(".trigger-button")
18046
+ ], RelTimePicker.prototype, "triggerButton", 2);
18047
+ __decorateClass([
18048
+ (0, import_decorators54.query)(".columns")
18049
+ ], RelTimePicker.prototype, "columnsElement", 2);
18050
+ RelTimePicker = __decorateClass([
18051
+ (0, import_decorators54.customElement)("rel-time-picker")
18052
+ ], RelTimePicker);
18053
+
18054
+ // src/components/rel-toast/rel-toast.ts
18055
+ var import_lit55 = require("lit");
18056
+ var import_decorators55 = require("lit/decorators.js");
18057
+ var RelToast = class extends import_lit55.LitElement {
18058
+ constructor() {
18059
+ super(...arguments);
18060
+ this.message = "";
18061
+ this.type = "default";
18062
+ this.duration = 4e3;
18063
+ this.show = false;
18064
+ this.visible = false;
18065
+ this._dismissTimer = null;
18066
+ }
18067
+ willUpdate(changed) {
18068
+ if (!changed.has("show")) return;
18069
+ if (this.show) {
18070
+ this.visible = true;
18071
+ if (this._dismissTimer) clearTimeout(this._dismissTimer);
18072
+ if (this.duration > 0) {
18073
+ this._dismissTimer = setTimeout(() => this.close(), this.duration);
18074
+ }
18075
+ } else {
18076
+ this.visible = false;
18077
+ if (this._dismissTimer) {
18078
+ clearTimeout(this._dismissTimer);
18079
+ this._dismissTimer = null;
18080
+ }
18081
+ }
18082
+ }
18083
+ disconnectedCallback() {
18084
+ super.disconnectedCallback();
18085
+ if (this._dismissTimer) clearTimeout(this._dismissTimer);
18086
+ }
18087
+ open() {
18088
+ this.show = true;
18089
+ }
18090
+ close() {
18091
+ this.show = false;
18092
+ this.visible = false;
18093
+ if (this._dismissTimer) {
18094
+ clearTimeout(this._dismissTimer);
18095
+ this._dismissTimer = null;
18096
+ }
18097
+ this.dispatchEvent(
18098
+ new CustomEvent("dismissed", {
18099
+ bubbles: true,
18100
+ composed: true
18101
+ })
18102
+ );
18103
+ }
18104
+ handleActionClick(event) {
18105
+ event.stopPropagation();
18106
+ this.dispatchEvent(
18107
+ new CustomEvent("action", {
18108
+ bubbles: true,
18109
+ composed: true
18110
+ })
18111
+ );
18112
+ this.close();
18113
+ }
18114
+ render() {
18115
+ return import_lit55.html`
18116
+ <div part="base" class="toast ${this.type} ${this.visible ? "" : "hidden"}" role="status" aria-live="polite">
18117
+ <span part="label" class="toast-message">${this.message}</span>
18118
+
18119
+ ${this.actionLabel ? import_lit55.html`<button
18120
+ part="action" class="toast-action"
18121
+ @click="${this.handleActionClick}"
18122
+ >
18123
+ ${this.actionLabel}
18124
+ </button>` : ""}
18125
+
18126
+ <button part="close-button" class="toast-close" @click="${this.close}" aria-label="Dismiss">✕</button>
18127
+ </div>
18128
+ `;
18129
+ }
18130
+ };
18131
+ RelToast.styles = import_lit55.css`
18132
+ :host {
18133
+ color: var(--rel-text-default);
18134
+ position: fixed;
18135
+ bottom: 24px;
18136
+ left: 50%;
18137
+ transform: translateX(-50%);
18138
+ display: flex;
18139
+ justify-content: center;
18140
+ width: 100%;
18141
+ pointer-events: none;
18142
+ z-index: var(--rel-z-toast, 9000);
18143
+ font-family: var(--rel-fonts-font-family);
18144
+ }
18145
+
18146
+ /* Toast container */
18147
+ .toast {
18148
+ display: inline-flex;
18149
+ align-items: center;
18150
+ gap: var(--rel-spacing-xs);
18151
+ background-color: var(--rel-bg-neutral-inverse-1);
18152
+ color: var(--rel-text-inverse);
18153
+
18154
+ border-radius: var(--rel-border-radius-sm);
18155
+ padding: var(--rel-spacing-sm) var(--rel-spacing-md);
18156
+ box-shadow: var(--rel-shadows-shadow-300);
18157
+ pointer-events: all;
18158
+ animation: fadeInUp 0.25s ease;
18159
+ font-family: var(--rel-body-md-regular-font-family);
18160
+ font-weight: var(--rel-body-md-regular-font-weight);
18161
+ font-size: var(--rel-body-md-regular-font-size);
18162
+ line-height: var(--rel-body-md-regular-line-height);
18163
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
18164
+ }
18165
+
18166
+ .toast.error {
18167
+ background-color: var(--rel-bg-danger);
18168
+ color: var(--rel-text-static-light);
18169
+ }
18170
+
18171
+ .toast.hidden {
18172
+ display: none;
18173
+ }
18174
+
18175
+ /* Message */
18176
+ .toast-message {
18177
+ flex: 1;
18178
+ }
18179
+
18180
+ /* Action button */
18181
+ .toast-action {
18182
+ background: transparent;
18183
+ border: none;
18184
+ color: var(--rel-action-neutral-label-icons-fill-inverse);
18185
+ font-family: var(--rel-action-md-font-family);
18186
+ font-weight: var(--rel-action-md-font-weight);
18187
+ font-size: var(--rel-action-md-font-size);
18188
+ line-height: var(--rel-action-md-line-height);
18189
+ letter-spacing: var(--rel-action-md-letter-spacing);
18190
+ cursor: pointer;
18191
+ transition: opacity 0.2s;
18192
+ }
18193
+
18194
+ .toast-action:hover {
18195
+ background: var(--rel-bg-neutral);
18196
+ color: var(--rel-text-default);
18197
+ opacity: 0.75;
18198
+ }
18199
+
18200
+ .toast-action:focus-visible {
18201
+ outline: 2px solid var(--rel-text-static-light);
18202
+ outline-offset: 2px;
18203
+ border-radius: var(--rel-border-radius-xs);
18204
+ }
18205
+
18206
+ /* Close button */
18207
+ .toast-close {
18208
+ background: transparent;
18209
+ color: var(--rel-action-neutral-label-icons-ghost);
18210
+ border: none;
18211
+ color: inherit;
18212
+ font-size: 1rem;
18213
+ cursor: pointer;
18214
+ line-height: 1;
18215
+ padding: var(--rel-spacing-2xs);
18216
+ margin-left: var(--rel-spacing-2xs);
18217
+ }
18218
+
18219
+ .toast-close:hover {
18220
+ opacity: 0.75;
18221
+ }
18222
+
18223
+ .toast-close:focus-visible {
18224
+ outline: 2px solid var(--rel-text-static-light);
18225
+ outline-offset: 2px;
18226
+ border-radius: var(--rel-border-radius-xs);
18227
+ }
18228
+
18229
+
18230
+ @media (prefers-reduced-motion: reduce) {
18231
+ .toast {
18232
+ animation: none;
18233
+ }
18234
+ }
18235
+
18236
+ /* Animations */
18237
+ @keyframes fadeInUp {
18238
+ from {
18239
+ opacity: 0;
18240
+ transform: translateY(20px);
18241
+ }
18242
+ to {
18243
+ opacity: 1;
18244
+ transform: translateY(0);
18245
+ }
18246
+ }
18247
+ `;
18248
+ __decorateClass([
18249
+ (0, import_decorators55.property)({ type: String })
18250
+ ], RelToast.prototype, "message", 2);
18251
+ __decorateClass([
18252
+ (0, import_decorators55.property)({ type: String })
18253
+ ], RelToast.prototype, "type", 2);
18254
+ __decorateClass([
18255
+ (0, import_decorators55.property)({ type: String })
18256
+ ], RelToast.prototype, "actionLabel", 2);
18257
+ __decorateClass([
18258
+ (0, import_decorators55.property)({ type: Number })
18259
+ ], RelToast.prototype, "duration", 2);
18260
+ __decorateClass([
18261
+ (0, import_decorators55.property)({ type: Boolean, reflect: true })
18262
+ ], RelToast.prototype, "show", 2);
18263
+ __decorateClass([
18264
+ (0, import_decorators55.state)()
18265
+ ], RelToast.prototype, "visible", 2);
18266
+ RelToast = __decorateClass([
18267
+ (0, import_decorators55.customElement)("rel-toast")
18268
+ ], RelToast);
18269
+
18270
+ // src/components/rel-tooltip/rel-tooltip.ts
18271
+ var import_lit56 = require("lit");
18272
+ var import_decorators56 = require("lit/decorators.js");
18273
+ var RelTooltip = class extends import_lit56.LitElement {
18274
+ constructor() {
18275
+ super(...arguments);
18276
+ this.text = "";
18277
+ this.position = "above";
18278
+ this.showTail = false;
18279
+ this.type = "single-line";
18280
+ // Stable id so aria-describedby can reference the bubble across renders.
18281
+ this.uniqueId = Math.random().toString(36).slice(2);
18282
+ }
18283
+ firstUpdated() {
18284
+ this.linkTrigger();
18285
+ }
18286
+ linkTrigger() {
18287
+ const slot = this.shadowRoot?.querySelector("slot");
18288
+ if (!slot) return;
18289
+ const trigger = slot.assignedElements()[0];
18290
+ if (trigger) {
18291
+ trigger.setAttribute("aria-describedby", `tooltip-${this.uniqueId}`);
18292
+ }
18293
+ }
18294
+ render() {
18295
+ return import_lit56.html`
18296
+ <slot @slotchange="${this.linkTrigger}"></slot>
18297
+ <div
18298
+ part="panel" class="tooltip-bubble ${this.position}"
18299
+ id="tooltip-${this.uniqueId}"
18300
+ role="tooltip"
18301
+ >
18302
+ ${this.text}
18303
+ ${this.showTail ? import_lit56.html`<span part="indicator" class="tooltip-tail"></span>` : ""}
18304
+ </div>
18305
+ `;
18306
+ }
18307
+ };
18308
+ RelTooltip.styles = import_lit56.css`
18309
+ :host {
18310
+ color: var(--rel-text-default);
18311
+ display: inline-block;
18312
+ position: relative;
18313
+ }
18314
+
18315
+ /* Tooltip container */
18316
+ .tooltip-bubble {
18317
+ font-family: var(--rel-body-md-regular-font-family);
18318
+ font-weight: var(--rel-body-md-medium-font-weight);
18319
+ font-size: var(--rel-body-md-regular-font-size);
18320
+ line-height: var(--rel-body-md-regular-line-height);
18321
+ letter-spacing: var(--rel-body-md-regular-letter-spacing);
18322
+ position: absolute;
18323
+ background-color: var(--rel-bg-neutral-inverse-2);
18324
+ color: var(--rel-text-inverse);
18325
+ padding: var(--rel-spacing-xs, 8px)
18326
+ var(--rel-spacing-sm, 12px);
18327
+ border-radius: var(--rel-border-radius-sm, 8px);
18328
+ white-space: nowrap;
18329
+ z-index: 1000;
18330
+ opacity: 0;
18331
+ pointer-events: none;
18332
+ transition: opacity 0.2s ease;
18333
+ overflow: visible;
18334
+ }
18335
+
18336
+ /* Figma Type=Multi Line: wrap inside a bounded bubble */
18337
+ :host([type="multi-line"]) .tooltip-bubble {
18338
+ white-space: normal;
18339
+ max-width: 220px;
18340
+ width: max-content;
18341
+ }
18342
+
18343
+ :host(:hover) .tooltip-bubble,
18344
+ :host(:focus-within) .tooltip-bubble {
18345
+ opacity: 1;
18346
+ }
18347
+
18348
+ /* Positioning */
18349
+ .tooltip-bubble.above {
18350
+ bottom: calc(100% + 6px);
18351
+ left: 50%;
18352
+ transform: translateX(-50%);
18353
+ }
18354
+
18355
+ .tooltip-bubble.below {
18356
+ top: calc(100% + 6px);
18357
+ left: 50%;
18358
+ transform: translateX(-50%);
18359
+ }
18360
+
18361
+ .tooltip-bubble.left {
18362
+ right: calc(100% + 6px);
18363
+ top: 50%;
18364
+ transform: translateY(-50%);
18365
+ }
18366
+
18367
+ .tooltip-bubble.right {
18368
+ left: calc(100% + 6px);
18369
+ top: 50%;
18370
+ transform: translateY(-50%);
18371
+ }
18372
+
18373
+ /* Tail/Arrow */
18374
+ .tooltip-tail {
18375
+ position: absolute;
18376
+ width: 0;
18377
+ height: 0;
18378
+ display: block;
18379
+ }
18380
+
18381
+ .tooltip-bubble.above .tooltip-tail,
18382
+ .tooltip-bubble.below .tooltip-tail {
18383
+ left: 50%;
18384
+ transform: translateX(-50%);
18385
+ border-left: 6px solid transparent;
18386
+ border-right: 6px solid transparent;
18387
+ }
18388
+
18389
+ .tooltip-bubble.above .tooltip-tail {
18390
+ bottom: -6px;
18391
+ border-top: 6px solid var(--rel-bg-neutral-inverse-2);
18392
+ border-bottom: none;
18393
+ }
18394
+
18395
+ .tooltip-bubble.below .tooltip-tail {
18396
+ top: -6px;
18397
+ border-bottom: 6px solid var(--rel-bg-neutral-inverse-2);
18398
+ border-top: none;
18399
+ }
18400
+
18401
+ .tooltip-bubble.left .tooltip-tail,
18402
+ .tooltip-bubble.right .tooltip-tail {
18403
+ top: 50%;
18404
+ transform: translateY(-50%);
18405
+ border-top: 6px solid transparent;
18406
+ border-bottom: 6px solid transparent;
18407
+ }
18408
+
18409
+ .tooltip-bubble.left .tooltip-tail {
18410
+ right: -6px;
18411
+ border-left: 6px solid var(--rel-bg-neutral-inverse-2);
18412
+ border-right: none;
18413
+ }
18414
+
18415
+ .tooltip-bubble.right .tooltip-tail {
18416
+ left: -6px;
18417
+ border-right: 6px solid var(--rel-bg-neutral-inverse-2);
18418
+ border-left: none;
18419
+ }
18420
+ `;
18421
+ __decorateClass([
18422
+ (0, import_decorators56.property)({ type: String })
18423
+ ], RelTooltip.prototype, "text", 2);
18424
+ __decorateClass([
18425
+ (0, import_decorators56.property)({ type: String })
18426
+ ], RelTooltip.prototype, "position", 2);
18427
+ __decorateClass([
18428
+ (0, import_decorators56.property)({ type: Boolean, attribute: "show-tail" })
18429
+ ], RelTooltip.prototype, "showTail", 2);
18430
+ __decorateClass([
18431
+ (0, import_decorators56.property)({ type: String, reflect: true })
18432
+ ], RelTooltip.prototype, "type", 2);
18433
+ RelTooltip = __decorateClass([
18434
+ (0, import_decorators56.customElement)("rel-tooltip")
18435
+ ], RelTooltip);
18436
+
18437
+ // src/components/rel-upload/rel-upload.ts
18438
+ var import_lit57 = require("lit");
18439
+ var import_decorators57 = require("lit/decorators.js");
18440
+ var import_rel_icon17 = require("@reliability-design/icons/rel-icon");
18441
+ var uid = 0;
18442
+ var RelUpload = class extends import_lit57.LitElement {
18443
+ constructor() {
18444
+ super(...arguments);
18445
+ this.accept = "";
18446
+ this.maxSize = "";
18447
+ this.multiple = false;
18448
+ this.disabled = false;
18449
+ this.hint = "";
18450
+ this.dropzoneTitle = "Drag and drop your files here";
18451
+ this.browseLabel = "Browse files";
18452
+ this.files = [];
18453
+ this.concurrency = 3;
18454
+ this.delayThreshold = 15e3;
18455
+ this.dragOver = false;
18456
+ this.delayed = false;
18457
+ this.lastSignature = "";
18458
+ this.openPicker = () => {
18459
+ if (this.disabled) return;
18460
+ this.inputEl?.click();
18461
+ };
18462
+ this.onInputChange = (e) => {
18463
+ const input = e.target;
18464
+ this.ingest(input.files);
18465
+ input.value = "";
18466
+ };
18467
+ this.onDragOver = (e) => {
18468
+ if (this.disabled) return;
18469
+ e.preventDefault();
18470
+ this.dragOver = true;
18471
+ };
18472
+ this.onDragLeave = (e) => {
18473
+ e.preventDefault();
18474
+ this.dragOver = false;
18475
+ };
18476
+ this.onDrop = (e) => {
18477
+ e.preventDefault();
18478
+ this.dragOver = false;
18479
+ if (this.disabled) return;
18480
+ this.ingest(e.dataTransfer?.files ?? null);
18481
+ };
18482
+ }
18483
+ // ── derived ───────────────────────────────────────────────────────────────
18484
+ /**
18485
+ * Aggregate state. Read-only; corresponds to the Figma modal variants.
18486
+ *
18487
+ * `failed` means there is nothing left to send AND something errored — while
18488
+ * any row is still `ready` the primary action is Upload, not Retry.
18489
+ */
18490
+ get phase() {
18491
+ if (this.files.length === 0) return "empty";
18492
+ if (this.files.some((f) => f.status === "uploading")) {
18493
+ return this.delayed ? "delayed" : "uploading";
18494
+ }
18495
+ if (this.pending.length > 0) return "selected";
18496
+ if (this.files.some((f) => f.status === "error")) return "failed";
18497
+ return "selected";
18498
+ }
18499
+ /** Files still eligible to be sent. */
18500
+ get pending() {
18501
+ return this.files.filter((f) => f.status === "ready");
18502
+ }
18503
+ get failedFiles() {
18504
+ return this.files.filter((f) => f.status === "error");
18505
+ }
18506
+ /**
18507
+ * Failed rows worth sending again. A file rejected by `accept`/`max-size`
18508
+ * will fail validation every time, so retrying it is pointless — re-check
18509
+ * rather than re-queueing it blindly.
18510
+ */
18511
+ get retryable() {
18512
+ const maxBytes = this.parseMaxBytes();
18513
+ return this.failedFiles.filter(
18514
+ (f) => !!f.file && this.matchesAccept(f.file) && f.file.size <= maxBytes
18515
+ );
18516
+ }
18517
+ get canUpload() {
18518
+ if (this.disabled) return false;
18519
+ const phase = this.phase;
18520
+ if (phase === "uploading" || phase === "delayed") return false;
18521
+ if (phase === "failed") return this.retryable.length > 0;
18522
+ return this.pending.length > 0;
18523
+ }
18524
+ /** Footer button label — "Upload", "Upload 3 files" or "Retry upload". */
18525
+ get primaryLabel() {
18526
+ if (this.phase === "failed") return "Retry upload";
18527
+ const n = this.files.length;
18528
+ return n > 1 ? `Upload ${n} files` : "Upload";
18529
+ }
18530
+ /** The snapshot `<rel-upload-actions>` renders from. */
18531
+ get state() {
18532
+ return {
18533
+ phase: this.phase,
18534
+ total: this.files.length,
18535
+ failed: this.failedFiles.length,
18536
+ canUpload: this.canUpload,
18537
+ primaryLabel: this.primaryLabel
18538
+ };
18539
+ }
18540
+ get listHeading() {
18541
+ const n = this.files.length;
18542
+ switch (this.phase) {
18543
+ case "uploading":
18544
+ case "delayed":
18545
+ return `Uploading ${n} ${n === 1 ? "file" : "files"}`;
18546
+ case "failed":
18547
+ return `${n} ${n === 1 ? "file" : "files"} - ${this.failedFiles.length} failed`;
18548
+ default:
18549
+ return n === 1 ? "Selected file (1)" : `Selected files (${n})`;
18550
+ }
18551
+ }
18552
+ get hintText() {
18553
+ if (this.hint) return this.hint;
18554
+ const formats = this.acceptDisplay;
18555
+ const size = this.maxSize ? ` - up to ${this.maxSize} per file` : "";
18556
+ return `${formats}${size}`;
18557
+ }
18558
+ get acceptDisplay() {
18559
+ const tokens = this.accept.split(",").map((s) => s.trim()).filter(Boolean).map((tok) => {
18560
+ if (tok.startsWith(".")) return tok.slice(1).toUpperCase();
18561
+ if (tok.endsWith("/*")) return tok.slice(0, -2).toUpperCase();
18562
+ return (tok.split("/").pop() ?? tok).toUpperCase();
18563
+ });
18564
+ if (tokens.length === 0) return "Any file type";
18565
+ if (tokens.length === 1) return tokens[0];
18566
+ return `${tokens.slice(0, -1).join(", ")} or ${tokens[tokens.length - 1]}`;
18567
+ }
18568
+ get alertContent() {
18569
+ if (this.phase === "delayed") {
18570
+ return {
18571
+ type: "warning",
18572
+ message1: "This is taking longer than usual",
18573
+ message2: "Large files can take a few minutes on a slow connection. You can leave this open or come back later."
18574
+ };
18575
+ }
18576
+ const failed = this.failedFiles.length;
18577
+ if (failed > 0 && this.phase !== "uploading") {
18578
+ const limit = this.maxSize ? `under ${this.maxSize} and ` : "";
18579
+ return {
18580
+ type: "error",
18581
+ message1: failed === 1 && this.files.length === 1 ? "We couldn't upload your file" : `We couldn't upload ${failed} of your files`,
18582
+ message2: `Check that each file is ${limit}in a supported format, then try again.`
18583
+ };
18584
+ }
18585
+ return null;
18586
+ }
18587
+ // ── validation ────────────────────────────────────────────────────────────
18588
+ parseMaxBytes() {
18589
+ const s = this.maxSize.trim().toLowerCase();
18590
+ if (!s) return Infinity;
18591
+ const m = s.match(/^([\d.]+)\s*(b|kb|mb|gb)?$/);
18592
+ if (!m) return Infinity;
18593
+ const n = parseFloat(m[1]);
18594
+ const unit = m[2] || "b";
18595
+ const mult = unit === "gb" ? 1e9 : unit === "mb" ? 1e6 : unit === "kb" ? 1e3 : 1;
18596
+ return n * mult;
18597
+ }
18598
+ matchesAccept(file) {
18599
+ const raw = this.accept.trim();
18600
+ if (!raw) return true;
18601
+ const tokens = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
18602
+ const name = file.name.toLowerCase();
18603
+ const type = (file.type || "").toLowerCase();
18604
+ return tokens.some((tok) => {
18605
+ if (tok.startsWith(".")) return name.endsWith(tok);
18606
+ if (tok.endsWith("/*")) return type.startsWith(tok.slice(0, -1));
18607
+ return type === tok;
18608
+ });
18609
+ }
18610
+ static fmtSize(bytes) {
18611
+ if (bytes < 1e3) return `${bytes} B`;
18612
+ if (bytes < 1e6) return `${(bytes / 1e3).toFixed(0)} KB`;
18613
+ return `${(bytes / 1e6).toFixed(1)} MB`;
18614
+ }
18615
+ static ext(name) {
18616
+ const i = name.lastIndexOf(".");
18617
+ return i > -1 ? name.slice(i + 1).toUpperCase() : "FILE";
18618
+ }
18619
+ /**
18620
+ * Validate and append. Invalid files are kept as `error` rows rather than
18621
+ * dropped — that is how the Figma Failure state surfaces them.
18622
+ */
18623
+ ingest(list) {
18624
+ if (!list || list.length === 0) return;
18625
+ const maxBytes = this.parseMaxBytes();
18626
+ const incoming = this.multiple ? Array.from(list) : Array.from(list).slice(0, 1);
18627
+ const added = [];
18628
+ const rejected = [];
18629
+ for (const f of incoming) {
18630
+ const entry = {
18631
+ id: `rel-upload-${++uid}`,
18632
+ name: f.name,
18633
+ size: f.size,
18634
+ status: "ready",
18635
+ file: f
18636
+ };
18637
+ if (!this.matchesAccept(f)) {
18638
+ entry.status = "error";
18639
+ entry.error = "unsupported file type";
18640
+ rejected.push({ name: f.name, reason: "format", message: `${f.name}: unsupported file type` });
18641
+ } else if (f.size > maxBytes) {
18642
+ entry.status = "error";
18643
+ entry.error = `file exceeds the ${this.maxSize} limit`;
18644
+ rejected.push({
18645
+ name: f.name,
18646
+ reason: "size",
18647
+ message: `${f.name}: exceeds ${this.maxSize}`
18648
+ });
18649
+ }
18650
+ added.push(entry);
18651
+ }
18652
+ const names = new Set(added.map((f) => f.name));
18653
+ const kept = this.multiple ? this.files.filter((f) => !names.has(f.name)) : [];
18654
+ this.files = [...kept, ...added];
18655
+ if (rejected.length) this.emit("rel-invalid", { rejected });
18656
+ this.emit("rel-files-added", { added, rejected });
18657
+ this.emit("rel-change", { files: this.files });
18658
+ this.notify();
18659
+ }
18660
+ // ── upload orchestration (self-driving mode) ──────────────────────────────
18661
+ patch(id, partial) {
18662
+ this.files = this.files.map((f) => f.id === id ? { ...f, ...partial } : f);
18663
+ this.notify();
18664
+ }
18665
+ startDelayTimer() {
18666
+ this.stopDelayTimer();
18667
+ if (this.delayThreshold <= 0) return;
18668
+ this.delayTimer = setTimeout(() => {
18669
+ if (this.files.some((f) => f.status === "uploading")) {
18670
+ this.delayed = true;
18671
+ this.notify();
18672
+ }
18673
+ }, this.delayThreshold);
18674
+ }
18675
+ stopDelayTimer() {
18676
+ if (this.delayTimer) clearTimeout(this.delayTimer);
18677
+ this.delayTimer = void 0;
18678
+ }
18679
+ async runBatch(targets) {
18680
+ const uploader = this.uploader;
18681
+ if (!uploader || targets.length === 0) return;
18682
+ this.aborter = new AbortController();
18683
+ const signal = this.aborter.signal;
18684
+ this.startDelayTimer();
18685
+ const queue = [...targets];
18686
+ const workers = Array.from({ length: Math.max(1, this.concurrency) }, async () => {
18687
+ while (queue.length) {
18688
+ if (signal.aborted) return;
18689
+ const item = queue.shift();
18690
+ if (!item || !item.file) continue;
18691
+ this.patch(item.id, { status: "uploading", progress: 0, loaded: 0, error: void 0 });
18692
+ try {
18693
+ await uploader(item.file, {
18694
+ file: item,
18695
+ signal,
18696
+ onProgress: (loaded, total) => {
18697
+ const t = total ?? item.size;
18698
+ const progress = t > 0 ? Math.min(100, Math.round(loaded / t * 100)) : 0;
18699
+ this.patch(item.id, { loaded, progress });
18700
+ this.emit("rel-progress", { id: item.id, progress, loaded, total: t });
18701
+ }
18702
+ });
18703
+ if (signal.aborted) {
18704
+ this.patch(item.id, { status: "ready", progress: void 0, loaded: void 0 });
18705
+ return;
18706
+ }
18707
+ this.patch(item.id, { status: "success", progress: 100, loaded: item.size });
18708
+ this.emit("rel-file-complete", { id: item.id, file: item.file });
18709
+ } catch (err) {
18710
+ if (signal.aborted) {
18711
+ this.patch(item.id, { status: "ready", progress: void 0, loaded: void 0 });
18712
+ return;
18713
+ }
18714
+ const message = err instanceof Error ? err.message : String(err);
18715
+ this.patch(item.id, { status: "error", error: message, progress: void 0 });
18716
+ this.emit("rel-file-error", { id: item.id, file: item.file, error: message });
18717
+ }
18718
+ }
18719
+ });
18720
+ await Promise.all(workers);
18721
+ this.stopDelayTimer();
18722
+ this.delayed = false;
18723
+ this.aborter = void 0;
18724
+ if (!signal.aborted) {
18725
+ this.emit("rel-complete", {
18726
+ succeeded: this.files.filter((f) => f.status === "success"),
18727
+ failed: this.failedFiles
18728
+ });
18729
+ }
18730
+ this.notify();
18731
+ }
18732
+ // ── public API ────────────────────────────────────────────────────────────
18733
+ /** Start (or resume) the batch. Emits `rel-upload`; runs it when `uploader` is set. */
18734
+ upload() {
18735
+ if (!this.canUpload) return;
18736
+ const targets = this.pending;
18737
+ this.emit("rel-upload", { files: targets });
18738
+ if (this.uploader) void this.runBatch(targets);
18739
+ else this.notify();
18740
+ }
18741
+ /** Re-queue every retryable failed row and send again. */
18742
+ retry() {
18743
+ const targets = this.retryable;
18744
+ if (targets.length === 0) return;
18745
+ const ids = new Set(targets.map((f) => f.id));
18746
+ this.files = this.files.map(
18747
+ (f) => ids.has(f.id) ? { ...f, status: "ready", error: void 0, progress: void 0 } : f
18748
+ );
18749
+ const requeued = this.files.filter((f) => ids.has(f.id));
18750
+ this.emit("rel-retry", { files: requeued });
18751
+ if (this.uploader) void this.runBatch(requeued);
18752
+ else this.notify();
18753
+ }
18754
+ /** Abort anything in flight and emit `rel-cancel`. Does not clear the list. */
18755
+ cancel() {
18756
+ this.aborter?.abort();
18757
+ this.stopDelayTimer();
18758
+ this.delayed = false;
18759
+ this.emit("rel-cancel");
18760
+ this.notify();
18761
+ }
18762
+ /** Remove one row by id. */
18763
+ removeFile(id) {
18764
+ const target = this.files.find((f) => f.id === id);
18765
+ this.files = this.files.filter((f) => f.id !== id);
18766
+ this.emit("rel-remove", { id, file: target?.file });
18767
+ this.emit("rel-change", { files: this.files });
18768
+ this.notify();
18769
+ }
18770
+ /** Clear everything and abort in-flight transfers. */
18771
+ reset() {
18772
+ this.aborter?.abort();
18773
+ this.stopDelayTimer();
18774
+ this.files = [];
18775
+ this.delayed = false;
18776
+ if (this.inputEl) this.inputEl.value = "";
18777
+ this.emit("rel-change", { files: this.files });
18778
+ this.notify();
18779
+ }
18780
+ // ── events ────────────────────────────────────────────────────────────────
18781
+ emit(type, detail) {
18782
+ this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
18783
+ }
18784
+ /**
18785
+ * Emit `rel-state` only when the derived snapshot actually changes, so
18786
+ * progress ticks don't spam `<rel-upload-actions>`.
18787
+ */
18788
+ notify() {
18789
+ const s = this.state;
18790
+ const signature = `${s.phase}|${s.total}|${s.failed}|${s.canUpload}|${s.primaryLabel}`;
18791
+ if (signature === this.lastSignature) return;
18792
+ this.lastSignature = signature;
18793
+ this.emit("rel-state", s);
18794
+ }
18795
+ connectedCallback() {
18796
+ super.connectedCallback();
18797
+ queueMicrotask(() => this.emit("rel-state", this.state));
18798
+ }
18799
+ disconnectedCallback() {
18800
+ super.disconnectedCallback();
18801
+ this.stopDelayTimer();
18802
+ this.aborter?.abort();
18803
+ }
18804
+ updated() {
18805
+ this.notify();
18806
+ }
18807
+ // ── render ────────────────────────────────────────────────────────────────
18808
+ rowMeta(f) {
18809
+ switch (f.status) {
18810
+ case "uploading": {
18811
+ const pct = f.progress ?? 0;
18812
+ const loaded = RelUpload.fmtSize(f.loaded ?? 0);
18813
+ return `${pct}% - ${loaded} of ${RelUpload.fmtSize(f.size)}`;
18814
+ }
18815
+ case "success":
18816
+ return `${RelUpload.ext(f.name)} - ${RelUpload.fmtSize(f.size)} - Uploaded`;
18817
+ case "error":
18818
+ return `Upload failed - ${f.error ?? "please try again"}`;
18819
+ default: {
18820
+ const base = `${RelUpload.ext(f.name)} - ${RelUpload.fmtSize(f.size)}`;
18821
+ const busy = this.phase === "uploading" || this.phase === "delayed";
18822
+ return busy ? `${base} - Waiting` : base;
18823
+ }
18824
+ }
18825
+ }
18826
+ renderRow(f) {
18827
+ return import_lit57.html`
18828
+ <div class="row ${f.status}">
18829
+ <rel-icon name="document" size="md"></rel-icon>
18830
+
18831
+ <div class="row-content">
18832
+ <span class="row-name" title=${f.name}>${f.name}</span>
18833
+ ${f.status === "uploading" ? import_lit57.html`<rel-progress
18834
+ mode="linear-determinate"
18835
+ .value=${f.progress ?? 0}
18836
+ ></rel-progress>` : import_lit57.nothing}
18837
+ <span class="row-meta">${this.rowMeta(f)}</span>
18838
+ </div>
18839
+
18840
+ <div class="row-trailing">
18841
+ ${f.status === "success" ? import_lit57.html`<rel-icon
18842
+ class="status-success"
18843
+ name="check-circle"
18844
+ size="md"
18845
+ role="img"
18846
+ aria-label="Uploaded"
18847
+ ></rel-icon>` : import_lit57.nothing}
18848
+ ${f.status === "error" ? import_lit57.html`<rel-icon
18849
+ class="status-error"
18850
+ name="error"
18851
+ size="md"
18852
+ role="img"
18853
+ aria-label="Failed"
18854
+ ></rel-icon>` : import_lit57.nothing}
18855
+ <button
18856
+ class="remove"
18857
+ aria-label="Remove ${f.name}"
18858
+ ?disabled=${this.disabled}
18859
+ @click=${() => this.removeFile(f.id)}
18860
+ >
18861
+ <rel-icon name="delete" size="md"></rel-icon>
18862
+ </button>
18863
+ </div>
18864
+ </div>
18865
+ `;
18866
+ }
18867
+ render() {
18868
+ const alert = this.alertContent;
18869
+ return import_lit57.html`
18870
+ <div class="body">
18871
+ ${alert ? import_lit57.html`<rel-alert
18872
+ type=${alert.type}
18873
+ appearance="subtle"
18874
+ message1=${alert.message1}
18875
+ message2=${alert.message2}
18876
+ ></rel-alert>` : import_lit57.nothing}
18877
+
18878
+ <div
18879
+ class="dropzone ${this.dragOver ? "dragover" : ""}"
18880
+ @click=${this.openPicker}
18881
+ @dragover=${this.onDragOver}
18882
+ @dragleave=${this.onDragLeave}
18883
+ @drop=${this.onDrop}
18884
+ >
18885
+ <span class="icon-badge">
18886
+ <rel-icon name="upload" size="md"></rel-icon>
18887
+ </span>
18888
+
18889
+ <div class="dz-copy">
18890
+ <p class="dz-title">${this.dropzoneTitle}</p>
18891
+ <p class="dz-hint">${this.hintText}</p>
18892
+ </div>
18893
+
18894
+ <button
18895
+ class="browse"
18896
+ type="button"
18897
+ ?disabled=${this.disabled}
18898
+ @click=${(e) => {
18899
+ e.stopPropagation();
18900
+ this.openPicker();
18901
+ }}
18902
+ >
18903
+ ${this.browseLabel}
18904
+ </button>
18905
+ </div>
18906
+
18907
+ ${this.files.length ? import_lit57.html`<div class="file-list">
18908
+ <p class="list-heading">${this.listHeading}</p>
18909
+ <div class="rows" role="list">${this.files.map((f) => this.renderRow(f))}</div>
18910
+ </div>` : import_lit57.nothing}
18911
+
18912
+ <input
18913
+ class="file-input"
18914
+ type="file"
18915
+ ?multiple=${this.multiple}
18916
+ accept=${this.accept || import_lit57.nothing}
18917
+ @change=${this.onInputChange}
18918
+ />
18919
+ </div>
18920
+ `;
18921
+ }
18922
+ };
18923
+ RelUpload.styles = import_lit57.css`
18924
+ :host {
18925
+ display: block;
18926
+ box-sizing: border-box;
18927
+ color: var(--rel-text-default);
18928
+ font-family: var(--rel-fonts-font-family);
18929
+ }
18930
+
18931
+ :host([disabled]) {
18932
+ opacity: 0.6;
18933
+ }
18934
+
18935
+ :host([disabled]) .body {
18936
+ pointer-events: none;
18937
+ }
18938
+
18939
+ .body {
18940
+ display: flex;
18941
+ flex-direction: column;
18942
+ gap: var(--rel-spacing-lg, 20px);
18943
+ }
18944
+
18945
+ /* ── Dropzone ─────────────────────────────────────────────────────────── */
18946
+
18947
+ .dropzone {
18948
+ box-sizing: border-box;
18949
+ display: flex;
18950
+ flex-direction: column;
18951
+ align-items: center;
18952
+ justify-content: center;
18953
+ gap: var(--rel-spacing-md, 16px);
18954
+ padding: var(--rel-spacing-2xl, 32px) var(--rel-spacing-xl, 24px);
18955
+ background: var(--rel-bg-surface);
18956
+ border: 1px dashed var(--rel-border-accent);
18957
+ border-radius: var(--rel-border-radius-md, 16px);
18958
+ text-align: center;
18959
+ cursor: pointer;
18960
+ transition: border-color 0.15s ease, background 0.15s ease;
18961
+ }
18962
+
18963
+ /* Design gap: no drag-over frame exists in Figma. */
18964
+ .dropzone.dragover {
18965
+ border-style: solid;
18966
+ border-color: var(--rel-border-brand);
18967
+ background: color-mix(in srgb, var(--rel-bg-brand) 8%, var(--rel-bg-surface));
18968
+ }
18969
+
18970
+ .icon-badge {
18971
+ display: inline-flex;
18972
+ align-items: center;
18973
+ justify-content: center;
18974
+ padding: var(--rel-spacing-sm, 12px);
18975
+ background: var(--rel-bg-surface);
18976
+ border: 1px solid var(--rel-border-subtle);
18977
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
18978
+ }
18979
+
18980
+ .dz-copy {
18981
+ display: flex;
18982
+ flex-direction: column;
18983
+ align-items: center;
18984
+ gap: var(--rel-spacing-2xs, 4px);
18985
+ }
18986
+
18987
+ .dz-title {
18988
+ margin: 0;
18989
+ color: var(--rel-text-default);
18990
+ font-family: var(--rel-body-lg-medium-font-family);
18991
+ font-weight: var(--rel-body-lg-medium-font-weight);
18992
+ font-size: var(--rel-body-lg-medium-font-size);
18993
+ line-height: var(--rel-body-lg-medium-line-height);
18994
+ }
18995
+
18996
+ .dz-hint {
18997
+ margin: 0;
18998
+ color: var(--rel-text-caption);
18999
+ font-family: var(--rel-body-sm-regular-font-family);
19000
+ font-weight: var(--rel-body-sm-regular-font-weight);
19001
+ font-size: var(--rel-body-sm-regular-font-size);
19002
+ line-height: var(--rel-body-sm-regular-line-height);
19003
+ }
19004
+
19005
+ /* The dropzone is a mouse affordance only — the Browse button is the sole
19006
+ focusable control, so there are no nested interactive elements. */
19007
+ .browse {
19008
+ display: inline-flex;
19009
+ align-items: center;
19010
+ gap: var(--rel-spacing-xs, 8px);
19011
+ padding: var(--rel-spacing-xs, 8px) var(--rel-spacing-md, 16px);
19012
+ background: transparent;
19013
+ color: var(--rel-text-default);
19014
+ border: 1px solid var(--rel-action-neutral-border-outline, var(--rel-border-accent));
19015
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
19016
+ cursor: pointer;
19017
+ font-family: var(--rel-body-md-semibold-font-family);
19018
+ font-weight: var(--rel-body-md-semibold-font-weight);
19019
+ font-size: var(--rel-body-md-semibold-font-size);
19020
+ line-height: var(--rel-body-md-semibold-line-height);
19021
+ }
19022
+
19023
+ .browse:hover:not(:disabled) {
19024
+ background: var(--rel-action-neutral-outline-hover, var(--rel-bg-surface-elevated));
19025
+ }
19026
+
19027
+ .browse:focus-visible {
19028
+ outline: 2px solid var(--rel-border-focus);
19029
+ outline-offset: 2px;
19030
+ }
19031
+
19032
+ .browse:disabled {
19033
+ cursor: not-allowed;
19034
+ }
19035
+
19036
+ /* ── File list ────────────────────────────────────────────────────────── */
19037
+
19038
+ .file-list {
19039
+ display: flex;
19040
+ flex-direction: column;
19041
+ gap: var(--rel-spacing-sm, 12px);
19042
+ }
19043
+
19044
+ .list-heading {
19045
+ margin: 0;
19046
+ color: var(--rel-text-default);
19047
+ font-family: var(--rel-body-md-semibold-font-family);
19048
+ font-weight: var(--rel-body-md-semibold-font-weight);
19049
+ font-size: var(--rel-body-md-semibold-font-size);
19050
+ line-height: var(--rel-body-md-semibold-line-height);
19051
+ }
19052
+
19053
+ .rows {
19054
+ display: flex;
19055
+ flex-direction: column;
19056
+ gap: var(--rel-spacing-xs, 8px);
19057
+ }
19058
+
19059
+ .row {
19060
+ display: flex;
19061
+ align-items: center;
19062
+ gap: var(--rel-spacing-sm, 12px);
19063
+ box-sizing: border-box;
19064
+ padding: var(--rel-spacing-sm, 12px) var(--rel-spacing-md, 16px);
19065
+ background: var(--rel-bg-surface);
19066
+ border: 1px solid var(--rel-border-default);
19067
+ border-radius: var(--rel-border-radius-sm, 8px);
19068
+ }
19069
+
19070
+ .row.error {
19071
+ background: var(--rel-bg-danger-light);
19072
+ border-color: var(--rel-border-danger);
19073
+ }
19074
+
19075
+ .row > rel-icon {
19076
+ flex-shrink: 0;
19077
+ }
19078
+
19079
+ .row-content {
19080
+ display: flex;
19081
+ flex-direction: column;
19082
+ gap: var(--rel-spacing-3xs, 2px);
19083
+ flex: 1;
19084
+ min-width: 0;
19085
+ }
19086
+
19087
+ .row.uploading .row-content {
19088
+ gap: var(--rel-spacing-2xs, 4px);
19089
+ }
19090
+
19091
+ .row-name {
19092
+ overflow: hidden;
19093
+ text-overflow: ellipsis;
19094
+ white-space: nowrap;
19095
+ color: var(--rel-text-default);
19096
+ font-family: var(--rel-body-md-regular-font-family);
19097
+ font-weight: var(--rel-body-md-regular-font-weight);
19098
+ font-size: var(--rel-body-md-regular-font-size);
19099
+ line-height: var(--rel-body-md-regular-line-height);
19100
+ }
19101
+
19102
+ .row-meta {
19103
+ color: var(--rel-text-caption);
19104
+ font-family: var(--rel-body-sm-regular-font-family);
19105
+ font-weight: var(--rel-body-sm-regular-font-weight);
19106
+ font-size: var(--rel-body-sm-regular-font-size);
19107
+ line-height: var(--rel-body-sm-regular-line-height);
19108
+ }
19109
+
19110
+ .row.success .row-meta {
19111
+ color: var(--rel-text-success);
19112
+ }
19113
+
19114
+ .row.error .row-meta {
19115
+ color: var(--rel-text-danger);
16755
19116
  }
16756
19117
 
16757
- .toast.hidden {
16758
- display: none;
19118
+ /* Figma's 4px rounded track; rel-progress ships a 6px square one. */
19119
+ rel-progress::part(track) {
19120
+ height: 4px;
19121
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
16759
19122
  }
16760
19123
 
16761
- /* Message */
16762
- .toast-message {
16763
- flex: 1;
19124
+ rel-progress::part(indicator) {
19125
+ background: var(--rel-bg-success);
19126
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
16764
19127
  }
16765
19128
 
16766
- /* Action button */
16767
- .toast-action {
16768
- background: transparent;
16769
- border: none;
16770
- color: var(--rel-action-neutral-label-icons-fill-inverse);
16771
- font-family: var(--rel-action-md-font-family);
16772
- font-weight: var(--rel-action-md-font-weight);
16773
- font-size: var(--rel-action-md-font-size);
16774
- line-height: var(--rel-action-md-line-height);
16775
- letter-spacing: var(--rel-action-md-letter-spacing);
16776
- cursor: pointer;
16777
- transition: opacity 0.2s;
19129
+ .row-trailing {
19130
+ display: flex;
19131
+ align-items: center;
19132
+ gap: var(--rel-spacing-sm, 12px);
19133
+ flex-shrink: 0;
16778
19134
  }
16779
19135
 
16780
- .toast-action:hover {
16781
- background: var(--rel-bg-neutral);
16782
- color: var(--rel-text-default);
16783
- opacity: 0.75;
19136
+ .status-success {
19137
+ color: var(--rel-text-success);
16784
19138
  }
16785
19139
 
16786
- .toast-action:focus-visible {
16787
- outline: 2px solid var(--rel-text-static-light);
16788
- outline-offset: 2px;
16789
- border-radius: var(--rel-border-radius-xs);
19140
+ .status-error {
19141
+ color: var(--rel-text-danger);
16790
19142
  }
16791
19143
 
16792
- /* Close button */
16793
- .toast-close {
16794
- background: transparent;
16795
- color: var(--rel-action-neutral-label-icons-ghost);
19144
+ .remove {
19145
+ display: inline-flex;
19146
+ align-items: center;
19147
+ justify-content: center;
19148
+ padding: 0;
19149
+ background: none;
16796
19150
  border: none;
16797
- color: inherit;
16798
- font-size: 1rem;
16799
19151
  cursor: pointer;
16800
- line-height: 1;
16801
- padding: var(--rel-spacing-2xs);
16802
- margin-left: var(--rel-spacing-2xs);
16803
- }
16804
-
16805
- .toast-close:hover {
16806
- opacity: 0.75;
19152
+ color: var(--rel-text-default);
16807
19153
  }
16808
19154
 
16809
- .toast-close:focus-visible {
16810
- outline: 2px solid var(--rel-text-static-light);
19155
+ .remove:focus-visible {
19156
+ outline: 2px solid var(--rel-border-focus);
16811
19157
  outline-offset: 2px;
16812
- border-radius: var(--rel-border-radius-xs);
19158
+ border-radius: var(--rel-border-radius-xs, 4px);
16813
19159
  }
16814
19160
 
16815
-
16816
- @media (prefers-reduced-motion: reduce) {
16817
- .toast {
16818
- animation: none;
16819
- }
19161
+ .file-input {
19162
+ display: none;
16820
19163
  }
16821
19164
 
16822
- /* Animations */
16823
- @keyframes fadeInUp {
16824
- from {
16825
- opacity: 0;
16826
- transform: translateY(20px);
16827
- }
16828
- to {
16829
- opacity: 1;
16830
- transform: translateY(0);
19165
+ @media (prefers-reduced-motion: reduce) {
19166
+ .dropzone {
19167
+ transition: none;
16831
19168
  }
16832
19169
  }
16833
19170
  `;
16834
19171
  __decorateClass([
16835
- (0, import_decorators54.property)({ type: String })
16836
- ], RelToast.prototype, "message", 2);
19172
+ (0, import_decorators57.property)({ type: String })
19173
+ ], RelUpload.prototype, "accept", 2);
16837
19174
  __decorateClass([
16838
- (0, import_decorators54.property)({ type: String })
16839
- ], RelToast.prototype, "type", 2);
19175
+ (0, import_decorators57.property)({ type: String, attribute: "max-size" })
19176
+ ], RelUpload.prototype, "maxSize", 2);
16840
19177
  __decorateClass([
16841
- (0, import_decorators54.property)({ type: String })
16842
- ], RelToast.prototype, "actionLabel", 2);
19178
+ (0, import_decorators57.property)({ type: Boolean })
19179
+ ], RelUpload.prototype, "multiple", 2);
16843
19180
  __decorateClass([
16844
- (0, import_decorators54.property)({ type: Number })
16845
- ], RelToast.prototype, "duration", 2);
19181
+ (0, import_decorators57.property)({ type: Boolean, reflect: true })
19182
+ ], RelUpload.prototype, "disabled", 2);
16846
19183
  __decorateClass([
16847
- (0, import_decorators54.property)({ type: Boolean, reflect: true })
16848
- ], RelToast.prototype, "show", 2);
19184
+ (0, import_decorators57.property)({ type: String })
19185
+ ], RelUpload.prototype, "hint", 2);
16849
19186
  __decorateClass([
16850
- (0, import_decorators54.state)()
16851
- ], RelToast.prototype, "visible", 2);
16852
- RelToast = __decorateClass([
16853
- (0, import_decorators54.customElement)("rel-toast")
16854
- ], RelToast);
19187
+ (0, import_decorators57.property)({ type: String, attribute: "dropzone-title" })
19188
+ ], RelUpload.prototype, "dropzoneTitle", 2);
19189
+ __decorateClass([
19190
+ (0, import_decorators57.property)({ type: String, attribute: "browse-label" })
19191
+ ], RelUpload.prototype, "browseLabel", 2);
19192
+ __decorateClass([
19193
+ (0, import_decorators57.property)({ attribute: false })
19194
+ ], RelUpload.prototype, "files", 2);
19195
+ __decorateClass([
19196
+ (0, import_decorators57.property)({ attribute: false })
19197
+ ], RelUpload.prototype, "uploader", 2);
19198
+ __decorateClass([
19199
+ (0, import_decorators57.property)({ type: Number })
19200
+ ], RelUpload.prototype, "concurrency", 2);
19201
+ __decorateClass([
19202
+ (0, import_decorators57.property)({ type: Number, attribute: "delay-threshold" })
19203
+ ], RelUpload.prototype, "delayThreshold", 2);
19204
+ __decorateClass([
19205
+ (0, import_decorators57.state)()
19206
+ ], RelUpload.prototype, "dragOver", 2);
19207
+ __decorateClass([
19208
+ (0, import_decorators57.state)()
19209
+ ], RelUpload.prototype, "delayed", 2);
19210
+ __decorateClass([
19211
+ (0, import_decorators57.query)(".file-input")
19212
+ ], RelUpload.prototype, "inputEl", 2);
19213
+ RelUpload = __decorateClass([
19214
+ (0, import_decorators57.customElement)("rel-upload")
19215
+ ], RelUpload);
16855
19216
 
16856
- // src/components/rel-tooltip/rel-tooltip.ts
16857
- var import_lit55 = require("lit");
16858
- var import_decorators55 = require("lit/decorators.js");
16859
- var RelTooltip = class extends import_lit55.LitElement {
19217
+ // src/components/rel-upload/rel-upload-actions.ts
19218
+ var import_lit58 = require("lit");
19219
+ var import_decorators58 = require("lit/decorators.js");
19220
+ var import_rel_icon18 = require("@reliability-design/icons/rel-icon");
19221
+ var EMPTY_STATE = {
19222
+ phase: "empty",
19223
+ total: 0,
19224
+ failed: 0,
19225
+ canUpload: false,
19226
+ primaryLabel: "Upload"
19227
+ };
19228
+ var RelUploadActions = class extends import_lit58.LitElement {
16860
19229
  constructor() {
16861
19230
  super(...arguments);
16862
- this.text = "";
16863
- this.position = "above";
16864
- this.showTail = false;
16865
- this.type = "single-line";
16866
- // Stable id so aria-describedby can reference the bubble across renders.
16867
- this.uniqueId = Math.random().toString(36).slice(2);
19231
+ this.for = "";
19232
+ this.cancelLabel = "Cancel";
19233
+ this.noCancel = false;
19234
+ this.snapshot = EMPTY_STATE;
19235
+ this.target = null;
19236
+ this.onState = (e) => {
19237
+ if (!this.target) this.resolveTarget();
19238
+ if (e.target !== this.target) return;
19239
+ this.snapshot = e.detail;
19240
+ };
19241
+ this.onCancel = () => {
19242
+ this.target?.cancel();
19243
+ this.dispatchEvent(new CustomEvent("rel-cancel", { bubbles: true, composed: true }));
19244
+ };
19245
+ this.onPrimary = () => {
19246
+ if (!this.target) return;
19247
+ if (this.snapshot.phase === "failed") this.target.retry();
19248
+ else this.target.upload();
19249
+ };
16868
19250
  }
16869
- firstUpdated() {
16870
- this.linkTrigger();
19251
+ connectedCallback() {
19252
+ super.connectedCallback();
19253
+ queueMicrotask(() => this.resolveTarget());
19254
+ this.root.addEventListener("rel-state", this.onState);
16871
19255
  }
16872
- linkTrigger() {
16873
- const slot = this.shadowRoot?.querySelector("slot");
16874
- if (!slot) return;
16875
- const trigger = slot.assignedElements()[0];
16876
- if (trigger) {
16877
- trigger.setAttribute("aria-describedby", `tooltip-${this.uniqueId}`);
19256
+ disconnectedCallback() {
19257
+ super.disconnectedCallback();
19258
+ this.root.removeEventListener("rel-state", this.onState);
19259
+ this.target = null;
19260
+ }
19261
+ updated(changed) {
19262
+ if (changed.has("for")) this.resolveTarget();
19263
+ }
19264
+ /**
19265
+ * Listen on our own root rather than `document`: a shared root means no
19266
+ * shadow boundary sits between us and the sibling `<rel-upload>`, so
19267
+ * `event.target` is never retargeted away from it.
19268
+ */
19269
+ get root() {
19270
+ const node = this.getRootNode();
19271
+ return node instanceof ShadowRoot ? node : this.ownerDocument;
19272
+ }
19273
+ resolveTarget() {
19274
+ const root = this.getRootNode();
19275
+ let found = null;
19276
+ if (this.for) {
19277
+ found = root.querySelector(`#${CSS.escape(this.for)}`);
19278
+ } else {
19279
+ found = this.parentElement?.querySelector("rel-upload") ?? root.querySelector("rel-upload");
16878
19280
  }
19281
+ this.target = found;
19282
+ if (found) this.snapshot = found.state;
16879
19283
  }
16880
19284
  render() {
16881
- return import_lit55.html`
16882
- <slot @slotchange="${this.linkTrigger}"></slot>
16883
- <div
16884
- part="panel" class="tooltip-bubble ${this.position}"
16885
- id="tooltip-${this.uniqueId}"
16886
- role="tooltip"
16887
- >
16888
- ${this.text}
16889
- ${this.showTail ? import_lit55.html`<span part="indicator" class="tooltip-tail"></span>` : ""}
19285
+ const { phase, canUpload, primaryLabel } = this.snapshot;
19286
+ const busy = phase === "uploading" || phase === "delayed";
19287
+ return import_lit58.html`
19288
+ <div class="actions" part="base">
19289
+ ${this.noCancel ? import_lit58.nothing : import_lit58.html`<rel-button part="cancel" variant="secondary" @click=${this.onCancel}>
19290
+ ${this.cancelLabel}
19291
+ </rel-button>`}
19292
+
19293
+ <rel-button
19294
+ part="primary"
19295
+ variant="primary"
19296
+ ?loading=${busy}
19297
+ ?disabled=${!canUpload}
19298
+ aria-label=${busy ? "Uploading" : primaryLabel}
19299
+ @click=${this.onPrimary}
19300
+ >
19301
+ ${busy ? import_lit58.nothing : import_lit58.html`${phase === "failed" ? import_lit58.html`<rel-icon name="refresh" size="sm"></rel-icon>` : import_lit58.nothing}${primaryLabel}`}
19302
+ </rel-button>
16890
19303
  </div>
16891
19304
  `;
16892
19305
  }
16893
19306
  };
16894
- RelTooltip.styles = import_lit55.css`
19307
+ RelUploadActions.styles = import_lit58.css`
16895
19308
  :host {
16896
- color: var(--rel-text-default);
16897
- display: inline-block;
16898
- position: relative;
16899
- }
16900
-
16901
- /* Tooltip container */
16902
- .tooltip-bubble {
16903
- font-family: var(--rel-body-md-regular-font-family);
16904
- font-weight: var(--rel-body-md-medium-font-weight);
16905
- font-size: var(--rel-body-md-regular-font-size);
16906
- line-height: var(--rel-body-md-regular-line-height);
16907
- letter-spacing: var(--rel-body-md-regular-letter-spacing);
16908
- position: absolute;
16909
- background-color: var(--rel-bg-neutral-inverse-2);
16910
- color: var(--rel-text-inverse);
16911
- padding: var(--rel-spacing-xs, 8px)
16912
- var(--rel-spacing-sm, 12px);
16913
- border-radius: var(--rel-border-radius-sm, 8px);
16914
- white-space: nowrap;
16915
- z-index: 1000;
16916
- opacity: 0;
16917
- pointer-events: none;
16918
- transition: opacity 0.2s ease;
16919
- overflow: visible;
16920
- }
16921
-
16922
- /* Figma Type=Multi Line: wrap inside a bounded bubble */
16923
- :host([type="multi-line"]) .tooltip-bubble {
16924
- white-space: normal;
16925
- max-width: 220px;
16926
- width: max-content;
16927
- }
16928
-
16929
- :host(:hover) .tooltip-bubble,
16930
- :host(:focus-within) .tooltip-bubble {
16931
- opacity: 1;
16932
- }
16933
-
16934
- /* Positioning */
16935
- .tooltip-bubble.above {
16936
- bottom: calc(100% + 6px);
16937
- left: 50%;
16938
- transform: translateX(-50%);
16939
- }
16940
-
16941
- .tooltip-bubble.below {
16942
- top: calc(100% + 6px);
16943
- left: 50%;
16944
- transform: translateX(-50%);
16945
- }
16946
-
16947
- .tooltip-bubble.left {
16948
- right: calc(100% + 6px);
16949
- top: 50%;
16950
- transform: translateY(-50%);
16951
- }
16952
-
16953
- .tooltip-bubble.right {
16954
- left: calc(100% + 6px);
16955
- top: 50%;
16956
- transform: translateY(-50%);
16957
- }
16958
-
16959
- /* Tail/Arrow */
16960
- .tooltip-tail {
16961
- position: absolute;
16962
- width: 0;
16963
- height: 0;
16964
19309
  display: block;
19310
+ font-family: var(--rel-fonts-font-family);
16965
19311
  }
16966
19312
 
16967
- .tooltip-bubble.above .tooltip-tail,
16968
- .tooltip-bubble.below .tooltip-tail {
16969
- left: 50%;
16970
- transform: translateX(-50%);
16971
- border-left: 6px solid transparent;
16972
- border-right: 6px solid transparent;
16973
- }
16974
-
16975
- .tooltip-bubble.above .tooltip-tail {
16976
- bottom: -6px;
16977
- border-top: 6px solid var(--rel-bg-neutral-inverse-2);
16978
- border-bottom: none;
16979
- }
16980
-
16981
- .tooltip-bubble.below .tooltip-tail {
16982
- top: -6px;
16983
- border-bottom: 6px solid var(--rel-bg-neutral-inverse-2);
16984
- border-top: none;
16985
- }
16986
-
16987
- .tooltip-bubble.left .tooltip-tail,
16988
- .tooltip-bubble.right .tooltip-tail {
16989
- top: 50%;
16990
- transform: translateY(-50%);
16991
- border-top: 6px solid transparent;
16992
- border-bottom: 6px solid transparent;
16993
- }
16994
-
16995
- .tooltip-bubble.left .tooltip-tail {
16996
- right: -6px;
16997
- border-left: 6px solid var(--rel-bg-neutral-inverse-2);
16998
- border-right: none;
19313
+ .actions {
19314
+ display: flex;
19315
+ align-items: center;
19316
+ justify-content: flex-end;
19317
+ gap: var(--rel-spacing-xs, 8px);
16999
19318
  }
17000
19319
 
17001
- .tooltip-bubble.right .tooltip-tail {
17002
- left: -6px;
17003
- border-right: 6px solid var(--rel-bg-neutral-inverse-2);
17004
- border-left: none;
19320
+ rel-button rel-icon {
19321
+ margin-inline-end: var(--rel-spacing-xs, 8px);
17005
19322
  }
17006
19323
  `;
17007
19324
  __decorateClass([
17008
- (0, import_decorators55.property)({ type: String })
17009
- ], RelTooltip.prototype, "text", 2);
19325
+ (0, import_decorators58.property)({ type: String })
19326
+ ], RelUploadActions.prototype, "for", 2);
17010
19327
  __decorateClass([
17011
- (0, import_decorators55.property)({ type: String })
17012
- ], RelTooltip.prototype, "position", 2);
19328
+ (0, import_decorators58.property)({ type: String, attribute: "cancel-label" })
19329
+ ], RelUploadActions.prototype, "cancelLabel", 2);
17013
19330
  __decorateClass([
17014
- (0, import_decorators55.property)({ type: Boolean, attribute: "show-tail" })
17015
- ], RelTooltip.prototype, "showTail", 2);
19331
+ (0, import_decorators58.property)({ type: Boolean, attribute: "no-cancel" })
19332
+ ], RelUploadActions.prototype, "noCancel", 2);
17016
19333
  __decorateClass([
17017
- (0, import_decorators55.property)({ type: String, reflect: true })
17018
- ], RelTooltip.prototype, "type", 2);
17019
- RelTooltip = __decorateClass([
17020
- (0, import_decorators55.customElement)("rel-tooltip")
17021
- ], RelTooltip);
19334
+ (0, import_decorators58.state)()
19335
+ ], RelUploadActions.prototype, "snapshot", 2);
19336
+ RelUploadActions = __decorateClass([
19337
+ (0, import_decorators58.customElement)("rel-upload-actions")
19338
+ ], RelUploadActions);
17022
19339
 
17023
19340
  // src/index.ts
17024
19341
  __reExport(index_exports, require("@reliability-design/icons"), module.exports);
@@ -17071,6 +19388,7 @@ __reExport(index_exports, require("@reliability-design/icons"), module.exports);
17071
19388
  RelTab,
17072
19389
  RelTabItem,
17073
19390
  RelTextfield,
19391
+ RelTimePicker,
17074
19392
  RelToast,
17075
19393
  RelTooltip,
17076
19394
  RelTopbar,
@@ -17082,6 +19400,8 @@ __reExport(index_exports, require("@reliability-design/icons"), module.exports);
17082
19400
  RelTopbarMobilePanel,
17083
19401
  RelTopbarProfile,
17084
19402
  RelTopbarUtilities,
19403
+ RelUpload,
19404
+ RelUploadActions,
17085
19405
  ...require("@reliability-design/icons")
17086
19406
  });
17087
19407
  //# sourceMappingURL=index.js.map