@masters-union/union-stack 0.3.10 → 0.3.12
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/cdn/loader.v1.global.js +34 -2
- package/dist/cdn/loader.v1.global.js.map +1 -1
- package/dist/{chunk-IV76YPDA.cjs → chunk-45FKUCRN.cjs} +7 -7
- package/dist/{chunk-IV76YPDA.cjs.map → chunk-45FKUCRN.cjs.map} +1 -1
- package/dist/{chunk-F4EVNUJZ.cjs → chunk-BLZGF6JP.cjs} +3 -3
- package/dist/{chunk-F4EVNUJZ.cjs.map → chunk-BLZGF6JP.cjs.map} +1 -1
- package/dist/{chunk-IIVRPVNX.js → chunk-UPKEFUU6.js} +3 -3
- package/dist/{chunk-IIVRPVNX.js.map → chunk-UPKEFUU6.js.map} +1 -1
- package/dist/{chunk-MFZ2JGLP.js → chunk-XGSFHZSI.js} +3 -3
- package/dist/{chunk-MFZ2JGLP.js.map → chunk-XGSFHZSI.js.map} +1 -1
- package/dist/index.cjs +4 -4
- package/dist/index.js +3 -3
- package/dist/node.cjs +7 -7
- package/dist/node.js +1 -1
- package/dist/picker.cjs +137 -23
- package/dist/picker.cjs.map +1 -1
- package/dist/picker.d.cts +1 -0
- package/dist/picker.d.ts +1 -0
- package/dist/picker.js +137 -23
- package/dist/picker.js.map +1 -1
- package/dist/react.cjs +3 -3
- package/dist/react.js +2 -2
- package/package.json +1 -1
package/dist/picker.d.cts
CHANGED
package/dist/picker.d.ts
CHANGED
package/dist/picker.js
CHANGED
|
@@ -105,6 +105,10 @@ var BASE_CSS = `
|
|
|
105
105
|
.us-picker-backdrop {
|
|
106
106
|
--us-font: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
107
107
|
--us-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
|
108
|
+
/* Default to a light color-scheme so native controls never inherit the
|
|
109
|
+
visitor's OS dark mode; picker.ts overrides this to match the resolved
|
|
110
|
+
theme mode on mount. */
|
|
111
|
+
color-scheme: light;
|
|
108
112
|
position: fixed; inset: 0; z-index: 2147483000;
|
|
109
113
|
background: rgba(2, 6, 23, 0.4);
|
|
110
114
|
-webkit-backdrop-filter: blur(8px);
|
|
@@ -277,6 +281,14 @@ var BASE_CSS = `
|
|
|
277
281
|
color: var(--us-muted); font-size: 11px; flex-shrink: 0;
|
|
278
282
|
font-family: var(--us-mono); letter-spacing: 0;
|
|
279
283
|
}
|
|
284
|
+
/* Failure reason \u2014 full-width, danger-colored, wraps. Collapses entirely when
|
|
285
|
+
empty so non-failed rows keep their original height. */
|
|
286
|
+
.us-file-error {
|
|
287
|
+
color: var(--us-danger); font-size: 12px; line-height: 1.35;
|
|
288
|
+
margin-top: 4px; letter-spacing: -0.006em;
|
|
289
|
+
overflow-wrap: anywhere;
|
|
290
|
+
}
|
|
291
|
+
.us-file-error:empty { display: none; }
|
|
280
292
|
|
|
281
293
|
.us-file-progress {
|
|
282
294
|
height: 3px; background: var(--us-border); border-radius: 999px; overflow: hidden;
|
|
@@ -516,6 +528,26 @@ var BASE_CSS = `
|
|
|
516
528
|
.us-tool svg { width: 13px; height: 13px; }
|
|
517
529
|
.us-tool-spacer { flex: 1; }
|
|
518
530
|
|
|
531
|
+
/* Crop dimension inputs \u2014 live W\xD7H readout, editable for free-form crops. */
|
|
532
|
+
.us-crop-dims {
|
|
533
|
+
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
|
534
|
+
padding: 10px 16px 0;
|
|
535
|
+
}
|
|
536
|
+
.us-crop-dims[hidden] { display: none; }
|
|
537
|
+
.us-crop-dims-label {
|
|
538
|
+
font-size: 11px; font-weight: 600; letter-spacing: 0.04em;
|
|
539
|
+
text-transform: uppercase; color: var(--us-muted);
|
|
540
|
+
}
|
|
541
|
+
.us-crop-dim-input {
|
|
542
|
+
width: 76px; appearance: none; text-align: center;
|
|
543
|
+
background: var(--us-overlay); border: 1px solid var(--us-border-strong);
|
|
544
|
+
border-radius: 8px; padding: 5px 8px; min-height: 30px;
|
|
545
|
+
font: inherit; font-size: 12px; color: var(--us-fg);
|
|
546
|
+
}
|
|
547
|
+
.us-crop-dim-input:focus { outline: none; border-color: var(--us-primary); }
|
|
548
|
+
.us-crop-dim-input:read-only { opacity: 0.55; cursor: default; }
|
|
549
|
+
.us-crop-dims-x, .us-crop-dims-unit { color: var(--us-muted); font-size: 12px; }
|
|
550
|
+
|
|
519
551
|
.us-editor-footer {
|
|
520
552
|
display: flex; gap: 8px; justify-content: flex-end;
|
|
521
553
|
padding: 12px 16px;
|
|
@@ -761,6 +793,16 @@ var ImageEditor = class {
|
|
|
761
793
|
this.displayCanvas.className = "us-editor-canvas";
|
|
762
794
|
this.canvasWrap.appendChild(this.displayCanvas);
|
|
763
795
|
this.root.appendChild(this.canvasWrap);
|
|
796
|
+
this.cropDims = el("div", "us-crop-dims");
|
|
797
|
+
this.cropDims.hidden = true;
|
|
798
|
+
this.widthInput = this.makeDimInput("Crop width in pixels", "w");
|
|
799
|
+
this.heightInput = this.makeDimInput("Crop height in pixels", "h");
|
|
800
|
+
this.cropDims.appendChild(el("span", "us-crop-dims-label", "Crop size"));
|
|
801
|
+
this.cropDims.appendChild(this.widthInput);
|
|
802
|
+
this.cropDims.appendChild(el("span", "us-crop-dims-x", "\xD7"));
|
|
803
|
+
this.cropDims.appendChild(this.heightInput);
|
|
804
|
+
this.cropDims.appendChild(el("span", "us-crop-dims-unit", "px"));
|
|
805
|
+
this.root.appendChild(this.cropDims);
|
|
764
806
|
const toolbar = el("div", "us-editor-toolbar");
|
|
765
807
|
this.cropTool = makeTool("Crop", ICON.crop);
|
|
766
808
|
this.cropTool.onclick = () => this.toggleCropMode();
|
|
@@ -854,21 +896,30 @@ var ImageEditor = class {
|
|
|
854
896
|
const dw = this.displayCanvas.width;
|
|
855
897
|
const dh = this.displayCanvas.height;
|
|
856
898
|
if (this.aspect) {
|
|
899
|
+
const a = this.aspect;
|
|
857
900
|
let w = dw;
|
|
858
|
-
let h = w /
|
|
901
|
+
let h = w / a;
|
|
859
902
|
if (h > dh) {
|
|
860
903
|
h = dh;
|
|
861
|
-
w = h *
|
|
904
|
+
w = h * a;
|
|
905
|
+
}
|
|
906
|
+
w = w * 0.9;
|
|
907
|
+
h = w / a;
|
|
908
|
+
if (w < MIN_CROP) {
|
|
909
|
+
w = MIN_CROP;
|
|
910
|
+
h = w / a;
|
|
911
|
+
}
|
|
912
|
+
if (h < MIN_CROP) {
|
|
913
|
+
h = MIN_CROP;
|
|
914
|
+
w = h * a;
|
|
862
915
|
}
|
|
863
|
-
w = Math.max(MIN_CROP, Math.round(w * 0.9));
|
|
864
|
-
h = Math.max(MIN_CROP, Math.round(w / this.aspect));
|
|
865
916
|
if (w > dw) {
|
|
866
917
|
w = dw;
|
|
867
|
-
h = w /
|
|
918
|
+
h = w / a;
|
|
868
919
|
}
|
|
869
920
|
if (h > dh) {
|
|
870
921
|
h = dh;
|
|
871
|
-
w = h *
|
|
922
|
+
w = h * a;
|
|
872
923
|
}
|
|
873
924
|
this.cropRect = {
|
|
874
925
|
x: Math.round((dw - w) / 2),
|
|
@@ -881,6 +932,12 @@ var ImageEditor = class {
|
|
|
881
932
|
this.cropRect = { x: inset, y: inset, w: dw - inset * 2, h: dh - inset * 2 };
|
|
882
933
|
}
|
|
883
934
|
this.renderCropBox();
|
|
935
|
+
const locked = this.aspect !== null;
|
|
936
|
+
this.widthInput.readOnly = locked;
|
|
937
|
+
this.heightInput.readOnly = locked;
|
|
938
|
+
this.cropDims.dataset.locked = locked ? "true" : "false";
|
|
939
|
+
this.cropDims.hidden = false;
|
|
940
|
+
this.syncCropDimsInputs();
|
|
884
941
|
}
|
|
885
942
|
exitCropMode() {
|
|
886
943
|
this.mode = "none";
|
|
@@ -891,6 +948,46 @@ var ImageEditor = class {
|
|
|
891
948
|
this.cropBox.remove();
|
|
892
949
|
this.cropBox = null;
|
|
893
950
|
}
|
|
951
|
+
this.cropDims.hidden = true;
|
|
952
|
+
}
|
|
953
|
+
// Build a numeric crop-dimension input. `axis` selects which side it edits.
|
|
954
|
+
makeDimInput(ariaLabel, axis) {
|
|
955
|
+
const input = document.createElement("input");
|
|
956
|
+
input.type = "number";
|
|
957
|
+
input.min = "1";
|
|
958
|
+
input.inputMode = "numeric";
|
|
959
|
+
input.className = "us-crop-dim-input";
|
|
960
|
+
input.setAttribute("aria-label", ariaLabel);
|
|
961
|
+
input.addEventListener("input", () => this.onDimInput(axis));
|
|
962
|
+
input.addEventListener("change", () => this.syncCropDimsInputs());
|
|
963
|
+
return input;
|
|
964
|
+
}
|
|
965
|
+
// Push the current crop frame size (in image/output pixels) into the inputs,
|
|
966
|
+
// skipping whichever the user is actively editing so we don't fight typing.
|
|
967
|
+
syncCropDimsInputs() {
|
|
968
|
+
if (!this.cropRect || !this.widthInput || !this.heightInput) return;
|
|
969
|
+
const s = this.displayScale || 1;
|
|
970
|
+
const wImg = Math.max(1, Math.round(this.cropRect.w / s));
|
|
971
|
+
const hImg = Math.max(1, Math.round(this.cropRect.h / s));
|
|
972
|
+
if (document.activeElement !== this.widthInput) this.widthInput.value = String(wImg);
|
|
973
|
+
if (document.activeElement !== this.heightInput) this.heightInput.value = String(hImg);
|
|
974
|
+
}
|
|
975
|
+
// Free-form only: resize the crop frame from a typed pixel dimension. Anchored
|
|
976
|
+
// at the frame's current top-left and clamped to the canvas + the min size.
|
|
977
|
+
onDimInput(axis) {
|
|
978
|
+
if (this.aspect !== null || !this.cropRect) return;
|
|
979
|
+
const s = this.displayScale || 1;
|
|
980
|
+
const input = axis === "w" ? this.widthInput : this.heightInput;
|
|
981
|
+
const valImg = parseInt(input.value, 10);
|
|
982
|
+
if (!Number.isFinite(valImg) || valImg <= 0) return;
|
|
983
|
+
if (axis === "w") {
|
|
984
|
+
const maxW = this.displayCanvas.width - this.cropRect.x;
|
|
985
|
+
this.cropRect = { ...this.cropRect, w: clamp(Math.round(valImg * s), MIN_CROP, maxW) };
|
|
986
|
+
} else {
|
|
987
|
+
const maxH = this.displayCanvas.height - this.cropRect.y;
|
|
988
|
+
this.cropRect = { ...this.cropRect, h: clamp(Math.round(valImg * s), MIN_CROP, maxH) };
|
|
989
|
+
}
|
|
990
|
+
this.updateCropBox();
|
|
894
991
|
}
|
|
895
992
|
renderCropBox() {
|
|
896
993
|
if (!this.cropRect) return;
|
|
@@ -921,6 +1018,7 @@ var ImageEditor = class {
|
|
|
921
1018
|
this.cropBox.style.top = `${top}px`;
|
|
922
1019
|
this.cropBox.style.width = `${this.cropRect.w}px`;
|
|
923
1020
|
this.cropBox.style.height = `${this.cropRect.h}px`;
|
|
1021
|
+
this.syncCropDimsInputs();
|
|
924
1022
|
}
|
|
925
1023
|
// ---- drag handlers (crop) ----------------------------------------------
|
|
926
1024
|
beginDrag(e, kind) {
|
|
@@ -989,6 +1087,7 @@ var ImageEditor = class {
|
|
|
989
1087
|
this.working = next;
|
|
990
1088
|
this.exitCropMode();
|
|
991
1089
|
this.markEdited(true);
|
|
1090
|
+
this.opts.onAspectReset?.();
|
|
992
1091
|
this.draw();
|
|
993
1092
|
}
|
|
994
1093
|
applyCircle() {
|
|
@@ -1012,11 +1111,13 @@ var ImageEditor = class {
|
|
|
1012
1111
|
this.hasAlpha = true;
|
|
1013
1112
|
this.exitCropMode();
|
|
1014
1113
|
this.markEdited(true);
|
|
1114
|
+
this.opts.onAspectReset?.();
|
|
1015
1115
|
this.draw();
|
|
1016
1116
|
}
|
|
1017
1117
|
async revert() {
|
|
1018
1118
|
await this.loadTrueOriginalIntoWorking();
|
|
1019
1119
|
this.exitCropMode();
|
|
1120
|
+
this.opts.onAspectReset?.();
|
|
1020
1121
|
this.draw();
|
|
1021
1122
|
}
|
|
1022
1123
|
markEdited(edited) {
|
|
@@ -1180,6 +1281,10 @@ var Picker = class {
|
|
|
1180
1281
|
this.editor = null;
|
|
1181
1282
|
this.abortCtrl = new AbortController();
|
|
1182
1283
|
this.uploadStarted = false;
|
|
1284
|
+
// Monotonic counter so a stale image-dimension decode (e.g. one started before
|
|
1285
|
+
// the user edited the file) can't clobber the row's state — only the latest
|
|
1286
|
+
// validation per item is allowed to mutate it.
|
|
1287
|
+
this.validateSeqCounter = 0;
|
|
1183
1288
|
this.resolved = false;
|
|
1184
1289
|
// Element focused before the modal opened, restored on close (a11y).
|
|
1185
1290
|
this.previouslyFocused = null;
|
|
@@ -1227,6 +1332,7 @@ var Picker = class {
|
|
|
1227
1332
|
Object.entries(themeToCssVars(this.opts.theme)).forEach(([k, v]) => {
|
|
1228
1333
|
root.style.setProperty(k, v);
|
|
1229
1334
|
});
|
|
1335
|
+
root.style.colorScheme = this.opts.theme?.mode === "dark" ? "dark" : "light";
|
|
1230
1336
|
root.addEventListener("click", (e) => {
|
|
1231
1337
|
if (e.target === root && !this.uploadStarted) this.cancel();
|
|
1232
1338
|
});
|
|
@@ -1637,6 +1743,9 @@ var Picker = class {
|
|
|
1637
1743
|
const bar = el2("div", "us-file-progress-bar");
|
|
1638
1744
|
progress.appendChild(bar);
|
|
1639
1745
|
main.appendChild(progress);
|
|
1746
|
+
const error = el2("div", "us-file-error");
|
|
1747
|
+
if (item.error) error.textContent = item.error;
|
|
1748
|
+
main.appendChild(error);
|
|
1640
1749
|
row.appendChild(main);
|
|
1641
1750
|
const isImage = item.file.type.startsWith("image/");
|
|
1642
1751
|
const editingEnabled = this.opts.imageEditing !== false;
|
|
@@ -1660,6 +1769,7 @@ var Picker = class {
|
|
|
1660
1769
|
item.$bar = bar;
|
|
1661
1770
|
item.$status = status;
|
|
1662
1771
|
item.$meta = meta;
|
|
1772
|
+
item.$error = error;
|
|
1663
1773
|
item.$thumb = thumb;
|
|
1664
1774
|
this.$list.appendChild(row);
|
|
1665
1775
|
this.updateSummary();
|
|
@@ -1711,6 +1821,11 @@ var Picker = class {
|
|
|
1711
1821
|
onCropApplied: () => {
|
|
1712
1822
|
didRatioCrop = true;
|
|
1713
1823
|
},
|
|
1824
|
+
// Rotating/circling/reverting after a crop changes the working aspect, so
|
|
1825
|
+
// the image is no longer guaranteed to match the locked ratio.
|
|
1826
|
+
onAspectReset: () => {
|
|
1827
|
+
didRatioCrop = false;
|
|
1828
|
+
},
|
|
1714
1829
|
onApply: (edited) => {
|
|
1715
1830
|
const wasEdited = edited !== item.originalFile;
|
|
1716
1831
|
this.replaceItemFile(item, edited, wasEdited);
|
|
@@ -1775,16 +1890,13 @@ var Picker = class {
|
|
|
1775
1890
|
case "uploading":
|
|
1776
1891
|
item.$meta.textContent = `${size} \xB7 ${Math.round(item.progress)}%`;
|
|
1777
1892
|
break;
|
|
1778
|
-
case "done":
|
|
1779
|
-
item.$meta.textContent = size;
|
|
1780
|
-
break;
|
|
1781
|
-
case "failed":
|
|
1782
|
-
item.$meta.textContent = item.error || "Failed";
|
|
1783
|
-
break;
|
|
1784
1893
|
default:
|
|
1785
1894
|
item.$meta.textContent = size;
|
|
1786
1895
|
}
|
|
1787
1896
|
}
|
|
1897
|
+
if (item.$error) {
|
|
1898
|
+
item.$error.textContent = state === "failed" ? item.error || "Upload failed" : "";
|
|
1899
|
+
}
|
|
1788
1900
|
this.updateSummary();
|
|
1789
1901
|
}
|
|
1790
1902
|
updateSummary() {
|
|
@@ -1844,25 +1956,27 @@ var Picker = class {
|
|
|
1844
1956
|
// Decode an image and fail the row if it's below the configured minimum.
|
|
1845
1957
|
// Non-decodable inputs are left queued — we can't prove they're too small.
|
|
1846
1958
|
async validateDimensions(item) {
|
|
1959
|
+
const seq = ++this.validateSeqCounter;
|
|
1960
|
+
item.validateSeq = seq;
|
|
1847
1961
|
const minW = this.opts.minImageWidth ?? 0;
|
|
1848
1962
|
const minH = this.opts.minImageHeight ?? 0;
|
|
1963
|
+
const file = item.file;
|
|
1849
1964
|
let dim = null;
|
|
1850
1965
|
try {
|
|
1851
|
-
dim = await readImageSize(
|
|
1966
|
+
dim = await readImageSize(file);
|
|
1852
1967
|
} catch {
|
|
1853
1968
|
dim = null;
|
|
1854
1969
|
}
|
|
1855
|
-
if (item.
|
|
1856
|
-
item.validating = false;
|
|
1857
|
-
return;
|
|
1858
|
-
}
|
|
1970
|
+
if (item.validateSeq !== seq) return;
|
|
1859
1971
|
item.validating = false;
|
|
1860
|
-
if (
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1972
|
+
if (item.state === "queued") {
|
|
1973
|
+
if (dim && dim.w > 0 && dim.h > 0 && (dim.w < minW || dim.h < minH)) {
|
|
1974
|
+
const need = `${minW || "?"}\xD7${minH || "?"}`;
|
|
1975
|
+
item.error = `Image too small \u2014 needs at least ${need}px (got ${dim.w}\xD7${dim.h})`;
|
|
1976
|
+
this.setItemState(item, "failed");
|
|
1977
|
+
} else {
|
|
1978
|
+
this.updateItemBadge(item);
|
|
1979
|
+
}
|
|
1866
1980
|
}
|
|
1867
1981
|
this.refreshConfirm();
|
|
1868
1982
|
this.maybeAutoUpload();
|