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