@masters-union/union-stack 0.3.11 → 0.3.13
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 +22 -2
- package/dist/cdn/loader.v1.global.js.map +1 -1
- package/dist/{chunk-Z5PZQVST.js → chunk-D3THUMKD.js} +3 -3
- package/dist/{chunk-Z5PZQVST.js.map → chunk-D3THUMKD.js.map} +1 -1
- package/dist/{chunk-NOAB2Q4B.cjs → chunk-FRXWGKUU.cjs} +3 -3
- package/dist/{chunk-NOAB2Q4B.cjs.map → chunk-FRXWGKUU.cjs.map} +1 -1
- package/dist/{chunk-SP4HHLZB.js → chunk-HGX7VKTJ.js} +3 -3
- package/dist/{chunk-SP4HHLZB.js.map → chunk-HGX7VKTJ.js.map} +1 -1
- package/dist/{chunk-NIO443GV.cjs → chunk-XEOQKI5K.cjs} +7 -7
- package/dist/{chunk-NIO443GV.cjs.map → chunk-XEOQKI5K.cjs.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 +138 -43
- package/dist/picker.cjs.map +1 -1
- package/dist/picker.js +138 -43
- 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
|
@@ -530,6 +530,26 @@ var BASE_CSS = `
|
|
|
530
530
|
.us-tool svg { width: 13px; height: 13px; }
|
|
531
531
|
.us-tool-spacer { flex: 1; }
|
|
532
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
|
+
|
|
533
553
|
.us-editor-footer {
|
|
534
554
|
display: flex; gap: 8px; justify-content: flex-end;
|
|
535
555
|
padding: 12px 16px;
|
|
@@ -642,8 +662,6 @@ var ImageEditor = class {
|
|
|
642
662
|
constructor(opts) {
|
|
643
663
|
this.opts = opts;
|
|
644
664
|
this.cropBox = null;
|
|
645
|
-
// What's currently rendered to the user, sized to fit MAX_DISPLAY.
|
|
646
|
-
this.displayScale = 1;
|
|
647
665
|
// Tracks whether the working canvas has transparency (post-circle), so
|
|
648
666
|
// export() can pick PNG vs JPEG correctly.
|
|
649
667
|
this.hasAlpha = false;
|
|
@@ -655,12 +673,13 @@ var ImageEditor = class {
|
|
|
655
673
|
this.dragStart = null;
|
|
656
674
|
this.onPointerMove = (e) => {
|
|
657
675
|
if (!this.dragKind || !this.dragStart || !this.cropRect) return;
|
|
658
|
-
const
|
|
659
|
-
const
|
|
676
|
+
const m = this.canvasMetrics();
|
|
677
|
+
const dx = (e.clientX - this.dragStart.px) / (m.scaleX || 1);
|
|
678
|
+
const dy = (e.clientY - this.dragStart.py) / (m.scaleY || 1);
|
|
660
679
|
const start = this.dragStart.rect;
|
|
661
|
-
const maxW = this.
|
|
662
|
-
const maxH = this.
|
|
663
|
-
const min =
|
|
680
|
+
const maxW = this.working.width;
|
|
681
|
+
const maxH = this.working.height;
|
|
682
|
+
const min = this.minCropImg();
|
|
664
683
|
if (this.aspect && this.dragKind !== "move") {
|
|
665
684
|
switch (this.dragKind) {
|
|
666
685
|
case "se":
|
|
@@ -775,6 +794,16 @@ var ImageEditor = class {
|
|
|
775
794
|
this.displayCanvas.className = "us-editor-canvas";
|
|
776
795
|
this.canvasWrap.appendChild(this.displayCanvas);
|
|
777
796
|
this.root.appendChild(this.canvasWrap);
|
|
797
|
+
this.cropDims = el("div", "us-crop-dims");
|
|
798
|
+
this.cropDims.hidden = true;
|
|
799
|
+
this.widthInput = this.makeDimInput("Crop width in pixels", "w");
|
|
800
|
+
this.heightInput = this.makeDimInput("Crop height in pixels", "h");
|
|
801
|
+
this.cropDims.appendChild(el("span", "us-crop-dims-label", "Crop size"));
|
|
802
|
+
this.cropDims.appendChild(this.widthInput);
|
|
803
|
+
this.cropDims.appendChild(el("span", "us-crop-dims-x", "\xD7"));
|
|
804
|
+
this.cropDims.appendChild(this.heightInput);
|
|
805
|
+
this.cropDims.appendChild(el("span", "us-crop-dims-unit", "px"));
|
|
806
|
+
this.root.appendChild(this.cropDims);
|
|
778
807
|
const toolbar = el("div", "us-editor-toolbar");
|
|
779
808
|
this.cropTool = makeTool("Crop", ICON.crop);
|
|
780
809
|
this.cropTool.onclick = () => this.toggleCropMode();
|
|
@@ -846,7 +875,6 @@ var ImageEditor = class {
|
|
|
846
875
|
const dh = Math.max(1, Math.round(wh * scale));
|
|
847
876
|
this.displayCanvas.width = dw;
|
|
848
877
|
this.displayCanvas.height = dh;
|
|
849
|
-
this.displayScale = scale;
|
|
850
878
|
const ctx = this.displayCanvas.getContext("2d");
|
|
851
879
|
if (!ctx) return;
|
|
852
880
|
ctx.clearRect(0, 0, dw, dh);
|
|
@@ -865,45 +893,52 @@ var ImageEditor = class {
|
|
|
865
893
|
this.mode = "crop";
|
|
866
894
|
this.cropTool.dataset.active = "true";
|
|
867
895
|
this.cropTool.innerHTML = `${ICON.check} <span>Apply crop</span>`;
|
|
868
|
-
const
|
|
869
|
-
const
|
|
896
|
+
const iw = this.working.width;
|
|
897
|
+
const ih = this.working.height;
|
|
870
898
|
if (this.aspect) {
|
|
871
899
|
const a = this.aspect;
|
|
872
|
-
|
|
900
|
+
const minImg = this.minCropImg();
|
|
901
|
+
let w = iw;
|
|
873
902
|
let h = w / a;
|
|
874
|
-
if (h >
|
|
875
|
-
h =
|
|
903
|
+
if (h > ih) {
|
|
904
|
+
h = ih;
|
|
876
905
|
w = h * a;
|
|
877
906
|
}
|
|
878
907
|
w = w * 0.9;
|
|
879
908
|
h = w / a;
|
|
880
|
-
if (w <
|
|
881
|
-
w =
|
|
909
|
+
if (w < minImg) {
|
|
910
|
+
w = minImg;
|
|
882
911
|
h = w / a;
|
|
883
912
|
}
|
|
884
|
-
if (h <
|
|
885
|
-
h =
|
|
913
|
+
if (h < minImg) {
|
|
914
|
+
h = minImg;
|
|
886
915
|
w = h * a;
|
|
887
916
|
}
|
|
888
|
-
if (w >
|
|
889
|
-
w =
|
|
917
|
+
if (w > iw) {
|
|
918
|
+
w = iw;
|
|
890
919
|
h = w / a;
|
|
891
920
|
}
|
|
892
|
-
if (h >
|
|
893
|
-
h =
|
|
921
|
+
if (h > ih) {
|
|
922
|
+
h = ih;
|
|
894
923
|
w = h * a;
|
|
895
924
|
}
|
|
896
925
|
this.cropRect = {
|
|
897
|
-
x: Math.round((
|
|
898
|
-
y: Math.round((
|
|
926
|
+
x: Math.round((iw - w) / 2),
|
|
927
|
+
y: Math.round((ih - h) / 2),
|
|
899
928
|
w,
|
|
900
929
|
h
|
|
901
930
|
};
|
|
902
931
|
} else {
|
|
903
|
-
const inset = Math.round(Math.min(
|
|
904
|
-
this.cropRect = { x: inset, y: inset, w:
|
|
932
|
+
const inset = Math.round(Math.min(iw, ih) * 0.1);
|
|
933
|
+
this.cropRect = { x: inset, y: inset, w: iw - inset * 2, h: ih - inset * 2 };
|
|
905
934
|
}
|
|
906
935
|
this.renderCropBox();
|
|
936
|
+
const locked = this.aspect !== null;
|
|
937
|
+
this.widthInput.readOnly = locked;
|
|
938
|
+
this.heightInput.readOnly = locked;
|
|
939
|
+
this.cropDims.dataset.locked = locked ? "true" : "false";
|
|
940
|
+
this.cropDims.hidden = false;
|
|
941
|
+
this.syncCropDimsInputs();
|
|
907
942
|
}
|
|
908
943
|
exitCropMode() {
|
|
909
944
|
this.mode = "none";
|
|
@@ -914,6 +949,46 @@ var ImageEditor = class {
|
|
|
914
949
|
this.cropBox.remove();
|
|
915
950
|
this.cropBox = null;
|
|
916
951
|
}
|
|
952
|
+
this.cropDims.hidden = true;
|
|
953
|
+
}
|
|
954
|
+
// Build a numeric crop-dimension input. `axis` selects which side it edits.
|
|
955
|
+
makeDimInput(ariaLabel, axis) {
|
|
956
|
+
const input = document.createElement("input");
|
|
957
|
+
input.type = "number";
|
|
958
|
+
input.min = "1";
|
|
959
|
+
input.inputMode = "numeric";
|
|
960
|
+
input.className = "us-crop-dim-input";
|
|
961
|
+
input.setAttribute("aria-label", ariaLabel);
|
|
962
|
+
input.addEventListener("input", () => this.onDimInput(axis));
|
|
963
|
+
input.addEventListener("change", () => this.syncCropDimsInputs());
|
|
964
|
+
return input;
|
|
965
|
+
}
|
|
966
|
+
// Push the current crop frame size (in image/output pixels) into the inputs,
|
|
967
|
+
// skipping whichever the user is actively editing so we don't fight typing.
|
|
968
|
+
// cropRect is already in image px, so this is a direct read.
|
|
969
|
+
syncCropDimsInputs() {
|
|
970
|
+
if (!this.cropRect || !this.widthInput || !this.heightInput) return;
|
|
971
|
+
const wImg = Math.max(1, Math.round(this.cropRect.w));
|
|
972
|
+
const hImg = Math.max(1, Math.round(this.cropRect.h));
|
|
973
|
+
if (document.activeElement !== this.widthInput) this.widthInput.value = String(wImg);
|
|
974
|
+
if (document.activeElement !== this.heightInput) this.heightInput.value = String(hImg);
|
|
975
|
+
}
|
|
976
|
+
// Free-form only: resize the crop frame from a typed output-pixel dimension.
|
|
977
|
+
// Anchored at the frame's current top-left and clamped to the image + min size.
|
|
978
|
+
onDimInput(axis) {
|
|
979
|
+
if (this.aspect !== null || !this.cropRect) return;
|
|
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
|
+
const min = this.minCropImg();
|
|
984
|
+
if (axis === "w") {
|
|
985
|
+
const maxW = this.working.width - this.cropRect.x;
|
|
986
|
+
this.cropRect = { ...this.cropRect, w: clamp(valImg, min, maxW) };
|
|
987
|
+
} else {
|
|
988
|
+
const maxH = this.working.height - this.cropRect.y;
|
|
989
|
+
this.cropRect = { ...this.cropRect, h: clamp(valImg, min, maxH) };
|
|
990
|
+
}
|
|
991
|
+
this.updateCropBox();
|
|
917
992
|
}
|
|
918
993
|
renderCropBox() {
|
|
919
994
|
if (!this.cropRect) return;
|
|
@@ -934,16 +1009,36 @@ var ImageEditor = class {
|
|
|
934
1009
|
this.cropBox = box;
|
|
935
1010
|
this.updateCropBox();
|
|
936
1011
|
}
|
|
1012
|
+
// The crop rect is stored in IMAGE (working-canvas / output) pixels — the same
|
|
1013
|
+
// space we ultimately crop in — so the on-screen selection always matches the
|
|
1014
|
+
// pixels we extract, regardless of how CSS scales the displayed canvas. These
|
|
1015
|
+
// metrics convert between image px and the canvas's live on-screen size.
|
|
1016
|
+
canvasMetrics() {
|
|
1017
|
+
const c = this.displayCanvas.getBoundingClientRect();
|
|
1018
|
+
const w = this.canvasWrap.getBoundingClientRect();
|
|
1019
|
+
return {
|
|
1020
|
+
scaleX: c.width / (this.working.width || 1),
|
|
1021
|
+
// CSS px per image px
|
|
1022
|
+
scaleY: c.height / (this.working.height || 1),
|
|
1023
|
+
offX: c.left - w.left,
|
|
1024
|
+
// canvas offset inside wrap
|
|
1025
|
+
offY: c.top - w.top
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
// Minimum crop size in IMAGE px, sized so the handles stay ~MIN_CROP px apart
|
|
1029
|
+
// on screen no matter how zoomed the canvas is.
|
|
1030
|
+
minCropImg() {
|
|
1031
|
+
const { scaleX } = this.canvasMetrics();
|
|
1032
|
+
return scaleX > 0 ? MIN_CROP / scaleX : MIN_CROP;
|
|
1033
|
+
}
|
|
937
1034
|
updateCropBox() {
|
|
938
1035
|
if (!this.cropBox || !this.cropRect) return;
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
this.cropBox.style.
|
|
944
|
-
this.
|
|
945
|
-
this.cropBox.style.width = `${this.cropRect.w}px`;
|
|
946
|
-
this.cropBox.style.height = `${this.cropRect.h}px`;
|
|
1036
|
+
const m = this.canvasMetrics();
|
|
1037
|
+
this.cropBox.style.left = `${m.offX + this.cropRect.x * m.scaleX}px`;
|
|
1038
|
+
this.cropBox.style.top = `${m.offY + this.cropRect.y * m.scaleY}px`;
|
|
1039
|
+
this.cropBox.style.width = `${this.cropRect.w * m.scaleX}px`;
|
|
1040
|
+
this.cropBox.style.height = `${this.cropRect.h * m.scaleY}px`;
|
|
1041
|
+
this.syncCropDimsInputs();
|
|
947
1042
|
}
|
|
948
1043
|
// ---- drag handlers (crop) ----------------------------------------------
|
|
949
1044
|
beginDrag(e, kind) {
|
|
@@ -960,12 +1055,13 @@ var ImageEditor = class {
|
|
|
960
1055
|
// the ratio exact; both are clamped so the rect stays on-canvas.
|
|
961
1056
|
lockedRectFromDrag(anchorX, anchorY, desiredW, signX, signY) {
|
|
962
1057
|
const a = this.aspect;
|
|
963
|
-
const maxW = this.
|
|
964
|
-
const maxH = this.
|
|
965
|
-
|
|
1058
|
+
const maxW = this.working.width;
|
|
1059
|
+
const maxH = this.working.height;
|
|
1060
|
+
const min = this.minCropImg();
|
|
1061
|
+
let w = Math.max(min, desiredW);
|
|
966
1062
|
let h = w / a;
|
|
967
|
-
if (h <
|
|
968
|
-
h =
|
|
1063
|
+
if (h < min) {
|
|
1064
|
+
h = min;
|
|
969
1065
|
w = h * a;
|
|
970
1066
|
}
|
|
971
1067
|
const roomX = signX > 0 ? maxW - anchorX : anchorX;
|
|
@@ -982,11 +1078,10 @@ var ImageEditor = class {
|
|
|
982
1078
|
this.exitCropMode();
|
|
983
1079
|
return;
|
|
984
1080
|
}
|
|
985
|
-
const
|
|
986
|
-
const
|
|
987
|
-
const
|
|
988
|
-
const
|
|
989
|
-
const sh = Math.round(this.cropRect.h / s);
|
|
1081
|
+
const sx = clamp(Math.round(this.cropRect.x), 0, this.working.width - 1);
|
|
1082
|
+
const sy = clamp(Math.round(this.cropRect.y), 0, this.working.height - 1);
|
|
1083
|
+
const sw = clamp(Math.round(this.cropRect.w), 1, this.working.width - sx);
|
|
1084
|
+
const sh = clamp(Math.round(this.cropRect.h), 1, this.working.height - sy);
|
|
990
1085
|
const next = document.createElement("canvas");
|
|
991
1086
|
next.width = sw;
|
|
992
1087
|
next.height = sh;
|