@unicitylabs/sphere-ui 0.1.23 → 0.1.25

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 CHANGED
@@ -334,8 +334,18 @@ interface MediaGalleryProps {
334
334
  onChange: (items: MediaItem[]) => void;
335
335
  uploadFn: MediaUploadFn;
336
336
  max?: number;
337
+ /**
338
+ * Defer mode: do NOT upload on select. Each chosen screenshot is kept locally
339
+ * (a File + blob preview) and the current File list is emitted via
340
+ * onFilesChange, so the parent can upload them AFTER the owner entity exists
341
+ * (e.g. project create, before there is an ownerId — mirrors MediaUploader's
342
+ * deferUpload for logo/banner). In this mode `items`/`onChange` are unused for
343
+ * screenshots; the parent owns the upload + persistence.
344
+ */
345
+ deferUpload?: boolean;
346
+ onFilesChange?: (files: File[]) => void;
337
347
  }
338
- declare function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max }: MediaGalleryProps): react.JSX.Element;
348
+ declare function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max, deferUpload, onFilesChange }: MediaGalleryProps): react.JSX.Element;
339
349
 
340
350
  interface MarketplaceProjectCardProps {
341
351
  name: string;
package/dist/index.js CHANGED
@@ -1411,6 +1411,17 @@ import { Fragment as Fragment4, jsx as jsx23, jsxs as jsxs18 } from "react/jsx-r
1411
1411
  function formatExtensions(mimes) {
1412
1412
  return mimes.map((m) => m.split("/")[1].toUpperCase()).join(", ");
1413
1413
  }
1414
+ async function readImageSize(file) {
1415
+ if (typeof createImageBitmap !== "function") return null;
1416
+ try {
1417
+ const bitmap = await createImageBitmap(file);
1418
+ const size = { width: bitmap.width, height: bitmap.height };
1419
+ bitmap.close?.();
1420
+ return size;
1421
+ } catch {
1422
+ return null;
1423
+ }
1424
+ }
1414
1425
  function MediaUploader({
1415
1426
  kind,
1416
1427
  ownerType,
@@ -1453,6 +1464,30 @@ function MediaUploader({
1453
1464
  });
1454
1465
  return;
1455
1466
  }
1467
+ if (file.type !== "image/svg+xml") {
1468
+ const size = await readImageSize(file);
1469
+ if (size) {
1470
+ const { width, height } = size;
1471
+ if (limit.maxWidth && width > limit.maxWidth || limit.maxHeight && height > limit.maxHeight) {
1472
+ setState({
1473
+ phase: "error",
1474
+ message: `Image too large (max ${limit.maxWidth}\xD7${limit.maxHeight}px \u2014 this is ${width}\xD7${height})`
1475
+ });
1476
+ return;
1477
+ }
1478
+ if (limit.aspectRatio) {
1479
+ const ratio = width / height;
1480
+ const tolerance = limit.aspectTolerance ?? 0;
1481
+ if (Math.abs(ratio - limit.aspectRatio) > limit.aspectRatio * tolerance) {
1482
+ setState({
1483
+ phase: "error",
1484
+ message: `Wrong aspect ratio (need ~${limit.aspectRatio}:1 \u2014 this is ${ratio.toFixed(2)}:1)`
1485
+ });
1486
+ return;
1487
+ }
1488
+ }
1489
+ }
1490
+ }
1456
1491
  if (deferUpload) {
1457
1492
  if (previewRef.current) URL.revokeObjectURL(previewRef.current);
1458
1493
  previewRef.current = URL.createObjectURL(file);
@@ -1656,7 +1691,7 @@ function MediaUploader({
1656
1691
  }
1657
1692
 
1658
1693
  // src/components/media/MediaGallery.tsx
1659
- import { useState as useState8 } from "react";
1694
+ import { useEffect as useEffect6, useState as useState8 } from "react";
1660
1695
  import {
1661
1696
  DndContext,
1662
1697
  closestCenter
@@ -1690,14 +1725,34 @@ function SortableTile({ item, onRemove }) {
1690
1725
  )
1691
1726
  ] });
1692
1727
  }
1693
- function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10 }) {
1728
+ function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10, deferUpload, onFilesChange }) {
1694
1729
  const [adding, setAdding] = useState8(false);
1730
+ const [pending, setPending] = useState8([]);
1731
+ useEffect6(
1732
+ () => () => {
1733
+ pending.forEach((p) => URL.revokeObjectURL(p.preview));
1734
+ },
1735
+ []
1736
+ // eslint-disable-line react-hooks/exhaustive-deps
1737
+ );
1738
+ const emitPending = (next) => {
1739
+ setPending(next);
1740
+ onFilesChange?.(next.map((p) => p.file));
1741
+ };
1695
1742
  function handleDragEnd(e) {
1696
1743
  if (!e.over || e.active.id === e.over.id) return;
1697
- const oldIndex = items.findIndex((i) => i.url === e.active.id);
1698
- const newIndex = items.findIndex((i) => i.url === e.over.id);
1699
- onChange(arrayMove(items, oldIndex, newIndex));
1744
+ if (deferUpload) {
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 {
1749
+ const oldIndex = items.findIndex((i) => i.url === e.active.id);
1750
+ const newIndex = items.findIndex((i) => i.url === e.over.id);
1751
+ onChange(arrayMove(items, oldIndex, newIndex));
1752
+ }
1700
1753
  }
1754
+ const count = deferUpload ? pending.length : items.length;
1755
+ const tileIds = deferUpload ? pending.map((p) => p.preview) : items.map((i) => i.url);
1701
1756
  return /* @__PURE__ */ jsxs19(
1702
1757
  "div",
1703
1758
  {
@@ -1707,13 +1762,23 @@ function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10
1707
1762
  children: [
1708
1763
  /* @__PURE__ */ jsxs19("div", { className: "text-sm text-neutral-700 dark:text-white/70", children: [
1709
1764
  "Screenshots (",
1710
- items.length,
1765
+ count,
1711
1766
  "/",
1712
1767
  max,
1713
1768
  ")"
1714
1769
  ] }),
1715
- /* @__PURE__ */ jsx24(DndContext, { collisionDetection: closestCenter, onDragEnd: handleDragEnd, children: /* @__PURE__ */ jsx24(SortableContext, { items: items.map((i) => i.url), strategy: horizontalListSortingStrategy, children: /* @__PURE__ */ jsxs19("div", { className: "flex flex-wrap gap-2", children: [
1716
- items.map((item, i) => /* @__PURE__ */ jsx24(
1770
+ /* @__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
+ deferUpload ? pending.map((p, i) => /* @__PURE__ */ jsx24(
1772
+ SortableTile,
1773
+ {
1774
+ item: { type: "screenshot", url: p.preview },
1775
+ onRemove: () => {
1776
+ URL.revokeObjectURL(p.preview);
1777
+ emitPending(pending.filter((_, j) => j !== i));
1778
+ }
1779
+ },
1780
+ p.preview
1781
+ )) : items.map((item, i) => /* @__PURE__ */ jsx24(
1717
1782
  SortableTile,
1718
1783
  {
1719
1784
  item,
@@ -1721,7 +1786,7 @@ function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10
1721
1786
  },
1722
1787
  item.url
1723
1788
  )),
1724
- items.length < max && !adding && /* @__PURE__ */ jsx24(
1789
+ count < max && !adding && /* @__PURE__ */ jsx24(
1725
1790
  "button",
1726
1791
  {
1727
1792
  type: "button",
@@ -1740,7 +1805,16 @@ function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10
1740
1805
  ownerType,
1741
1806
  ownerId,
1742
1807
  uploadFn,
1743
- onChange: (url) => {
1808
+ deferUpload,
1809
+ onFileSelected: deferUpload ? (file) => {
1810
+ if (file) {
1811
+ const preview = URL.createObjectURL(file);
1812
+ emitPending([...pending, { file, preview }]);
1813
+ }
1814
+ setAdding(false);
1815
+ } : void 0,
1816
+ onChange: deferUpload ? () => {
1817
+ } : (url) => {
1744
1818
  if (url) {
1745
1819
  const isDuplicate = items.some((i) => i.url === url);
1746
1820
  if (!isDuplicate) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unicitylabs/sphere-ui",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",