@masters-union/union-stack 0.3.10 → 0.3.11

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
@@ -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;
@@ -856,21 +868,30 @@ var ImageEditor = class {
856
868
  const dw = this.displayCanvas.width;
857
869
  const dh = this.displayCanvas.height;
858
870
  if (this.aspect) {
871
+ const a = this.aspect;
859
872
  let w = dw;
860
- let h = w / this.aspect;
873
+ let h = w / a;
861
874
  if (h > dh) {
862
875
  h = dh;
863
- w = h * this.aspect;
876
+ w = h * a;
877
+ }
878
+ w = w * 0.9;
879
+ h = w / a;
880
+ if (w < MIN_CROP) {
881
+ w = MIN_CROP;
882
+ h = w / a;
883
+ }
884
+ if (h < MIN_CROP) {
885
+ h = MIN_CROP;
886
+ w = h * a;
864
887
  }
865
- w = Math.max(MIN_CROP, Math.round(w * 0.9));
866
- h = Math.max(MIN_CROP, Math.round(w / this.aspect));
867
888
  if (w > dw) {
868
889
  w = dw;
869
- h = w / this.aspect;
890
+ h = w / a;
870
891
  }
871
892
  if (h > dh) {
872
893
  h = dh;
873
- w = h * this.aspect;
894
+ w = h * a;
874
895
  }
875
896
  this.cropRect = {
876
897
  x: Math.round((dw - w) / 2),
@@ -991,6 +1012,7 @@ var ImageEditor = class {
991
1012
  this.working = next;
992
1013
  this.exitCropMode();
993
1014
  this.markEdited(true);
1015
+ this.opts.onAspectReset?.();
994
1016
  this.draw();
995
1017
  }
996
1018
  applyCircle() {
@@ -1014,11 +1036,13 @@ var ImageEditor = class {
1014
1036
  this.hasAlpha = true;
1015
1037
  this.exitCropMode();
1016
1038
  this.markEdited(true);
1039
+ this.opts.onAspectReset?.();
1017
1040
  this.draw();
1018
1041
  }
1019
1042
  async revert() {
1020
1043
  await this.loadTrueOriginalIntoWorking();
1021
1044
  this.exitCropMode();
1045
+ this.opts.onAspectReset?.();
1022
1046
  this.draw();
1023
1047
  }
1024
1048
  markEdited(edited) {
@@ -1182,6 +1206,10 @@ var Picker = class {
1182
1206
  this.editor = null;
1183
1207
  this.abortCtrl = new AbortController();
1184
1208
  this.uploadStarted = false;
1209
+ // Monotonic counter so a stale image-dimension decode (e.g. one started before
1210
+ // the user edited the file) can't clobber the row's state — only the latest
1211
+ // validation per item is allowed to mutate it.
1212
+ this.validateSeqCounter = 0;
1185
1213
  this.resolved = false;
1186
1214
  // Element focused before the modal opened, restored on close (a11y).
1187
1215
  this.previouslyFocused = null;
@@ -1229,6 +1257,7 @@ var Picker = class {
1229
1257
  Object.entries(themeToCssVars(this.opts.theme)).forEach(([k, v]) => {
1230
1258
  root.style.setProperty(k, v);
1231
1259
  });
1260
+ root.style.colorScheme = this.opts.theme?.mode === "dark" ? "dark" : "light";
1232
1261
  root.addEventListener("click", (e) => {
1233
1262
  if (e.target === root && !this.uploadStarted) this.cancel();
1234
1263
  });
@@ -1639,6 +1668,9 @@ var Picker = class {
1639
1668
  const bar = el2("div", "us-file-progress-bar");
1640
1669
  progress.appendChild(bar);
1641
1670
  main.appendChild(progress);
1671
+ const error = el2("div", "us-file-error");
1672
+ if (item.error) error.textContent = item.error;
1673
+ main.appendChild(error);
1642
1674
  row.appendChild(main);
1643
1675
  const isImage = item.file.type.startsWith("image/");
1644
1676
  const editingEnabled = this.opts.imageEditing !== false;
@@ -1662,6 +1694,7 @@ var Picker = class {
1662
1694
  item.$bar = bar;
1663
1695
  item.$status = status;
1664
1696
  item.$meta = meta;
1697
+ item.$error = error;
1665
1698
  item.$thumb = thumb;
1666
1699
  this.$list.appendChild(row);
1667
1700
  this.updateSummary();
@@ -1713,6 +1746,11 @@ var Picker = class {
1713
1746
  onCropApplied: () => {
1714
1747
  didRatioCrop = true;
1715
1748
  },
1749
+ // Rotating/circling/reverting after a crop changes the working aspect, so
1750
+ // the image is no longer guaranteed to match the locked ratio.
1751
+ onAspectReset: () => {
1752
+ didRatioCrop = false;
1753
+ },
1716
1754
  onApply: (edited) => {
1717
1755
  const wasEdited = edited !== item.originalFile;
1718
1756
  this.replaceItemFile(item, edited, wasEdited);
@@ -1777,16 +1815,13 @@ var Picker = class {
1777
1815
  case "uploading":
1778
1816
  item.$meta.textContent = `${size} \xB7 ${Math.round(item.progress)}%`;
1779
1817
  break;
1780
- case "done":
1781
- item.$meta.textContent = size;
1782
- break;
1783
- case "failed":
1784
- item.$meta.textContent = item.error || "Failed";
1785
- break;
1786
1818
  default:
1787
1819
  item.$meta.textContent = size;
1788
1820
  }
1789
1821
  }
1822
+ if (item.$error) {
1823
+ item.$error.textContent = state === "failed" ? item.error || "Upload failed" : "";
1824
+ }
1790
1825
  this.updateSummary();
1791
1826
  }
1792
1827
  updateSummary() {
@@ -1846,25 +1881,27 @@ var Picker = class {
1846
1881
  // Decode an image and fail the row if it's below the configured minimum.
1847
1882
  // Non-decodable inputs are left queued — we can't prove they're too small.
1848
1883
  async validateDimensions(item) {
1884
+ const seq = ++this.validateSeqCounter;
1885
+ item.validateSeq = seq;
1849
1886
  const minW = this.opts.minImageWidth ?? 0;
1850
1887
  const minH = this.opts.minImageHeight ?? 0;
1888
+ const file = item.file;
1851
1889
  let dim = null;
1852
1890
  try {
1853
- dim = await readImageSize(item.file);
1891
+ dim = await readImageSize(file);
1854
1892
  } catch {
1855
1893
  dim = null;
1856
1894
  }
1857
- if (item.state !== "queued") {
1858
- item.validating = false;
1859
- return;
1860
- }
1895
+ if (item.validateSeq !== seq) return;
1861
1896
  item.validating = false;
1862
- if (dim && dim.w > 0 && dim.h > 0 && (dim.w < minW || dim.h < minH)) {
1863
- const need = `${minW || "?"}\xD7${minH || "?"}`;
1864
- item.error = `Image too small \u2014 needs at least ${need}px (got ${dim.w}\xD7${dim.h})`;
1865
- this.setItemState(item, "failed");
1866
- } else {
1867
- this.updateItemBadge(item);
1897
+ if (item.state === "queued") {
1898
+ if (dim && dim.w > 0 && dim.h > 0 && (dim.w < minW || dim.h < minH)) {
1899
+ const need = `${minW || "?"}\xD7${minH || "?"}`;
1900
+ item.error = `Image too small \u2014 needs at least ${need}px (got ${dim.w}\xD7${dim.h})`;
1901
+ this.setItemState(item, "failed");
1902
+ } else {
1903
+ this.updateItemBadge(item);
1904
+ }
1868
1905
  }
1869
1906
  this.refreshConfirm();
1870
1907
  this.maybeAutoUpload();