@unicitylabs/sphere-ui 0.1.26 → 0.1.28
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/index.d.ts +0 -3
- package/dist/index.js +101 -45
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -314,9 +314,6 @@ interface MediaUploaderProps {
|
|
|
314
314
|
* locally, render a blob-URL preview, and call onFileSelected(file). The
|
|
315
315
|
* consumer is responsible for uploading later (e.g. after creating the
|
|
316
316
|
* parent entity to get a real ownerId).
|
|
317
|
-
*
|
|
318
|
-
* In this mode the URL paste fallback is hidden — the parent entity does
|
|
319
|
-
* not yet exist, so an external URL can't be persisted to it anyway.
|
|
320
317
|
*/
|
|
321
318
|
deferUpload?: boolean;
|
|
322
319
|
onFileSelected?: (file: File | null) => void;
|
package/dist/index.js
CHANGED
|
@@ -1422,6 +1422,48 @@ async function readImageSize(file) {
|
|
|
1422
1422
|
return null;
|
|
1423
1423
|
}
|
|
1424
1424
|
}
|
|
1425
|
+
async function fitImage(file, limit) {
|
|
1426
|
+
if (file.type === "image/svg+xml") return file;
|
|
1427
|
+
if (typeof createImageBitmap !== "function" || typeof document === "undefined") return file;
|
|
1428
|
+
try {
|
|
1429
|
+
const bitmap = await createImageBitmap(file);
|
|
1430
|
+
const sw = bitmap.width;
|
|
1431
|
+
const sh = bitmap.height;
|
|
1432
|
+
let cropW = sw, cropH = sh, cropX = 0, cropY = 0;
|
|
1433
|
+
if (limit.aspectRatio) {
|
|
1434
|
+
if (sw / sh > limit.aspectRatio) {
|
|
1435
|
+
cropW = Math.round(sh * limit.aspectRatio);
|
|
1436
|
+
cropX = Math.round((sw - cropW) / 2);
|
|
1437
|
+
} else if (sw / sh < limit.aspectRatio) {
|
|
1438
|
+
cropH = Math.round(sw / limit.aspectRatio);
|
|
1439
|
+
cropY = Math.round((sh - cropH) / 2);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
const scale = Math.min(1, (limit.maxWidth ?? cropW) / cropW, (limit.maxHeight ?? cropH) / cropH);
|
|
1443
|
+
const outW = Math.max(1, Math.round(cropW * scale));
|
|
1444
|
+
const outH = Math.max(1, Math.round(cropH * scale));
|
|
1445
|
+
if (cropX === 0 && cropY === 0 && cropW === sw && cropH === sh && scale === 1) {
|
|
1446
|
+
bitmap.close?.();
|
|
1447
|
+
return file;
|
|
1448
|
+
}
|
|
1449
|
+
const canvas = document.createElement("canvas");
|
|
1450
|
+
canvas.width = outW;
|
|
1451
|
+
canvas.height = outH;
|
|
1452
|
+
const ctx = canvas.getContext("2d");
|
|
1453
|
+
if (!ctx) {
|
|
1454
|
+
bitmap.close?.();
|
|
1455
|
+
return file;
|
|
1456
|
+
}
|
|
1457
|
+
ctx.drawImage(bitmap, cropX, cropY, cropW, cropH, 0, 0, outW, outH);
|
|
1458
|
+
bitmap.close?.();
|
|
1459
|
+
const mime = file.type === "image/jpeg" || file.type === "image/webp" ? file.type : "image/png";
|
|
1460
|
+
const blob = await new Promise((resolve) => canvas.toBlob(resolve, mime, 0.92));
|
|
1461
|
+
if (!blob) return file;
|
|
1462
|
+
return new File([blob], file.name, { type: mime });
|
|
1463
|
+
} catch {
|
|
1464
|
+
return file;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1425
1467
|
function MediaUploader({
|
|
1426
1468
|
kind,
|
|
1427
1469
|
ownerType,
|
|
@@ -1436,6 +1478,7 @@ function MediaUploader({
|
|
|
1436
1478
|
const limit = MEDIA_LIMITS[kind];
|
|
1437
1479
|
const [state, setState] = useState7({ phase: "idle" });
|
|
1438
1480
|
const [urlInput, setUrlInput] = useState7(value && !value.startsWith("blob:") ? value : "");
|
|
1481
|
+
const [source, setSource] = useState7("upload");
|
|
1439
1482
|
const previewRef = useRef3(null);
|
|
1440
1483
|
const fileInputRef = useRef3(null);
|
|
1441
1484
|
const isPlaceholder = value?.includes("placehold.co") ?? false;
|
|
@@ -1464,8 +1507,10 @@ function MediaUploader({
|
|
|
1464
1507
|
});
|
|
1465
1508
|
return;
|
|
1466
1509
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1510
|
+
setUrlInput("");
|
|
1511
|
+
const fitted = await fitImage(file, limit);
|
|
1512
|
+
if (fitted.type !== "image/svg+xml") {
|
|
1513
|
+
const size = await readImageSize(fitted);
|
|
1469
1514
|
if (size) {
|
|
1470
1515
|
const { width, height } = size;
|
|
1471
1516
|
if (limit.maxWidth && width > limit.maxWidth || limit.maxHeight && height > limit.maxHeight) {
|
|
@@ -1490,18 +1535,18 @@ function MediaUploader({
|
|
|
1490
1535
|
}
|
|
1491
1536
|
if (deferUpload) {
|
|
1492
1537
|
if (previewRef.current) URL.revokeObjectURL(previewRef.current);
|
|
1493
|
-
previewRef.current = URL.createObjectURL(
|
|
1494
|
-
setState({ phase: "pending", file });
|
|
1495
|
-
onFileSelected?.(
|
|
1538
|
+
previewRef.current = URL.createObjectURL(fitted);
|
|
1539
|
+
setState({ phase: "pending", file: fitted });
|
|
1540
|
+
onFileSelected?.(fitted);
|
|
1496
1541
|
onChange(null);
|
|
1497
1542
|
return;
|
|
1498
1543
|
}
|
|
1499
1544
|
const abort = new AbortController();
|
|
1500
|
-
setState({ phase: "uploading", file, progress: 0, abort });
|
|
1545
|
+
setState({ phase: "uploading", file: fitted, progress: 0, abort });
|
|
1501
1546
|
if (previewRef.current) URL.revokeObjectURL(previewRef.current);
|
|
1502
|
-
previewRef.current = URL.createObjectURL(
|
|
1547
|
+
previewRef.current = URL.createObjectURL(fitted);
|
|
1503
1548
|
try {
|
|
1504
|
-
const result = await uploadFn(
|
|
1549
|
+
const result = await uploadFn(fitted, {
|
|
1505
1550
|
kind,
|
|
1506
1551
|
ownerType,
|
|
1507
1552
|
ownerId,
|
|
@@ -1566,9 +1611,26 @@ function MediaUploader({
|
|
|
1566
1611
|
setUrlInput("");
|
|
1567
1612
|
if (deferUpload) onFileSelected?.(null);
|
|
1568
1613
|
};
|
|
1614
|
+
const commitUrl = () => {
|
|
1615
|
+
const url = urlInput.trim();
|
|
1616
|
+
onChange(url || null);
|
|
1617
|
+
if (url) {
|
|
1618
|
+
if (previewRef.current) {
|
|
1619
|
+
URL.revokeObjectURL(previewRef.current);
|
|
1620
|
+
previewRef.current = null;
|
|
1621
|
+
}
|
|
1622
|
+
setState({ phase: "idle" });
|
|
1623
|
+
if (deferUpload) onFileSelected?.(null);
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
const tabClass = (active) => `px-3 py-1 rounded-md transition-colors ${active ? "bg-white dark:bg-white/10 text-neutral-900 dark:text-white shadow-sm" : "text-neutral-500 dark:text-white/45 hover:text-neutral-700 dark:hover:text-white/70"}`;
|
|
1569
1627
|
return /* @__PURE__ */ jsxs18("div", { className: "space-y-2", children: [
|
|
1570
1628
|
label && /* @__PURE__ */ jsx23("div", { className: "text-sm text-neutral-700 dark:text-white/70", children: label }),
|
|
1571
|
-
/* @__PURE__ */ jsxs18(
|
|
1629
|
+
/* @__PURE__ */ jsxs18("div", { className: "inline-flex gap-0.5 rounded-lg p-0.5 bg-neutral-100 dark:bg-white/5 text-xs w-fit", children: [
|
|
1630
|
+
/* @__PURE__ */ jsx23("button", { type: "button", onClick: () => setSource("upload"), className: tabClass(source === "upload"), children: "Upload" }),
|
|
1631
|
+
/* @__PURE__ */ jsx23("button", { type: "button", onClick: () => setSource("url"), className: tabClass(source === "url"), children: "URL" })
|
|
1632
|
+
] }),
|
|
1633
|
+
source === "upload" && /* @__PURE__ */ jsxs18(
|
|
1572
1634
|
"div",
|
|
1573
1635
|
{
|
|
1574
1636
|
...getRootProps(),
|
|
@@ -1674,19 +1736,16 @@ function MediaUploader({
|
|
|
1674
1736
|
]
|
|
1675
1737
|
}
|
|
1676
1738
|
),
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
}
|
|
1688
|
-
)
|
|
1689
|
-
] })
|
|
1739
|
+
source === "url" && /* @__PURE__ */ jsx23(
|
|
1740
|
+
Input,
|
|
1741
|
+
{
|
|
1742
|
+
type: "url",
|
|
1743
|
+
placeholder: "https://...",
|
|
1744
|
+
value: urlInput,
|
|
1745
|
+
onChange: (e) => setUrlInput(e.target.value),
|
|
1746
|
+
onBlur: commitUrl
|
|
1747
|
+
}
|
|
1748
|
+
)
|
|
1690
1749
|
] });
|
|
1691
1750
|
}
|
|
1692
1751
|
|
|
@@ -1741,18 +1800,18 @@ function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10,
|
|
|
1741
1800
|
};
|
|
1742
1801
|
function handleDragEnd(e) {
|
|
1743
1802
|
if (!e.over || e.active.id === e.over.id) return;
|
|
1744
|
-
if (
|
|
1745
|
-
const oldIndex = pending.findIndex((p) => p.preview === e.active.id);
|
|
1746
|
-
const newIndex = pending.findIndex((p) => p.preview === e.over.id);
|
|
1747
|
-
emitPending(arrayMove(pending, oldIndex, newIndex));
|
|
1748
|
-
} else {
|
|
1803
|
+
if (items.some((i) => i.url === e.active.id)) {
|
|
1749
1804
|
const oldIndex = items.findIndex((i) => i.url === e.active.id);
|
|
1750
1805
|
const newIndex = items.findIndex((i) => i.url === e.over.id);
|
|
1751
|
-
onChange(arrayMove(items, oldIndex, newIndex));
|
|
1806
|
+
if (newIndex >= 0) onChange(arrayMove(items, oldIndex, newIndex));
|
|
1807
|
+
} else if (deferUpload) {
|
|
1808
|
+
const oldIndex = pending.findIndex((p) => p.preview === e.active.id);
|
|
1809
|
+
const newIndex = pending.findIndex((p) => p.preview === e.over.id);
|
|
1810
|
+
if (newIndex >= 0) emitPending(arrayMove(pending, oldIndex, newIndex));
|
|
1752
1811
|
}
|
|
1753
1812
|
}
|
|
1754
|
-
const count = deferUpload ? pending.length :
|
|
1755
|
-
const tileIds =
|
|
1813
|
+
const count = items.length + (deferUpload ? pending.length : 0);
|
|
1814
|
+
const tileIds = [...items.map((i) => i.url), ...deferUpload ? pending.map((p) => p.preview) : []];
|
|
1756
1815
|
return /* @__PURE__ */ jsxs19(
|
|
1757
1816
|
"div",
|
|
1758
1817
|
{
|
|
@@ -1768,7 +1827,15 @@ function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10,
|
|
|
1768
1827
|
")"
|
|
1769
1828
|
] }),
|
|
1770
1829
|
/* @__PURE__ */ jsx24(DndContext, { collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsx24(SortableContext, { items: tileIds, strategy: horizontalListSortingStrategy, children: /* @__PURE__ */ jsxs19("div", { className: "flex flex-wrap gap-2", children: [
|
|
1771
|
-
|
|
1830
|
+
items.map((item, i) => /* @__PURE__ */ jsx24(
|
|
1831
|
+
SortableTile,
|
|
1832
|
+
{
|
|
1833
|
+
item,
|
|
1834
|
+
onRemove: () => onChange(items.filter((_, j) => j !== i))
|
|
1835
|
+
},
|
|
1836
|
+
item.url
|
|
1837
|
+
)),
|
|
1838
|
+
deferUpload && pending.map((p, i) => /* @__PURE__ */ jsx24(
|
|
1772
1839
|
SortableTile,
|
|
1773
1840
|
{
|
|
1774
1841
|
item: { type: "screenshot", url: p.preview },
|
|
@@ -1778,13 +1845,6 @@ function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10,
|
|
|
1778
1845
|
}
|
|
1779
1846
|
},
|
|
1780
1847
|
p.preview
|
|
1781
|
-
)) : items.map((item, i) => /* @__PURE__ */ jsx24(
|
|
1782
|
-
SortableTile,
|
|
1783
|
-
{
|
|
1784
|
-
item,
|
|
1785
|
-
onRemove: () => onChange(items.filter((_, j) => j !== i))
|
|
1786
|
-
},
|
|
1787
|
-
item.url
|
|
1788
1848
|
)),
|
|
1789
1849
|
count < max && !adding && /* @__PURE__ */ jsx24(
|
|
1790
1850
|
"button",
|
|
@@ -1813,13 +1873,9 @@ function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10,
|
|
|
1813
1873
|
}
|
|
1814
1874
|
setAdding(false);
|
|
1815
1875
|
} : void 0,
|
|
1816
|
-
onChange:
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
const isDuplicate = items.some((i) => i.url === url);
|
|
1820
|
-
if (!isDuplicate) {
|
|
1821
|
-
onChange([...items, { type: "screenshot", url }]);
|
|
1822
|
-
}
|
|
1876
|
+
onChange: (url) => {
|
|
1877
|
+
if (url && !items.some((i) => i.url === url)) {
|
|
1878
|
+
onChange([...items, { type: "screenshot", url }]);
|
|
1823
1879
|
}
|
|
1824
1880
|
setAdding(false);
|
|
1825
1881
|
},
|