@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/components/rel-autocomplete/README.md +1 -0
- package/dist/components/rel-autocomplete/rel-autocomplete.ts +56 -1
- package/dist/components/rel-checkbox/README.md +69 -22
- package/dist/components/rel-checkbox/rel-checkbox.ts +116 -9
- package/dist/components/rel-pagination/README.md +14 -0
- package/dist/components/rel-pagination/rel-pagination.ts +30 -14
- package/dist/components/rel-range-picker/rel-range-picker.ts +117 -2
- package/dist/components/rel-select/README.md +1 -0
- package/dist/components/rel-select/rel-select.ts +1237 -1170
- package/dist/components/rel-upload/UPLOAD-DESIGN-CHANGES.md +134 -0
- package/dist/components/rel-upload/rel-upload-actions.ts +163 -0
- package/dist/components/rel-upload/rel-upload.ts +943 -0
- package/dist/index.d.mts +267 -2
- package/dist/index.d.ts +267 -2
- package/dist/index.js +1259 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1273 -61
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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(() =>
|
|
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(
|
|
1801
|
+
async runSearch(query12) {
|
|
1764
1802
|
const seq = ++this.searchSeq;
|
|
1765
1803
|
try {
|
|
1766
|
-
const results = await this.searchFn(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
|
|
11542
|
-
|
|
11543
|
-
|
|
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:
|
|
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-
|
|
13664
|
-
|
|
13665
|
-
|
|
13666
|
-
|
|
13667
|
-
|
|
13668
|
-
|
|
13669
|
-
|
|
13670
|
-
|
|
13671
|
-
|
|
13672
|
-
|
|
13673
|
-
|
|
13674
|
-
|
|
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
|
-
.
|
|
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
|