@masters-union/union-stack 0.3.11 → 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/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;
@@ -775,6 +795,16 @@ var ImageEditor = class {
775
795
  this.displayCanvas.className = "us-editor-canvas";
776
796
  this.canvasWrap.appendChild(this.displayCanvas);
777
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);
778
808
  const toolbar = el("div", "us-editor-toolbar");
779
809
  this.cropTool = makeTool("Crop", ICON.crop);
780
810
  this.cropTool.onclick = () => this.toggleCropMode();
@@ -904,6 +934,12 @@ var ImageEditor = class {
904
934
  this.cropRect = { x: inset, y: inset, w: dw - inset * 2, h: dh - inset * 2 };
905
935
  }
906
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();
907
943
  }
908
944
  exitCropMode() {
909
945
  this.mode = "none";
@@ -914,6 +950,46 @@ var ImageEditor = class {
914
950
  this.cropBox.remove();
915
951
  this.cropBox = null;
916
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();
917
993
  }
918
994
  renderCropBox() {
919
995
  if (!this.cropRect) return;
@@ -944,6 +1020,7 @@ var ImageEditor = class {
944
1020
  this.cropBox.style.top = `${top}px`;
945
1021
  this.cropBox.style.width = `${this.cropRect.w}px`;
946
1022
  this.cropBox.style.height = `${this.cropRect.h}px`;
1023
+ this.syncCropDimsInputs();
947
1024
  }
948
1025
  // ---- drag handlers (crop) ----------------------------------------------
949
1026
  beginDrag(e, kind) {