@ai-matrx/capture 0.5.2 → 0.5.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog — @ai-matrx/capture
2
2
 
3
+ ## 0.5.3 — 2026-09-01
4
+
5
+ - `ImageEditSheet` now keeps Save, crop presets, rotate, and flip unavailable while a newly baked bitmap is decoding, then enables them from the image's real `load` event. A transformed image can no longer hit the zero-dimension silent-return race and leave the editor stuck with no persistence or error.
6
+ - Rotate and flip now bake the active crop selection into their output instead of discarding it. Exact fail-first coverage proves Square crop → Rotate → newly loaded bitmap → Save emits one edited blob.
7
+
3
8
  ## 0.5.2 — 2026-09-01
4
9
 
5
10
  - `CameraCapture` no longer wraps the injected recents thumbnail in its Open-library button. Persisted-media error fallbacks may own remedy buttons (for example, Copy URL); those controls now remain valid siblings, act without also opening the library, and the non-interactive thumbnail area still opens the library.
package/dist/react.cjs CHANGED
@@ -1230,6 +1230,8 @@ function ImageEditSheet({
1230
1230
  const [workingSrc, setWorkingSrc] = (0, import_react4.useState)(null);
1231
1231
  const [aspect, setAspect] = (0, import_react4.useState)("free");
1232
1232
  const [crop, setCrop] = (0, import_react4.useState)(FULL_CROP);
1233
+ const [imageReady, setImageReady] = (0, import_react4.useState)(false);
1234
+ const [transforming, setTransforming] = (0, import_react4.useState)(false);
1233
1235
  const [saving, setSaving] = (0, import_react4.useState)(false);
1234
1236
  const [editError, setEditError] = (0, import_react4.useState)(null);
1235
1237
  const stageRef = (0, import_react4.useRef)(null);
@@ -1245,6 +1247,8 @@ function ImageEditSheet({
1245
1247
  setWorkingSrc(null);
1246
1248
  setAspect("free");
1247
1249
  setCrop(FULL_CROP);
1250
+ setImageReady(false);
1251
+ setTransforming(false);
1248
1252
  setSaving(false);
1249
1253
  setEditError(null);
1250
1254
  }
@@ -1253,21 +1257,53 @@ function ImageEditSheet({
1253
1257
  const bake = (0, import_react4.useCallback)(
1254
1258
  (op) => {
1255
1259
  const img = imgRef.current;
1256
- if (!img || img.naturalWidth === 0 || saving) return;
1260
+ if (!img || !imageReady || img.naturalWidth === 0 || saving || transforming) {
1261
+ setEditError("The image is still loading \u2014 wait a moment and try again.");
1262
+ return;
1263
+ }
1264
+ setTransforming(true);
1265
+ setEditError(null);
1257
1266
  try {
1267
+ const sourceX = Math.round(crop.x * img.naturalWidth);
1268
+ const sourceY = Math.round(crop.y * img.naturalHeight);
1269
+ const sourceWidth = Math.max(
1270
+ 1,
1271
+ Math.min(
1272
+ img.naturalWidth - sourceX,
1273
+ Math.round(crop.w * img.naturalWidth)
1274
+ )
1275
+ );
1276
+ const sourceHeight = Math.max(
1277
+ 1,
1278
+ Math.min(
1279
+ img.naturalHeight - sourceY,
1280
+ Math.round(crop.h * img.naturalHeight)
1281
+ )
1282
+ );
1258
1283
  const swap = op === "rotate-left";
1259
1284
  const canvas = document.createElement("canvas");
1260
- canvas.width = swap ? img.naturalHeight : img.naturalWidth;
1261
- canvas.height = swap ? img.naturalWidth : img.naturalHeight;
1285
+ canvas.width = swap ? sourceHeight : sourceWidth;
1286
+ canvas.height = swap ? sourceWidth : sourceHeight;
1262
1287
  const ctx = canvas.getContext("2d");
1263
1288
  if (!ctx) throw new Error("no 2d context");
1264
1289
  ctx.translate(canvas.width / 2, canvas.height / 2);
1265
1290
  if (swap) ctx.rotate(-Math.PI / 2);
1266
1291
  else ctx.scale(-1, 1);
1267
- ctx.drawImage(img, -img.naturalWidth / 2, -img.naturalHeight / 2);
1292
+ ctx.drawImage(
1293
+ img,
1294
+ sourceX,
1295
+ sourceY,
1296
+ sourceWidth,
1297
+ sourceHeight,
1298
+ -sourceWidth / 2,
1299
+ -sourceHeight / 2,
1300
+ sourceWidth,
1301
+ sourceHeight
1302
+ );
1268
1303
  canvas.toBlob(
1269
1304
  (blob) => {
1270
1305
  if (!blob) {
1306
+ setTransforming(false);
1271
1307
  setEditError(
1272
1308
  "This image is too large to process on this device."
1273
1309
  );
@@ -1276,26 +1312,32 @@ function ImageEditSheet({
1276
1312
  revokeOwned();
1277
1313
  const url = URL.createObjectURL(blob);
1278
1314
  ownedUrlsRef.current.push(url);
1315
+ setImageReady(false);
1279
1316
  setWorkingSrc(url);
1280
1317
  setCrop(FULL_CROP);
1281
1318
  setAspect("free");
1319
+ setTransforming(false);
1282
1320
  setEditError(null);
1283
1321
  },
1284
1322
  "image/jpeg",
1285
1323
  0.95
1286
1324
  );
1287
1325
  } catch {
1326
+ setTransforming(false);
1288
1327
  setEditError("This image could not be edited on this device.");
1289
1328
  }
1290
1329
  },
1291
- [saving, revokeOwned]
1330
+ [crop, imageReady, saving, transforming, revokeOwned]
1292
1331
  );
1293
1332
  const applyAspect = (0, import_react4.useCallback)((preset) => {
1333
+ const img = imgRef.current;
1334
+ if (!img || img.naturalWidth === 0) {
1335
+ setEditError("The image is still loading \u2014 wait a moment and try again.");
1336
+ return;
1337
+ }
1294
1338
  setAspect(preset);
1295
1339
  const ratio = ASPECTS.find((a) => a.id === preset)?.ratio ?? null;
1296
1340
  if (ratio === null) return;
1297
- const img = imgRef.current;
1298
- if (!img || img.naturalWidth === 0) return;
1299
1341
  const imageRatio = img.naturalWidth / img.naturalHeight;
1300
1342
  let w = 1;
1301
1343
  let h = 1;
@@ -1360,7 +1402,10 @@ function ImageEditSheet({
1360
1402
  }, []);
1361
1403
  const save = (0, import_react4.useCallback)(() => {
1362
1404
  const img = imgRef.current;
1363
- if (!img || img.naturalWidth === 0) return;
1405
+ if (!img || !imageReady || img.naturalWidth === 0) {
1406
+ setEditError("The image is still loading \u2014 wait a moment and try again.");
1407
+ return;
1408
+ }
1364
1409
  setSaving(true);
1365
1410
  setEditError(null);
1366
1411
  try {
@@ -1390,7 +1435,7 @@ function ImageEditSheet({
1390
1435
  setSaving(false);
1391
1436
  setEditError("Saving failed on this device \u2014 try again.");
1392
1437
  }
1393
- }, [crop, onSave, onClose]);
1438
+ }, [crop, imageReady, onSave, onClose]);
1394
1439
  if (!open || !src) return null;
1395
1440
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "absolute inset-0 z-50 flex flex-col bg-black", children: [
1396
1441
  /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
@@ -1418,12 +1463,13 @@ function ImageEditSheet({
1418
1463
  {
1419
1464
  type: "button",
1420
1465
  onClick: save,
1421
- disabled: saving,
1466
+ disabled: saving || transforming || !imageReady,
1422
1467
  "aria-label": "Save edited image",
1468
+ "aria-busy": saving || transforming || !imageReady,
1423
1469
  className: "flex h-11 items-center gap-1.5 rounded-full px-3 text-[15px] font-semibold text-[#FFCC00] disabled:opacity-50",
1424
1470
  children: [
1425
1471
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(CheckIcon, { className: "h-5 w-5" }),
1426
- "Save"
1472
+ saving ? "Saving\u2026" : transforming || !imageReady ? "Loading\u2026" : "Save"
1427
1473
  ]
1428
1474
  }
1429
1475
  )
@@ -1446,6 +1492,12 @@ function ImageEditSheet({
1446
1492
  src: workingSrc ?? src,
1447
1493
  alt: "Image being edited",
1448
1494
  draggable: false,
1495
+ onLoad: () => setImageReady(true),
1496
+ onError: () => {
1497
+ setImageReady(false);
1498
+ setTransforming(false);
1499
+ setEditError("This image could not be loaded on this device.");
1500
+ },
1449
1501
  className: "max-h-[62dvh] max-w-full select-none object-contain"
1450
1502
  }
1451
1503
  ),
@@ -1489,6 +1541,7 @@ function ImageEditSheet({
1489
1541
  {
1490
1542
  type: "button",
1491
1543
  onClick: () => applyAspect(a.id),
1544
+ disabled: saving || transforming || !imageReady,
1492
1545
  "aria-pressed": aspect === a.id,
1493
1546
  className: cn(
1494
1547
  "touch-manipulation rounded-full px-3.5 py-1.5 text-[12px] font-semibold uppercase tracking-wide transition-colors",
@@ -1504,6 +1557,7 @@ function ImageEditSheet({
1504
1557
  {
1505
1558
  type: "button",
1506
1559
  onClick: () => bake("rotate-left"),
1560
+ disabled: saving || transforming || !imageReady,
1507
1561
  "aria-label": "Rotate left",
1508
1562
  className: "flex h-12 w-12 touch-manipulation items-center justify-center rounded-full bg-white/10 text-white",
1509
1563
  children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(RotateCcwIcon, { className: "h-5 w-5" })
@@ -1514,6 +1568,7 @@ function ImageEditSheet({
1514
1568
  {
1515
1569
  type: "button",
1516
1570
  onClick: () => bake("flip"),
1571
+ disabled: saving || transforming || !imageReady,
1517
1572
  "aria-label": "Flip horizontally",
1518
1573
  className: "flex h-12 w-12 touch-manipulation items-center justify-center rounded-full bg-white/10 text-white",
1519
1574
  children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(FlipHorizontal2Icon, { className: "h-5 w-5" })