@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.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;
@@ -773,6 +793,16 @@ var ImageEditor = class {
773
793
  this.displayCanvas.className = "us-editor-canvas";
774
794
  this.canvasWrap.appendChild(this.displayCanvas);
775
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);
776
806
  const toolbar = el("div", "us-editor-toolbar");
777
807
  this.cropTool = makeTool("Crop", ICON.crop);
778
808
  this.cropTool.onclick = () => this.toggleCropMode();
@@ -902,6 +932,12 @@ var ImageEditor = class {
902
932
  this.cropRect = { x: inset, y: inset, w: dw - inset * 2, h: dh - inset * 2 };
903
933
  }
904
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();
905
941
  }
906
942
  exitCropMode() {
907
943
  this.mode = "none";
@@ -912,6 +948,46 @@ var ImageEditor = class {
912
948
  this.cropBox.remove();
913
949
  this.cropBox = null;
914
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();
915
991
  }
916
992
  renderCropBox() {
917
993
  if (!this.cropRect) return;
@@ -942,6 +1018,7 @@ var ImageEditor = class {
942
1018
  this.cropBox.style.top = `${top}px`;
943
1019
  this.cropBox.style.width = `${this.cropRect.w}px`;
944
1020
  this.cropBox.style.height = `${this.cropRect.h}px`;
1021
+ this.syncCropDimsInputs();
945
1022
  }
946
1023
  // ---- drag handlers (crop) ----------------------------------------------
947
1024
  beginDrag(e, kind) {