@unicitylabs/sphere-ui 0.1.18 → 0.1.20
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 +36 -8
- package/dist/index.js +147 -68
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as react from 'react';
|
|
3
|
-
import { ReactNode, ButtonHTMLAttributes, InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes } from 'react';
|
|
3
|
+
import { ReactNode, ButtonHTMLAttributes, InputHTMLAttributes, SelectHTMLAttributes, TextareaHTMLAttributes, MouseEvent, Ref } from 'react';
|
|
4
4
|
import { ColumnDef } from '@tanstack/react-table';
|
|
5
5
|
export { A as AchievementData, a as AchievementFormApi, Q as QueryKeys, b as QuestData, c as QuestFormApi, T as TrackData, d as TrackFormApi } from './index-DMHfA7fr.js';
|
|
6
6
|
|
|
@@ -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';
|
|
@@ -377,22 +388,37 @@ interface FeaturedProjectCardProps {
|
|
|
377
388
|
*/
|
|
378
389
|
declare function FeaturedProjectCard({ name, tagline, logoUrl, bannerUrl, accentColor, users, quests, positivePercent, ratingCount, onClick, }: FeaturedProjectCardProps): react_jsx_runtime.JSX.Element;
|
|
379
390
|
|
|
391
|
+
/** Loose typing — dnd-kit listeners/attributes don't fit React's HTMLButtonAttributes due to motion.button overrides. */
|
|
392
|
+
type ButtonExtraProps = {
|
|
393
|
+
className?: string;
|
|
394
|
+
style?: React.CSSProperties;
|
|
395
|
+
} & Record<string, unknown>;
|
|
380
396
|
interface InstalledProjectIconProps {
|
|
381
397
|
name: string;
|
|
382
398
|
logoUrl?: string | null;
|
|
383
399
|
/** Hex like "#FF6F00". Defaults to brand orange. */
|
|
384
400
|
accentColor?: string;
|
|
385
401
|
onClick?: () => void;
|
|
402
|
+
/** Right-click handler. Wallet uses it to toggle a context menu. */
|
|
403
|
+
onContextMenu?: (e: MouseEvent<HTMLButtonElement>) => void;
|
|
404
|
+
/** Slot overlaid on the icon tile (absolute top/right). Wallet uses it for the MoreVertical menu button. */
|
|
405
|
+
topRightAction?: ReactNode;
|
|
386
406
|
/** When true, render the name label under the icon (dock vs grid layout). Default true. */
|
|
387
407
|
showLabel?: boolean;
|
|
408
|
+
/** Ref attached to the inner button. Use for dnd-kit `setActivatorNodeRef`. */
|
|
409
|
+
buttonRef?: Ref<HTMLButtonElement>;
|
|
410
|
+
/** Extra props spread on the inner button (e.g. dnd-kit `attributes` + `listeners`). */
|
|
411
|
+
buttonProps?: ButtonExtraProps;
|
|
388
412
|
}
|
|
389
413
|
/**
|
|
390
|
-
* InstalledProjectIcon —
|
|
414
|
+
* InstalledProjectIcon — desktop tile for an installed app/skill.
|
|
391
415
|
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
416
|
+
* Renders the visual (glow + colored tile + logo + label). Behavior is
|
|
417
|
+
* driven entirely by props: pass `onClick` for the primary action,
|
|
418
|
+
* `onContextMenu` for right-click, and `topRightAction` to overlay a
|
|
419
|
+
* small action button (e.g. context-menu trigger) on the tile.
|
|
394
420
|
*/
|
|
395
|
-
declare function InstalledProjectIcon({ name, logoUrl, accentColor, onClick, showLabel, }: InstalledProjectIconProps): react_jsx_runtime.JSX.Element;
|
|
421
|
+
declare function InstalledProjectIcon({ name, logoUrl, accentColor, onClick, onContextMenu, topRightAction, showLabel, buttonRef, buttonProps, }: InstalledProjectIconProps): react_jsx_runtime.JSX.Element;
|
|
396
422
|
|
|
397
423
|
interface QuestPreviewSummary {
|
|
398
424
|
slug: string;
|
|
@@ -430,6 +456,8 @@ interface ProjectPagePreviewProps {
|
|
|
430
456
|
quests?: QuestPreviewSummary[];
|
|
431
457
|
/** Sample achievements */
|
|
432
458
|
achievements?: AchievementPreviewSummary[];
|
|
459
|
+
/** Project tags — first 3 are rendered as chips next to the category badge */
|
|
460
|
+
tags?: string[];
|
|
433
461
|
}
|
|
434
462
|
/**
|
|
435
463
|
* ProjectPagePreview — stateless 1:1 visual copy of sphere wallet's `/apps/:slug` page.
|
|
@@ -437,7 +465,7 @@ interface ProjectPagePreviewProps {
|
|
|
437
465
|
* Used by dev-portal & backoffice as a live preview while editing a project.
|
|
438
466
|
* No router / hooks / data fetching — every value comes via props.
|
|
439
467
|
*/
|
|
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;
|
|
468
|
+
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
469
|
|
|
442
470
|
declare const MEDIA_LIMITS: Record<MediaKind, MediaLimit>;
|
|
443
471
|
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
|
{
|
|
@@ -1702,7 +1762,7 @@ function MediaGallery({ ownerType, ownerId, items, onChange, uploadFn, max = 10
|
|
|
1702
1762
|
// src/components/media/MarketplaceProjectCard.tsx
|
|
1703
1763
|
import { motion } from "framer-motion";
|
|
1704
1764
|
import { Users, Target, ThumbsUp, Plus, Check } from "lucide-react";
|
|
1705
|
-
import {
|
|
1765
|
+
import { jsx as jsx25, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
1706
1766
|
var categoryLabels = {
|
|
1707
1767
|
game: "Game",
|
|
1708
1768
|
defi: "DeFi",
|
|
@@ -1742,19 +1802,18 @@ function MarketplaceProjectCard({
|
|
|
1742
1802
|
className: "no-text-shadow group rounded-2xl border border-neutral-200 dark:border-white/8 hover:border-orange-500/60 dark:hover:border-brand-orange/60 hover:shadow-lg hover:shadow-orange-500/10 dark:hover:shadow-brand-orange/15 transition-all duration-200 cursor-pointer relative overflow-hidden",
|
|
1743
1803
|
children: [
|
|
1744
1804
|
/* @__PURE__ */ jsxs20("div", { className: "relative h-24 overflow-hidden", "data-testid": "banner", children: [
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1805
|
+
/* @__PURE__ */ jsx25(
|
|
1806
|
+
"div",
|
|
1807
|
+
{
|
|
1808
|
+
className: "absolute inset-0 bg-cover bg-center transition-transform duration-500 group-hover:scale-105",
|
|
1809
|
+
style: {
|
|
1810
|
+
backgroundColor: accentColor,
|
|
1811
|
+
backgroundImage: hasBanner ? `url(${bannerUrl})` : void 0
|
|
1751
1812
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
} })
|
|
1756
|
-
] }) : /* @__PURE__ */ jsx25("div", { className: "absolute inset-0", style: {
|
|
1757
|
-
background: `linear-gradient(135deg, ${accentColor}cc 0%, ${accentColor}44 100%)`
|
|
1813
|
+
}
|
|
1814
|
+
),
|
|
1815
|
+
/* @__PURE__ */ jsx25("div", { className: "absolute inset-0", style: {
|
|
1816
|
+
background: hasBanner ? `linear-gradient(to bottom, transparent 0%, ${accentColor}66 100%)` : `linear-gradient(135deg, ${accentColor}cc 0%, ${accentColor}44 100%)`
|
|
1758
1817
|
} }),
|
|
1759
1818
|
showInstall && /* @__PURE__ */ jsx25(
|
|
1760
1819
|
"button",
|
|
@@ -1911,18 +1970,26 @@ function InstalledProjectIcon({
|
|
|
1911
1970
|
logoUrl,
|
|
1912
1971
|
accentColor = "#FF6F00",
|
|
1913
1972
|
onClick,
|
|
1914
|
-
|
|
1973
|
+
onContextMenu,
|
|
1974
|
+
topRightAction,
|
|
1975
|
+
showLabel = true,
|
|
1976
|
+
buttonRef,
|
|
1977
|
+
buttonProps
|
|
1915
1978
|
}) {
|
|
1916
1979
|
const [imgError, setImgError] = useState9(false);
|
|
1917
1980
|
return /* @__PURE__ */ jsx27("div", { className: "relative", children: /* @__PURE__ */ jsxs22(
|
|
1918
1981
|
motion3.button,
|
|
1919
1982
|
{
|
|
1983
|
+
ref: buttonRef,
|
|
1920
1984
|
type: "button",
|
|
1921
1985
|
onClick,
|
|
1986
|
+
onContextMenu,
|
|
1922
1987
|
whileHover: { scale: 1.08, y: -4 },
|
|
1923
1988
|
whileTap: { scale: 0.92 },
|
|
1924
1989
|
transition: { duration: 0.05 },
|
|
1925
|
-
|
|
1990
|
+
...buttonProps,
|
|
1991
|
+
className: `flex flex-col items-center gap-2 p-3 rounded-2xl group cursor-pointer relative${buttonProps?.className ? " " + buttonProps.className : ""}`,
|
|
1992
|
+
style: { touchAction: "none", ...buttonProps?.style },
|
|
1926
1993
|
children: [
|
|
1927
1994
|
/* @__PURE__ */ jsxs22("div", { className: "relative", children: [
|
|
1928
1995
|
/* @__PURE__ */ jsx27(
|
|
@@ -1955,12 +2022,13 @@ function InstalledProjectIcon({
|
|
|
1955
2022
|
src: logoUrl,
|
|
1956
2023
|
alt: name,
|
|
1957
2024
|
onError: () => setImgError(true),
|
|
1958
|
-
className: "
|
|
2025
|
+
className: "absolute inset-0 w-full h-full object-cover z-10"
|
|
1959
2026
|
}
|
|
1960
|
-
) : /* @__PURE__ */ jsx27("span", { className: "text-white font-bold text-
|
|
2027
|
+
) : /* @__PURE__ */ jsx27("span", { className: "text-white font-bold text-2xl sm:text-3xl relative z-10", children: name[0] ?? "?" })
|
|
1961
2028
|
]
|
|
1962
2029
|
}
|
|
1963
|
-
)
|
|
2030
|
+
),
|
|
2031
|
+
topRightAction
|
|
1964
2032
|
] }),
|
|
1965
2033
|
showLabel && /* @__PURE__ */ jsx27("span", { className: "text-xs sm:text-sm font-medium text-neutral-500 dark:text-[rgba(255,255,255,0.45)] group-hover:text-neutral-900 dark:group-hover:text-white transition-colors truncate max-w-20 sm:max-w-24 text-center leading-tight", children: name })
|
|
1966
2034
|
]
|
|
@@ -2020,7 +2088,8 @@ function ProjectPagePreview({
|
|
|
2020
2088
|
positivePercent = 0,
|
|
2021
2089
|
ratingCount = 0,
|
|
2022
2090
|
quests = [],
|
|
2023
|
-
achievements = []
|
|
2091
|
+
achievements = [],
|
|
2092
|
+
tags = []
|
|
2024
2093
|
}) {
|
|
2025
2094
|
const placeholderLogo = `https://placehold.co/80x80/${accentColor.slice(1)}/white?text=${name[0] ?? "?"}`;
|
|
2026
2095
|
return /* @__PURE__ */ jsxs23(
|
|
@@ -2066,18 +2135,28 @@ function ProjectPagePreview({
|
|
|
2066
2135
|
/* @__PURE__ */ jsxs23("div", { children: [
|
|
2067
2136
|
/* @__PURE__ */ jsx28("h1", { className: "text-2xl sm:text-3xl font-bold", children: name }),
|
|
2068
2137
|
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
|
-
|
|
2138
|
+
(category || tags.length > 0) && /* @__PURE__ */ jsxs23("div", { className: "flex items-center gap-2 mt-3", children: [
|
|
2139
|
+
category && /* @__PURE__ */ jsx28(
|
|
2140
|
+
"span",
|
|
2141
|
+
{
|
|
2142
|
+
className: "inline-flex px-2.5 py-0.5 rounded-lg text-xs font-semibold uppercase tracking-wider",
|
|
2143
|
+
style: {
|
|
2144
|
+
backgroundColor: `${accentColor}15`,
|
|
2145
|
+
color: accentColor,
|
|
2146
|
+
border: `1px solid ${accentColor}30`
|
|
2147
|
+
},
|
|
2148
|
+
children: categoryLabels2[category] ?? category
|
|
2149
|
+
}
|
|
2150
|
+
),
|
|
2151
|
+
tags.slice(0, 3).map((tag) => /* @__PURE__ */ jsx28(
|
|
2152
|
+
"span",
|
|
2153
|
+
{
|
|
2154
|
+
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",
|
|
2155
|
+
children: tag
|
|
2077
2156
|
},
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2157
|
+
tag
|
|
2158
|
+
))
|
|
2159
|
+
] })
|
|
2081
2160
|
] }),
|
|
2082
2161
|
/* @__PURE__ */ jsxs23("div", { className: "flex gap-2 shrink-0 flex-wrap", children: [
|
|
2083
2162
|
/* @__PURE__ */ jsxs23(
|