@reliability-design/web 1.4.3 → 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
@@ -87,7 +87,9 @@ __export(index_exports, {
87
87
  RelTopbarItem: () => RelTopbarItem,
88
88
  RelTopbarMobilePanel: () => RelTopbarMobilePanel,
89
89
  RelTopbarProfile: () => RelTopbarProfile,
90
- RelTopbarUtilities: () => RelTopbarUtilities
90
+ RelTopbarUtilities: () => RelTopbarUtilities,
91
+ RelUpload: () => RelUpload,
92
+ RelUploadActions: () => RelUploadActions
91
93
  });
92
94
  module.exports = __toCommonJS(index_exports);
93
95
  var import_vars = require("./vars-XNMTSRVN.css");
@@ -1648,6 +1650,14 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1648
1650
  if (left + t.width > window.innerWidth - 8) {
1649
1651
  left = Math.max(8, window.innerWidth - t.width - 8);
1650
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`;
1651
1661
  this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
1652
1662
  };
1653
1663
  }
@@ -1659,6 +1669,7 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1659
1669
  disconnectedCallback() {
1660
1670
  super.disconnectedCallback();
1661
1671
  this.removeGlobalListeners();
1672
+ this.hideNativePopover();
1662
1673
  clearTimeout(this.debounceTimer);
1663
1674
  }
1664
1675
  willUpdate(changed) {
@@ -1718,13 +1729,40 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1718
1729
  this.open = true;
1719
1730
  this.highlightedIndex = -1;
1720
1731
  this.addGlobalListeners();
1721
- requestAnimationFrame(() => this.updatePosition());
1732
+ requestAnimationFrame(() => {
1733
+ this.showNativePopover();
1734
+ this.updatePosition();
1735
+ });
1722
1736
  }
1723
1737
  closePanel() {
1724
1738
  if (!this.open) return;
1725
1739
  this.open = false;
1726
1740
  this.highlightedIndex = -1;
1727
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
+ }
1728
1766
  }
1729
1767
  /** Re-evaluates whether the panel should be showing for the current state. */
1730
1768
  syncPanel() {
@@ -1760,17 +1798,17 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1760
1798
  this.syncPanel();
1761
1799
  }
1762
1800
  }
1763
- async runSearch(query11) {
1801
+ async runSearch(query12) {
1764
1802
  const seq = ++this.searchSeq;
1765
1803
  try {
1766
- const results = await this.searchFn(query11);
1804
+ const results = await this.searchFn(query12);
1767
1805
  if (seq !== this.searchSeq) return;
1768
1806
  this.remoteItems = results ?? [];
1769
1807
  } catch (err) {
1770
1808
  if (seq !== this.searchSeq) return;
1771
1809
  this.remoteItems = [];
1772
1810
  this.dispatchEvent(
1773
- new CustomEvent("rel-search-error", { detail: { query: query11, error: err }, bubbles: true, composed: true })
1811
+ new CustomEvent("rel-search-error", { detail: { query: query12, error: err }, bubbles: true, composed: true })
1774
1812
  );
1775
1813
  } finally {
1776
1814
  if (seq === this.searchSeq) {
@@ -1940,6 +1978,7 @@ var RelAutocomplete = class extends import_lit4.LitElement {
1940
1978
  id="${panelId}"
1941
1979
  part="panel list"
1942
1980
  class="ac-panel"
1981
+ popover="manual"
1943
1982
  role="listbox"
1944
1983
  aria-label="${this.accessibleName || "Suggestions"}"
1945
1984
  style="${this.panelStyle}"
@@ -1994,6 +2033,15 @@ RelAutocomplete.styles = import_lit4.css`
1994
2033
  position: fixed;
1995
2034
  z-index: 1001;
1996
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;
1997
2045
  box-sizing: border-box;
1998
2046
  background: var(--rel-autocomplete-bg, var(--rel-bg-neutral));
1999
2047
  border: 1px solid var(--rel-autocomplete-menu-border, var(--rel-border-default));
@@ -4467,6 +4515,9 @@ var RelCheckbox = class extends import_lit13.LitElement {
4467
4515
  this.state = "default";
4468
4516
  this.status = "unchecked";
4469
4517
  this.value = "";
4518
+ this.size = "medium";
4519
+ this.density = "compact";
4520
+ this.alignment = "leading";
4470
4521
  this._onChange = () => {
4471
4522
  };
4472
4523
  this._onTouched = () => {
@@ -4478,6 +4529,17 @@ var RelCheckbox = class extends import_lit13.LitElement {
4478
4529
  set checked(val) {
4479
4530
  this.status = val ? "checked" : "unchecked";
4480
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
+ }
4481
4543
  toggle() {
4482
4544
  if (this.state === "disabled") return;
4483
4545
  this.status = this.status === "checked" ? "unchecked" : "checked";
@@ -4525,12 +4587,22 @@ var RelCheckbox = class extends import_lit13.LitElement {
4525
4587
  </svg>
4526
4588
  </span>
4527
4589
 
4528
- ${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}
4597
+
4598
+ <span part="label" class="label">${this.label}</span>
4599
+ </span>
4529
4600
 
4530
- <span part="label" class="label">${this.label}</span>
4601
+ ${this.effectiveDensity === "comfortable" && this.supportingtext ? import_lit13.html`<span part="helper" class="text">${this.supportingtext}</span>` : null}
4602
+ </span>
4531
4603
  </label>
4532
4604
 
4533
- ${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}
4534
4606
  </div>
4535
4607
  `;
4536
4608
  }
@@ -4563,6 +4635,29 @@ RelCheckbox.styles = import_lit13.css`
4563
4635
  cursor: pointer;
4564
4636
  }
4565
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
+
4566
4661
  input {
4567
4662
  position: absolute;
4568
4663
  inset: 0;
@@ -4705,7 +4800,6 @@ RelCheckbox.styles = import_lit13.css`
4705
4800
 
4706
4801
  .text {
4707
4802
  color: var(--rel-text-caption);
4708
- padding-left: 28px;
4709
4803
 
4710
4804
  font-family: var(--rel-body-sm-regular-font-family);
4711
4805
 
@@ -4718,6 +4812,44 @@ RelCheckbox.styles = import_lit13.css`
4718
4812
  letter-spacing: var(--rel-body-sm-regular-letter-spacing);
4719
4813
 
4720
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
+ }
4721
4853
  `;
4722
4854
  __decorateClass([
4723
4855
  (0, import_decorators13.property)({ type: String })
@@ -4737,6 +4869,15 @@ __decorateClass([
4737
4869
  __decorateClass([
4738
4870
  (0, import_decorators13.property)({ type: String })
4739
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);
4740
4881
  RelCheckbox = __decorateClass([
4741
4882
  (0, import_decorators13.customElement)("rel-checkbox")
4742
4883
  ], RelCheckbox);
@@ -5868,6 +6009,31 @@ var RelRangePicker = class extends import_lit15.LitElement {
5868
6009
  this.viewMonth = (/* @__PURE__ */ new Date()).getMonth();
5869
6010
  this.isSelectingRange = false;
5870
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
+ };
5871
6037
  // ── Open / close ──
5872
6038
  this.handleClickOutside = (e) => {
5873
6039
  if (!this.open) return;
@@ -5910,14 +6076,64 @@ var RelRangePicker = class extends import_lit15.LitElement {
5910
6076
  super.disconnectedCallback();
5911
6077
  document.removeEventListener("click", this.handleClickOutside);
5912
6078
  document.removeEventListener("keydown", this.handleGlobalKeydown);
6079
+ window.removeEventListener("resize", this.onReposition);
6080
+ window.removeEventListener("scroll", this.onReposition, true);
6081
+ this.hideTopLayer();
5913
6082
  }
5914
- updated() {
6083
+ updated(changed) {
5915
6084
  this.style.width = this.width;
5916
6085
  this.style.minWidth = this.minWidth;
5917
6086
  this.style.maxWidth = this.maxWidth;
5918
6087
  this.style.height = this.height;
5919
6088
  this.style.minHeight = this.minHeight;
5920
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
+ }
5921
6137
  }
5922
6138
  toggleOpen() {
5923
6139
  if (this.disabled) return;
@@ -6273,10 +6489,11 @@ var RelRangePicker = class extends import_lit15.LitElement {
6273
6489
  </button>
6274
6490
  `}
6275
6491
 
6276
- ${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}
6277
6493
 
6278
6494
  <div
6279
6495
  part="panel" class="popover ${this.open ? "open" : ""} ${isPopup ? "popup" : ""}"
6496
+ popover="manual"
6280
6497
  role="dialog"
6281
6498
  aria-label="Select date range"
6282
6499
  >
@@ -6487,6 +6704,17 @@ RelRangePicker.styles = import_lit15.css`
6487
6704
 
6488
6705
  /* ── Popover shell ── */
6489
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;
6490
6718
  position: absolute;
6491
6719
  top: calc(100% + 4px);
6492
6720
  left: 0;
@@ -6511,6 +6739,13 @@ RelRangePicker.styles = import_lit15.css`
6511
6739
  .backdrop {
6512
6740
  position: fixed;
6513
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;
6514
6749
  z-index: 2147483646;
6515
6750
  background: rgba(0,0,0,0.4);
6516
6751
  }
@@ -6884,6 +7119,12 @@ __decorateClass([
6884
7119
  __decorateClass([
6885
7120
  (0, import_decorators15.query)(".trigger-button")
6886
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);
6887
7128
  RelRangePicker = __decorateClass([
6888
7129
  (0, import_decorators15.customElement)("rel-range-picker")
6889
7130
  ], RelRangePicker);
@@ -11488,6 +11729,14 @@ var RelPagination = class extends import_lit38.LitElement {
11488
11729
  }
11489
11730
  return pages;
11490
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
+ }
11491
11740
  handlePageChange(newPage) {
11492
11741
  const total = this.computedTotalPages;
11493
11742
  if (this.disabled || newPage < 1 || newPage > total || newPage === this.activePage) return;
@@ -11533,15 +11782,20 @@ var RelPagination = class extends import_lit38.LitElement {
11533
11782
  <div class="pag-stack">
11534
11783
  ${this.renderNavBtn("prev")}
11535
11784
  <div class="pag-numbers">
11536
- ${pages.map(
11537
- (p) => p === -1 ? import_lit38.html`<span class="pag-ellipsis">...</span>` : import_lit38.html`<button
11538
- type="button"
11539
- class="pag-page-btn ${Math.abs(p - current) <= 1 ? "near" : ""} ${p === 1 || p === total ? "edge" : ""}"
11540
- aria-label="Page ${p}"
11541
- aria-current="${p === current ? "page" : "false"}"
11542
- @click="${() => this.handlePageChange(p)}"
11543
- >${p}</button>`
11544
- )}
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
+ })}
11545
11799
  </div>
11546
11800
  ${this.renderNavBtn("next")}
11547
11801
  </div>
@@ -11694,9 +11948,10 @@ RelPagination.styles = import_lit38.css`
11694
11948
  display: inline-flex;
11695
11949
  align-items: center;
11696
11950
  justify-content: center;
11697
- width: 40px;
11951
+ width: auto;
11952
+ min-width: 40px;
11698
11953
  height: 40px;
11699
- padding: var(--rel-spacing-xs);
11954
+ padding: 0 var(--rel-spacing-xs);
11700
11955
  border-radius: var(--rel-border-radius-sm);
11701
11956
  border: 1px solid var(--rel-border-default);
11702
11957
  background: var(--rel-bg-neutral);
@@ -13299,6 +13554,14 @@ var RelSelect = class extends import_lit46.LitElement {
13299
13554
  if (left + t.width > window.innerWidth - 8) {
13300
13555
  left = Math.max(8, window.innerWidth - t.width - 8);
13301
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`;
13302
13565
  this.panelStyle = `top:${top}px;left:${left}px;width:${t.width}px;`;
13303
13566
  };
13304
13567
  this.onSearchInput = (e) => {
@@ -13316,6 +13579,7 @@ var RelSelect = class extends import_lit46.LitElement {
13316
13579
  disconnectedCallback() {
13317
13580
  super.disconnectedCallback();
13318
13581
  this.removeGlobalListeners();
13582
+ this.hideNativePopover();
13319
13583
  this.chipResizeObserver?.disconnect();
13320
13584
  }
13321
13585
  firstUpdated() {
@@ -13432,6 +13696,7 @@ var RelSelect = class extends import_lit46.LitElement {
13432
13696
  this.highlightedIndex = -1;
13433
13697
  this.addGlobalListeners();
13434
13698
  requestAnimationFrame(() => {
13699
+ this.showNativePopover();
13435
13700
  this.updatePosition();
13436
13701
  if (!this.noSearch) {
13437
13702
  this.searchInputEl?.focus({ preventScroll: true });
@@ -13442,8 +13707,32 @@ var RelSelect = class extends import_lit46.LitElement {
13442
13707
  if (!this.open) return;
13443
13708
  this.open = false;
13444
13709
  this.removeGlobalListeners();
13710
+ this.hideNativePopover();
13445
13711
  this.triggerEl?.focus();
13446
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
+ }
13447
13736
  selectSingleOption(option) {
13448
13737
  this.selected = option.value;
13449
13738
  if (this.state === "error") this.state = "default";
@@ -13654,39 +13943,42 @@ var RelSelect = class extends import_lit46.LitElement {
13654
13943
  <div
13655
13944
  id="${listboxId}"
13656
13945
  part="panel list" class="select-listbox"
13946
+ popover="manual"
13657
13947
  role="listbox"
13658
13948
  aria-multiselectable="${this.multiple}"
13659
13949
  aria-label="${accessibleName || "Options"}"
13660
13950
  style="${this.panelStyle}"
13661
13951
  ?data-open="${this.open}"
13662
13952
  >
13663
- ${!this.noSearch ? import_lit46.html`<div class="select-search">
13664
- <div class="search-input-wrap">
13665
- <rel-icon name="search" size="${this.searchIconSize}" class="icon"></rel-icon>
13666
- <input
13667
- part="input" class="search-input"
13668
- type="text"
13669
- placeholder="${this.searchPlaceholder}"
13670
- .value="${this.searchValue}"
13671
- @input="${this.onSearchInput}"
13672
- @keydown="${(e) => this.onSearchKeyDown(e)}"
13673
- />
13674
- </div>
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>` : ""}
13675
13977
  </div>` : ""}
13676
13978
 
13677
13979
  ${filtered.length === 0 ? import_lit46.html`<div class="select-empty">
13678
13980
  ${opts.length === 0 ? "No options available" : "No matches found"}
13679
13981
  </div>` : this.multiple ? import_lit46.html`
13680
- <div class="select-all-row">
13681
- <rel-checkbox
13682
- label="${this.selectAllLabel}"
13683
- status="${this.selectAllStatus}"
13684
- @rel-change=${(e) => {
13685
- e.stopPropagation();
13686
- this.toggleSelectAll();
13687
- }}
13688
- ></rel-checkbox>
13689
- </div>
13690
13982
  <div class="select-options">
13691
13983
  ${(0, import_repeat2.repeat)(
13692
13984
  filtered,
@@ -14029,6 +14321,15 @@ RelSelect.styles = import_lit46.css`
14029
14321
  position: fixed;
14030
14322
  z-index: 1001;
14031
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;
14032
14333
  box-sizing: border-box;
14033
14334
  background: var(--rel-select-bg, var(--rel-bg-neutral));
14034
14335
  border: 1px solid var(--rel-select-menu-border, var(--rel-border-default));
@@ -14045,10 +14346,18 @@ RelSelect.styles = import_lit46.css`
14045
14346
  display: block;
14046
14347
  }
14047
14348
 
14048
- .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 {
14049
14354
  position: sticky;
14050
14355
  top: 0;
14356
+ z-index: 1;
14051
14357
  background: var(--rel-select-bg, var(--rel-bg-neutral));
14358
+ }
14359
+
14360
+ .select-search {
14052
14361
  padding: var(--rel-spacing-xs);
14053
14362
  border-bottom: 1px solid var(--rel-select-divider, var(--rel-border-subtle));
14054
14363
  }
@@ -18125,6 +18434,909 @@ RelTooltip = __decorateClass([
18125
18434
  (0, import_decorators56.customElement)("rel-tooltip")
18126
18435
  ], RelTooltip);
18127
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);
19116
+ }
19117
+
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);
19122
+ }
19123
+
19124
+ rel-progress::part(indicator) {
19125
+ background: var(--rel-bg-success);
19126
+ border-radius: var(--rel-border-radius-full-rounded, 1000px);
19127
+ }
19128
+
19129
+ .row-trailing {
19130
+ display: flex;
19131
+ align-items: center;
19132
+ gap: var(--rel-spacing-sm, 12px);
19133
+ flex-shrink: 0;
19134
+ }
19135
+
19136
+ .status-success {
19137
+ color: var(--rel-text-success);
19138
+ }
19139
+
19140
+ .status-error {
19141
+ color: var(--rel-text-danger);
19142
+ }
19143
+
19144
+ .remove {
19145
+ display: inline-flex;
19146
+ align-items: center;
19147
+ justify-content: center;
19148
+ padding: 0;
19149
+ background: none;
19150
+ border: none;
19151
+ cursor: pointer;
19152
+ color: var(--rel-text-default);
19153
+ }
19154
+
19155
+ .remove:focus-visible {
19156
+ outline: 2px solid var(--rel-border-focus);
19157
+ outline-offset: 2px;
19158
+ border-radius: var(--rel-border-radius-xs, 4px);
19159
+ }
19160
+
19161
+ .file-input {
19162
+ display: none;
19163
+ }
19164
+
19165
+ @media (prefers-reduced-motion: reduce) {
19166
+ .dropzone {
19167
+ transition: none;
19168
+ }
19169
+ }
19170
+ `;
19171
+ __decorateClass([
19172
+ (0, import_decorators57.property)({ type: String })
19173
+ ], RelUpload.prototype, "accept", 2);
19174
+ __decorateClass([
19175
+ (0, import_decorators57.property)({ type: String, attribute: "max-size" })
19176
+ ], RelUpload.prototype, "maxSize", 2);
19177
+ __decorateClass([
19178
+ (0, import_decorators57.property)({ type: Boolean })
19179
+ ], RelUpload.prototype, "multiple", 2);
19180
+ __decorateClass([
19181
+ (0, import_decorators57.property)({ type: Boolean, reflect: true })
19182
+ ], RelUpload.prototype, "disabled", 2);
19183
+ __decorateClass([
19184
+ (0, import_decorators57.property)({ type: String })
19185
+ ], RelUpload.prototype, "hint", 2);
19186
+ __decorateClass([
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);
19216
+
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 {
19229
+ constructor() {
19230
+ super(...arguments);
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
+ };
19250
+ }
19251
+ connectedCallback() {
19252
+ super.connectedCallback();
19253
+ queueMicrotask(() => this.resolveTarget());
19254
+ this.root.addEventListener("rel-state", this.onState);
19255
+ }
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");
19280
+ }
19281
+ this.target = found;
19282
+ if (found) this.snapshot = found.state;
19283
+ }
19284
+ render() {
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>
19303
+ </div>
19304
+ `;
19305
+ }
19306
+ };
19307
+ RelUploadActions.styles = import_lit58.css`
19308
+ :host {
19309
+ display: block;
19310
+ font-family: var(--rel-fonts-font-family);
19311
+ }
19312
+
19313
+ .actions {
19314
+ display: flex;
19315
+ align-items: center;
19316
+ justify-content: flex-end;
19317
+ gap: var(--rel-spacing-xs, 8px);
19318
+ }
19319
+
19320
+ rel-button rel-icon {
19321
+ margin-inline-end: var(--rel-spacing-xs, 8px);
19322
+ }
19323
+ `;
19324
+ __decorateClass([
19325
+ (0, import_decorators58.property)({ type: String })
19326
+ ], RelUploadActions.prototype, "for", 2);
19327
+ __decorateClass([
19328
+ (0, import_decorators58.property)({ type: String, attribute: "cancel-label" })
19329
+ ], RelUploadActions.prototype, "cancelLabel", 2);
19330
+ __decorateClass([
19331
+ (0, import_decorators58.property)({ type: Boolean, attribute: "no-cancel" })
19332
+ ], RelUploadActions.prototype, "noCancel", 2);
19333
+ __decorateClass([
19334
+ (0, import_decorators58.state)()
19335
+ ], RelUploadActions.prototype, "snapshot", 2);
19336
+ RelUploadActions = __decorateClass([
19337
+ (0, import_decorators58.customElement)("rel-upload-actions")
19338
+ ], RelUploadActions);
19339
+
18128
19340
  // src/index.ts
18129
19341
  __reExport(index_exports, require("@reliability-design/icons"), module.exports);
18130
19342
  // Annotate the CommonJS export names for ESM import in node:
@@ -18188,6 +19400,8 @@ __reExport(index_exports, require("@reliability-design/icons"), module.exports);
18188
19400
  RelTopbarMobilePanel,
18189
19401
  RelTopbarProfile,
18190
19402
  RelTopbarUtilities,
19403
+ RelUpload,
19404
+ RelUploadActions,
18191
19405
  ...require("@reliability-design/icons")
18192
19406
  });
18193
19407
  //# sourceMappingURL=index.js.map