@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 CHANGED
@@ -310,8 +310,19 @@ interface MediaUploaderProps {
310
310
  onChange: (url: string | null) => void;
311
311
  uploadFn: MediaUploadFn;
312
312
  label?: string;
313
- }
314
- declare function MediaUploader({ kind, ownerType, ownerId, value, onChange, uploadFn, label, }: MediaUploaderProps): react_jsx_runtime.JSX.Element;
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-[--text-secondary]", children: label }),
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-[--radius-md] p-6 text-center cursor-pointer transition-colors ${isDragActive ? "border-[--accent] bg-[--accent-glow]" : "border-[--border]"}`,
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
- state.phase === "idle" && /* @__PURE__ */ jsxs18(Fragment4, { children: [
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-[--text-muted]", children: [
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-[--radius-sm] object-cover border border-[--border] mx-auto mb-2"
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__ */ jsx23("div", { className: "text-xs text-[--text-muted]", children: "or paste URL:" }),
1576
- /* @__PURE__ */ jsx23(
1577
- "input",
1578
- {
1579
- type: "url",
1580
- placeholder: "https://...",
1581
- value: urlInput,
1582
- onChange: (e) => setUrlInput(e.target.value),
1583
- onBlur: () => urlInput && onChange(urlInput),
1584
- className: "w-full bg-[--bg-surface] border border-[--border] rounded-[--radius-sm] px-3 py-2 text-sm"
1585
- }
1586
- ),
1587
- value && /* @__PURE__ */ jsx23(
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-[--radius-md] border border-[--border] overflow-hidden", children: [
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-[--text-secondary]", children: [
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-[--radius-md] border-2 border-dashed border-[--border] text-2xl hover:border-[--accent]",
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-[--border] rounded-[--radius-md] p-3", children: [
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__ */ jsx28("div", { className: "flex items-center gap-2 mt-3", children: /* @__PURE__ */ jsx28(
2070
- "span",
2071
- {
2072
- className: "inline-flex px-2.5 py-0.5 rounded-lg text-xs font-semibold uppercase tracking-wider",
2073
- style: {
2074
- backgroundColor: `${accentColor}15`,
2075
- color: accentColor,
2076
- border: `1px solid ${accentColor}30`
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
- children: categoryLabels2[category] ?? category
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unicitylabs/sphere-ui",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",