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