@unicitylabs/sphere-ui 0.1.18 → 0.1.19
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 +16 -3
- package/dist/index.js +121 -50
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -310,8 +310,19 @@ interface MediaUploaderProps {
|
|
|
310
310
|
onChange: (url: string | null) => void;
|
|
311
311
|
uploadFn: MediaUploadFn;
|
|
312
312
|
label?: string;
|
|
313
|
-
|
|
314
|
-
|
|
313
|
+
/**
|
|
314
|
+
* When true, do NOT call uploadFn on file selection. Instead store the file
|
|
315
|
+
* locally, render a blob-URL preview, and call onFileSelected(file). The
|
|
316
|
+
* consumer is responsible for uploading later (e.g. after creating the
|
|
317
|
+
* parent entity to get a real ownerId).
|
|
318
|
+
*
|
|
319
|
+
* In this mode the URL paste fallback is hidden — the parent entity does
|
|
320
|
+
* not yet exist, so an external URL can't be persisted to it anyway.
|
|
321
|
+
*/
|
|
322
|
+
deferUpload?: boolean;
|
|
323
|
+
onFileSelected?: (file: File | null) => void;
|
|
324
|
+
}
|
|
325
|
+
declare function MediaUploader({ kind, ownerType, ownerId, value, onChange, uploadFn, label, deferUpload, onFileSelected, }: MediaUploaderProps): react_jsx_runtime.JSX.Element;
|
|
315
326
|
|
|
316
327
|
interface MediaItem {
|
|
317
328
|
type: 'screenshot' | 'video';
|
|
@@ -430,6 +441,8 @@ interface ProjectPagePreviewProps {
|
|
|
430
441
|
quests?: QuestPreviewSummary[];
|
|
431
442
|
/** Sample achievements */
|
|
432
443
|
achievements?: AchievementPreviewSummary[];
|
|
444
|
+
/** Project tags — first 3 are rendered as chips next to the category badge */
|
|
445
|
+
tags?: string[];
|
|
433
446
|
}
|
|
434
447
|
/**
|
|
435
448
|
* ProjectPagePreview — stateless 1:1 visual copy of sphere wallet's `/apps/:slug` page.
|
|
@@ -437,7 +450,7 @@ interface ProjectPagePreviewProps {
|
|
|
437
450
|
* Used by dev-portal & backoffice as a live preview while editing a project.
|
|
438
451
|
* No router / hooks / data fetching — every value comes via props.
|
|
439
452
|
*/
|
|
440
|
-
declare function ProjectPagePreview({ name, tagline, description, logoUrl, bannerUrl, accentColor, category, websiteUrl, discordUrl, twitterUrl, media, users, activeQuests, positivePercent, ratingCount, quests, achievements, }: ProjectPagePreviewProps): react_jsx_runtime.JSX.Element;
|
|
453
|
+
declare function ProjectPagePreview({ name, tagline, description, logoUrl, bannerUrl, accentColor, category, websiteUrl, discordUrl, twitterUrl, media, users, activeQuests, positivePercent, ratingCount, quests, achievements, tags, }: ProjectPagePreviewProps): react_jsx_runtime.JSX.Element;
|
|
441
454
|
|
|
442
455
|
declare const MEDIA_LIMITS: Record<MediaKind, MediaLimit>;
|
|
443
456
|
declare function isMimeAllowed(kind: MediaKind, mime: string): mime is MediaMime;
|
package/dist/index.js
CHANGED
|
@@ -1418,12 +1418,16 @@ function MediaUploader({
|
|
|
1418
1418
|
value,
|
|
1419
1419
|
onChange,
|
|
1420
1420
|
uploadFn,
|
|
1421
|
-
label
|
|
1421
|
+
label,
|
|
1422
|
+
deferUpload,
|
|
1423
|
+
onFileSelected
|
|
1422
1424
|
}) {
|
|
1423
1425
|
const limit = MEDIA_LIMITS[kind];
|
|
1424
1426
|
const [state, setState] = useState7({ phase: "idle" });
|
|
1425
1427
|
const [urlInput, setUrlInput] = useState7(value && !value.startsWith("blob:") ? value : "");
|
|
1426
1428
|
const previewRef = useRef3(null);
|
|
1429
|
+
const fileInputRef = useRef3(null);
|
|
1430
|
+
const isPlaceholder = value?.includes("placehold.co") ?? false;
|
|
1427
1431
|
useEffect5(
|
|
1428
1432
|
() => () => {
|
|
1429
1433
|
if (previewRef.current) {
|
|
@@ -1449,6 +1453,14 @@ function MediaUploader({
|
|
|
1449
1453
|
});
|
|
1450
1454
|
return;
|
|
1451
1455
|
}
|
|
1456
|
+
if (deferUpload) {
|
|
1457
|
+
if (previewRef.current) URL.revokeObjectURL(previewRef.current);
|
|
1458
|
+
previewRef.current = URL.createObjectURL(file);
|
|
1459
|
+
setState({ phase: "pending", file });
|
|
1460
|
+
onFileSelected?.(file);
|
|
1461
|
+
onChange(null);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1452
1464
|
const abort = new AbortController();
|
|
1453
1465
|
setState({ phase: "uploading", file, progress: 0, abort });
|
|
1454
1466
|
if (previewRef.current) URL.revokeObjectURL(previewRef.current);
|
|
@@ -1472,7 +1484,7 @@ function MediaUploader({
|
|
|
1472
1484
|
setState({ phase: "error", message });
|
|
1473
1485
|
}
|
|
1474
1486
|
},
|
|
1475
|
-
[kind, ownerType, ownerId, uploadFn, onChange, limit]
|
|
1487
|
+
[kind, ownerType, ownerId, uploadFn, onChange, limit, deferUpload, onFileSelected]
|
|
1476
1488
|
);
|
|
1477
1489
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
1478
1490
|
accept: Object.fromEntries(limit.mimes.map((m) => [m, []])),
|
|
@@ -1498,31 +1510,88 @@ function MediaUploader({
|
|
|
1498
1510
|
}
|
|
1499
1511
|
}
|
|
1500
1512
|
});
|
|
1513
|
+
const hasUploadedValue = !!value && !isPlaceholder && !value.startsWith("blob:");
|
|
1514
|
+
const hasPendingFile = state.phase === "pending" && !!previewRef.current;
|
|
1515
|
+
const hasDoneUpload = state.phase === "done" && hasUploadedValue;
|
|
1516
|
+
const hasImage = hasPendingFile || hasDoneUpload || state.phase === "idle" && hasUploadedValue;
|
|
1517
|
+
const thumbnailSrc = hasPendingFile ? previewRef.current : hasUploadedValue ? value : "";
|
|
1518
|
+
const isSelected = hasPendingFile;
|
|
1519
|
+
const handleReplace = (e) => {
|
|
1520
|
+
e.stopPropagation();
|
|
1521
|
+
fileInputRef.current?.querySelector("input")?.click();
|
|
1522
|
+
};
|
|
1523
|
+
const handleRemove = (e) => {
|
|
1524
|
+
e.stopPropagation();
|
|
1525
|
+
if (previewRef.current) {
|
|
1526
|
+
URL.revokeObjectURL(previewRef.current);
|
|
1527
|
+
previewRef.current = null;
|
|
1528
|
+
}
|
|
1529
|
+
setState({ phase: "idle" });
|
|
1530
|
+
onChange(null);
|
|
1531
|
+
setUrlInput("");
|
|
1532
|
+
if (deferUpload) onFileSelected?.(null);
|
|
1533
|
+
};
|
|
1501
1534
|
return /* @__PURE__ */ jsxs18("div", { className: "space-y-2", children: [
|
|
1502
|
-
label && /* @__PURE__ */ jsx23("div", { className: "text-sm text-
|
|
1535
|
+
label && /* @__PURE__ */ jsx23("div", { className: "text-sm text-neutral-700 dark:text-white/70", children: label }),
|
|
1503
1536
|
/* @__PURE__ */ jsxs18(
|
|
1504
1537
|
"div",
|
|
1505
1538
|
{
|
|
1506
1539
|
...getRootProps(),
|
|
1507
|
-
className: `border-2 border-dashed rounded-
|
|
1540
|
+
className: `border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${isDragActive ? "border-orange-500 dark:border-brand-orange bg-orange-500/10 dark:bg-brand-orange/15" : "border-neutral-200 dark:border-white/8"}`,
|
|
1508
1541
|
children: [
|
|
1509
|
-
/* @__PURE__ */ jsx23("input", { ...getInputProps(), "aria-label": "file uploader" }),
|
|
1510
|
-
|
|
1542
|
+
/* @__PURE__ */ jsx23("span", { ref: fileInputRef, style: { display: "contents" }, children: /* @__PURE__ */ jsx23("input", { ...getInputProps(), "aria-label": "file uploader" }) }),
|
|
1543
|
+
hasImage ? /* @__PURE__ */ jsxs18("div", { className: "flex items-center gap-4", children: [
|
|
1544
|
+
/* @__PURE__ */ jsx23(
|
|
1545
|
+
"img",
|
|
1546
|
+
{
|
|
1547
|
+
src: thumbnailSrc,
|
|
1548
|
+
alt: "uploaded",
|
|
1549
|
+
className: "w-16 h-16 rounded-lg object-cover border border-neutral-200 dark:border-white/8 shrink-0"
|
|
1550
|
+
}
|
|
1551
|
+
),
|
|
1552
|
+
/* @__PURE__ */ jsxs18("div", { className: "flex-1 text-left min-w-0", children: [
|
|
1553
|
+
/* @__PURE__ */ jsxs18("div", { className: "text-sm text-green-500 dark:text-green-400 flex items-center gap-1", children: [
|
|
1554
|
+
/* @__PURE__ */ jsx23("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx23("polyline", { points: "20 6 9 17 4 12" }) }),
|
|
1555
|
+
isSelected ? "Selected" : "Uploaded"
|
|
1556
|
+
] }),
|
|
1557
|
+
isSelected && /* @__PURE__ */ jsx23("div", { className: "text-[10px] text-neutral-500 dark:text-white/45 mt-0.5", children: "Uploads when you save" }),
|
|
1558
|
+
hasPendingFile && /* @__PURE__ */ jsx23("div", { className: "text-xs text-neutral-500 dark:text-white/45 mt-0.5 truncate", children: state.file.name }),
|
|
1559
|
+
/* @__PURE__ */ jsxs18("div", { className: "flex gap-2 mt-2 text-xs", children: [
|
|
1560
|
+
/* @__PURE__ */ jsx23(
|
|
1561
|
+
"button",
|
|
1562
|
+
{
|
|
1563
|
+
type: "button",
|
|
1564
|
+
onClick: handleReplace,
|
|
1565
|
+
className: "text-neutral-700 dark:text-white/70 hover:text-orange-500 dark:hover:text-brand-orange underline-offset-2 hover:underline",
|
|
1566
|
+
children: "Replace"
|
|
1567
|
+
}
|
|
1568
|
+
),
|
|
1569
|
+
/* @__PURE__ */ jsx23(
|
|
1570
|
+
"button",
|
|
1571
|
+
{
|
|
1572
|
+
type: "button",
|
|
1573
|
+
onClick: handleRemove,
|
|
1574
|
+
className: "text-neutral-700 dark:text-white/70 hover:text-red-500 dark:hover:text-red-400 underline-offset-2 hover:underline",
|
|
1575
|
+
children: "Remove"
|
|
1576
|
+
}
|
|
1577
|
+
)
|
|
1578
|
+
] })
|
|
1579
|
+
] })
|
|
1580
|
+
] }) : state.phase === "idle" ? /* @__PURE__ */ jsxs18(Fragment4, { children: [
|
|
1511
1581
|
/* @__PURE__ */ jsx23("div", { className: "text-sm mb-1", children: "Drop image here or click to choose" }),
|
|
1512
|
-
/* @__PURE__ */ jsxs18("div", { className: "text-xs text-
|
|
1582
|
+
/* @__PURE__ */ jsxs18("div", { className: "text-xs text-neutral-500 dark:text-white/45", children: [
|
|
1513
1583
|
formatExtensions(limit.mimes),
|
|
1514
1584
|
" \xB7 max ",
|
|
1515
1585
|
humanSize(limit.maxSize),
|
|
1516
1586
|
limit.maxWidth && limit.maxHeight && ` \xB7 ${limit.maxWidth}\xD7${limit.maxHeight}`
|
|
1517
1587
|
] })
|
|
1518
|
-
] }),
|
|
1519
|
-
state.phase === "uploading" && /* @__PURE__ */ jsxs18(Fragment4, { children: [
|
|
1588
|
+
] }) : state.phase === "uploading" ? /* @__PURE__ */ jsxs18(Fragment4, { children: [
|
|
1520
1589
|
previewRef.current && /* @__PURE__ */ jsx23(
|
|
1521
1590
|
"img",
|
|
1522
1591
|
{
|
|
1523
1592
|
src: previewRef.current,
|
|
1524
1593
|
alt: "upload preview",
|
|
1525
|
-
className: "max-w-[64px] max-h-[64px] rounded
|
|
1594
|
+
className: "max-w-[64px] max-h-[64px] rounded object-cover border border-neutral-200 dark:border-white/8 mx-auto mb-2"
|
|
1526
1595
|
}
|
|
1527
1596
|
),
|
|
1528
1597
|
/* @__PURE__ */ jsxs18("div", { className: "text-sm", children: [
|
|
@@ -1552,9 +1621,7 @@ function MediaUploader({
|
|
|
1552
1621
|
children: "Cancel"
|
|
1553
1622
|
}
|
|
1554
1623
|
)
|
|
1555
|
-
] }),
|
|
1556
|
-
state.phase === "done" && /* @__PURE__ */ jsx23("div", { className: "text-sm text-green-500", children: "\u2713 Uploaded" }),
|
|
1557
|
-
state.phase === "error" && /* @__PURE__ */ jsxs18(Fragment4, { children: [
|
|
1624
|
+
] }) : state.phase === "error" ? /* @__PURE__ */ jsxs18(Fragment4, { children: [
|
|
1558
1625
|
/* @__PURE__ */ jsx23("div", { className: "text-sm text-red-500", children: state.message }),
|
|
1559
1626
|
/* @__PURE__ */ jsx23(
|
|
1560
1627
|
"button",
|
|
@@ -1568,30 +1635,23 @@ function MediaUploader({
|
|
|
1568
1635
|
children: "Try again"
|
|
1569
1636
|
}
|
|
1570
1637
|
)
|
|
1571
|
-
] })
|
|
1638
|
+
] }) : null
|
|
1572
1639
|
]
|
|
1573
1640
|
}
|
|
1574
1641
|
),
|
|
1575
|
-
/* @__PURE__ */
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
"img",
|
|
1589
|
-
{
|
|
1590
|
-
src: value,
|
|
1591
|
-
alt: "current value preview",
|
|
1592
|
-
className: "max-w-[64px] max-h-[64px] rounded-[--radius-sm] object-cover border border-[--border] mt-2"
|
|
1593
|
-
}
|
|
1594
|
-
)
|
|
1642
|
+
!deferUpload && /* @__PURE__ */ jsxs18(Fragment4, { children: [
|
|
1643
|
+
/* @__PURE__ */ jsx23("div", { className: "text-xs text-neutral-500 dark:text-white/45", children: "or paste URL:" }),
|
|
1644
|
+
/* @__PURE__ */ jsx23(
|
|
1645
|
+
Input,
|
|
1646
|
+
{
|
|
1647
|
+
type: "url",
|
|
1648
|
+
placeholder: "https://...",
|
|
1649
|
+
value: urlInput,
|
|
1650
|
+
onChange: (e) => setUrlInput(e.target.value),
|
|
1651
|
+
onBlur: () => urlInput && onChange(urlInput)
|
|
1652
|
+
}
|
|
1653
|
+
)
|
|
1654
|
+
] })
|
|
1595
1655
|
] });
|
|
1596
1656
|
}
|
|
1597
1657
|
|
|
@@ -1615,7 +1675,7 @@ function SortableTile({ item, onRemove }) {
|
|
|
1615
1675
|
transform: CSS.Transform.toString(transform),
|
|
1616
1676
|
transition
|
|
1617
1677
|
};
|
|
1618
|
-
return /* @__PURE__ */ jsxs19("div", { ref: setNodeRef, style, className: "relative w-24 h-24 rounded-
|
|
1678
|
+
return /* @__PURE__ */ jsxs19("div", { ref: setNodeRef, style, className: "relative w-24 h-24 rounded-lg border border-neutral-200 dark:border-white/8 overflow-hidden", children: [
|
|
1619
1679
|
/* @__PURE__ */ jsx24("button", { ...attributes, ...listeners, "aria-label": "drag handle", className: "absolute top-1 left-1 z-10 text-xs opacity-70 hover:opacity-100", children: "\u283F" }),
|
|
1620
1680
|
/* @__PURE__ */ jsx24("img", { src: item.url, alt: `${item.type} thumbnail`, className: "w-full h-full object-cover" }),
|
|
1621
1681
|
/* @__PURE__ */ jsx24(
|
|
@@ -1645,7 +1705,7 @@ function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10
|
|
|
1645
1705
|
onDragOver: (e) => e.preventDefault(),
|
|
1646
1706
|
onDrop: (e) => e.preventDefault(),
|
|
1647
1707
|
children: [
|
|
1648
|
-
/* @__PURE__ */ jsxs19("div", { className: "text-sm text-
|
|
1708
|
+
/* @__PURE__ */ jsxs19("div", { className: "text-sm text-neutral-700 dark:text-white/70", children: [
|
|
1649
1709
|
"Screenshots (",
|
|
1650
1710
|
items.length,
|
|
1651
1711
|
"/",
|
|
@@ -1667,12 +1727,12 @@ function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10
|
|
|
1667
1727
|
type: "button",
|
|
1668
1728
|
"aria-label": "add screenshot",
|
|
1669
1729
|
onClick: () => setAdding(true),
|
|
1670
|
-
className: "w-24 h-24 rounded-
|
|
1730
|
+
className: "w-24 h-24 rounded-lg border-2 border-dashed border-neutral-200 dark:border-white/8 text-2xl hover:border-orange-500 dark:hover:border-brand-orange",
|
|
1671
1731
|
children: "+"
|
|
1672
1732
|
}
|
|
1673
1733
|
)
|
|
1674
1734
|
] }) }) }),
|
|
1675
|
-
adding && /* @__PURE__ */ jsxs19("div", { className: "border border-
|
|
1735
|
+
adding && /* @__PURE__ */ jsxs19("div", { className: "border border-neutral-200 dark:border-white/8 rounded-lg p-3", children: [
|
|
1676
1736
|
/* @__PURE__ */ jsx24(
|
|
1677
1737
|
MediaUploader,
|
|
1678
1738
|
{
|
|
@@ -2020,7 +2080,8 @@ function ProjectPagePreview({
|
|
|
2020
2080
|
positivePercent = 0,
|
|
2021
2081
|
ratingCount = 0,
|
|
2022
2082
|
quests = [],
|
|
2023
|
-
achievements = []
|
|
2083
|
+
achievements = [],
|
|
2084
|
+
tags = []
|
|
2024
2085
|
}) {
|
|
2025
2086
|
const placeholderLogo = `https://placehold.co/80x80/${accentColor.slice(1)}/white?text=${name[0] ?? "?"}`;
|
|
2026
2087
|
return /* @__PURE__ */ jsxs23(
|
|
@@ -2066,18 +2127,28 @@ function ProjectPagePreview({
|
|
|
2066
2127
|
/* @__PURE__ */ jsxs23("div", { children: [
|
|
2067
2128
|
/* @__PURE__ */ jsx28("h1", { className: "text-2xl sm:text-3xl font-bold", children: name }),
|
|
2068
2129
|
tagline && /* @__PURE__ */ jsx28("p", { className: "text-neutral-500 dark:text-white/55 mt-1", children: tagline }),
|
|
2069
|
-
category && /* @__PURE__ */
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2130
|
+
(category || tags.length > 0) && /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-2 mt-3", children: [
|
|
2131
|
+
category && /* @__PURE__ */ jsx28(
|
|
2132
|
+
"span",
|
|
2133
|
+
{
|
|
2134
|
+
className: "inline-flex px-2.5 py-0.5 rounded-lg text-xs font-semibold uppercase tracking-wider",
|
|
2135
|
+
style: {
|
|
2136
|
+
backgroundColor: `${accentColor}15`,
|
|
2137
|
+
color: accentColor,
|
|
2138
|
+
border: `1px solid ${accentColor}30`
|
|
2139
|
+
},
|
|
2140
|
+
children: categoryLabels2[category] ?? category
|
|
2141
|
+
}
|
|
2142
|
+
),
|
|
2143
|
+
tags.slice(0, 3).map((tag) => /* @__PURE__ */ jsx28(
|
|
2144
|
+
"span",
|
|
2145
|
+
{
|
|
2146
|
+
className: "px-2 py-0.5 rounded-md bg-neutral-100 dark:bg-white/6 text-neutral-500 dark:text-white/40 text-[10px] font-mono",
|
|
2147
|
+
children: tag
|
|
2077
2148
|
},
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2149
|
+
tag
|
|
2150
|
+
))
|
|
2151
|
+
] })
|
|
2081
2152
|
] }),
|
|
2082
2153
|
/* @__PURE__ */ jsxs23("div", { className: "flex gap-2 shrink-0 flex-wrap", children: [
|
|
2083
2154
|
/* @__PURE__ */ jsxs23(
|