diff --git a/src/renderer/src/components/ui/Card.tsx b/src/renderer/src/components/ui/Card.tsx new file mode 100644 index 0000000..33f17c2 --- /dev/null +++ b/src/renderer/src/components/ui/Card.tsx @@ -0,0 +1,17 @@ +import type { ButtonHTMLAttributes } from "react"; + +const BASE = + "group block overflow-hidden rounded-lg border border-border bg-surface text-left transition-colors"; + +export function Card({ + className = "", + interactive = true, + ...props +}: ButtonHTMLAttributes & { interactive?: boolean }) { + return ( + +
+        {value}
+      
+ + ); +} diff --git a/src/renderer/src/components/ui/CollapsibleCard.tsx b/src/renderer/src/components/ui/CollapsibleCard.tsx index 2692ade..85ef2b0 100644 --- a/src/renderer/src/components/ui/CollapsibleCard.tsx +++ b/src/renderer/src/components/ui/CollapsibleCard.tsx @@ -1,5 +1,6 @@ import { useState, type ReactNode } from "react"; import { ChevronDown, ChevronRight } from "lucide-react"; +import { LABEL_HEADING } from "./styles"; export function CollapsibleCard({ title, @@ -17,9 +18,7 @@ export function CollapsibleCard({
-
-              {JSON.stringify(entry.value, null, 2)}
-            
- + ) : null} diff --git a/src/renderer/src/features/debug/tabs/GroupsTab.tsx b/src/renderer/src/features/debug/tabs/GroupsTab.tsx index 5b51678..db773d9 100644 --- a/src/renderer/src/features/debug/tabs/GroupsTab.tsx +++ b/src/renderer/src/features/debug/tabs/GroupsTab.tsx @@ -1,9 +1,8 @@ import { useMemo, useState } from "react"; import { ChevronDown, ChevronRight, Users } from "lucide-react"; import type { Group } from "../../../../../shared/types/group"; -import { Avatar, Badge, Panel } from "../../../components/ui"; +import { Avatar, Badge, CodeBlock, Panel } from "../../../components/ui"; import { useAllGroups, useGroups } from "../../../store/groups"; -import { useCopied } from "../useDebug"; import { Empty, SearchInput } from "../ui"; export function GroupStorePanel() { @@ -45,7 +44,6 @@ export function GroupStorePanel() { function GroupStoreRow({ group }: { group: Group }) { const [open, setOpen] = useState(false); - const [copied, copy] = useCopied(); return (
-
-              {JSON.stringify(group, null, 2)}
-            
-
+ ) : null} diff --git a/src/renderer/src/features/debug/tabs/ReposTab.tsx b/src/renderer/src/features/debug/tabs/ReposTab.tsx index c2732b2..8702549 100644 --- a/src/renderer/src/features/debug/tabs/ReposTab.tsx +++ b/src/renderer/src/features/debug/tabs/ReposTab.tsx @@ -1,9 +1,9 @@ import { useEffect, useMemo, useState } from "react"; import { ChevronDown, ChevronRight, Trash2 } from "lucide-react"; import type { RepoStats, StoredEntity } from "../../../../../shared/types/repository"; -import { Button, Panel, Stat } from "../../../components/ui"; +import { Button, CodeBlock, Panel, Stat } from "../../../components/ui"; import { api } from "../../../lib/api"; -import { useCopied, useNow } from "../useDebug"; +import { useNow } from "../useDebug"; import { Empty, Meta, @@ -192,7 +192,6 @@ function RepoInspector({ name, onBack, now }: { name: string; onBack: () => void function EntityRow({ entity, now }: { entity: StoredEntity<{ id: string }>; now: number }) { const [open, setOpen] = useState(false); - const [copied, copy] = useCopied(); const data = entity.data as Record; const label = (data.name ?? data.displayName ?? data.id) as string; const fieldNames = Object.keys(entity.meta.fields).sort(); @@ -255,17 +254,10 @@ function EntityRow({ entity, now }: { entity: StoredEntity<{ id: string }>; now: -
- -
-              {JSON.stringify(entity.data, null, 2)}
-            
-
+ ) : null} diff --git a/src/renderer/src/features/debug/tabs/SocialTab.tsx b/src/renderer/src/features/debug/tabs/SocialTab.tsx index c5e370f..2454d2f 100644 --- a/src/renderer/src/features/debug/tabs/SocialTab.tsx +++ b/src/renderer/src/features/debug/tabs/SocialTab.tsx @@ -1,10 +1,9 @@ import { useMemo, useState } from "react"; import { ChevronDown, ChevronRight } from "lucide-react"; import type { UserProfile } from "../../../../../shared/types/user"; -import { Badge, Panel, PresenceAvatar, Tag } from "../../../components/ui"; +import { Badge, CodeBlock, Panel, PresenceAvatar, Tag } from "../../../components/ui"; import { statusMeta, trustMeta } from "../../../lib/vrchat"; import { useSocial } from "../../../store/social"; -import { useCopied } from "../useDebug"; import { Chip, Empty, Meta, SearchInput } from "../ui"; type SocialFilter = "all" | "self" | "friends" | "other"; @@ -72,7 +71,6 @@ export function SocialTab() { function UserRow({ user, isSelf }: { user: UserProfile; isSelf: boolean }) { const [open, setOpen] = useState(false); - const [copied, copy] = useCopied(); const status = statusMeta[user.status]; const trust = trustMeta[user.trustRank]; @@ -111,17 +109,7 @@ function UserRow({ user, isSelf }: { user: UserProfile; isSelf: boolean }) { {user.statusDescription ? (

“{user.statusDescription}”

) : null} -
- -
-              {JSON.stringify(user, null, 2)}
-            
-
+ ) : null} diff --git a/src/renderer/src/features/debug/tabs/WebSocketTab.tsx b/src/renderer/src/features/debug/tabs/WebSocketTab.tsx index 836711c..fe28aff 100644 --- a/src/renderer/src/features/debug/tabs/WebSocketTab.tsx +++ b/src/renderer/src/features/debug/tabs/WebSocketTab.tsx @@ -1,8 +1,7 @@ import { useMemo, useState } from "react"; import { ChevronDown, ChevronRight } from "lucide-react"; import type { WsEvent } from "../../../../../shared/types/debug"; -import { Badge, Button, Panel } from "../../../components/ui"; -import { useCopied } from "../useDebug"; +import { Badge, Button, CodeBlock, Panel } from "../../../components/ui"; import { Chip, Empty, SearchInput, formatTime } from "../ui"; export function WsTab({ ws, onClear }: { ws: WsEvent[]; onClear: () => void }) { @@ -49,7 +48,6 @@ export function WsTab({ ws, onClear }: { ws: WsEvent[]; onClear: () => void }) { function WsRow({ event }: { event: WsEvent }) { const [open, setOpen] = useState(false); - const [copied, copy] = useCopied(); return (
-
-              {JSON.stringify(event.content, null, 2)}
-            
-
+ ) : null} diff --git a/src/renderer/src/features/debug/tabs/WorldsTab.tsx b/src/renderer/src/features/debug/tabs/WorldsTab.tsx index dd15a54..bd5b0c2 100644 --- a/src/renderer/src/features/debug/tabs/WorldsTab.tsx +++ b/src/renderer/src/features/debug/tabs/WorldsTab.tsx @@ -1,9 +1,8 @@ import { useMemo, useState } from "react"; import { ChevronDown, ChevronRight, Star } from "lucide-react"; import type { World } from "../../../../../shared/types/world"; -import { Badge, Panel } from "../../../components/ui"; +import { Badge, CodeBlock, Panel } from "../../../components/ui"; import { useAllWorlds, useWorlds } from "../../../store/worlds"; -import { useCopied } from "../useDebug"; import { Empty, SearchInput } from "../ui"; export function WorldStorePanel() { @@ -45,7 +44,6 @@ export function WorldStorePanel() { function WorldStoreRow({ world }: { world: World }) { const [open, setOpen] = useState(false); - const [copied, copy] = useCopied(); const img = world.thumbnailImageUrl || world.imageUrl; return (
@@ -72,17 +70,7 @@ function WorldStoreRow({ world }: { world: World }) { {open ? (
-
- -
-              {JSON.stringify(world, null, 2)}
-            
-
+
) : null}
diff --git a/src/renderer/src/features/debug/useDebug.ts b/src/renderer/src/features/debug/useDebug.ts index d5aacba..2adbc55 100644 --- a/src/renderer/src/features/debug/useDebug.ts +++ b/src/renderer/src/features/debug/useDebug.ts @@ -1,8 +1,10 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import type { CacheEntryInfo, CacheStats, LogEntry, WsEvent } from "../../../../shared/types/debug"; import type { RepoStats } from "../../../../shared/types/repository"; import { api, events } from "../../lib/api"; +export { useCopied } from "../../lib/useCopied"; + const MAX_LOGS = 500; export function useNow(ms = 1000): number { @@ -14,19 +16,6 @@ export function useNow(ms = 1000): number { return now; } -export function useCopied(): [boolean, (text: string) => void] { - const [copied, setCopied] = useState(false); - const timer = useRef>(undefined); - const copy = (text: string): void => { - void navigator.clipboard.writeText(text).then(() => { - setCopied(true); - clearTimeout(timer.current); - timer.current = setTimeout(() => setCopied(false), 1200); - }); - }; - return [copied, copy]; -} - export function useDebug() { const [cache, setCache] = useState([]); const [stats, setStats] = useState(null); diff --git a/src/renderer/src/features/group/GroupView.tsx b/src/renderer/src/features/group/GroupView.tsx index 18f387c..7a20f0e 100644 --- a/src/renderer/src/features/group/GroupView.tsx +++ b/src/renderer/src/features/group/GroupView.tsx @@ -1,6 +1,6 @@ import { BadgeCheck, Globe, Users, UserCheck } from "lucide-react"; import type { Group } from "../../../../shared/types/group"; -import { Avatar, Banner, Fact, Section, StatTile, Tag } from "../../components/ui"; +import { Avatar, Banner, Fact, Section, Skeleton, StatTile, Tag } from "../../components/ui"; import { api } from "../../lib/api"; import { useAsync } from "../../lib/useAsync"; import { compactNumber, formatDate, prettyTag } from "../../lib/format"; @@ -132,15 +132,15 @@ function GroupSkeleton() { >
-
+
-
-
+ +
{Array.from({ length: 3 }).map((_, i) => ( -
+ ))}
diff --git a/src/renderer/src/features/profile/LocationSection.tsx b/src/renderer/src/features/profile/LocationSection.tsx index 6e9a22e..d12169f 100644 --- a/src/renderer/src/features/profile/LocationSection.tsx +++ b/src/renderer/src/features/profile/LocationSection.tsx @@ -1,5 +1,6 @@ import { ChevronRight, Globe, MapPin, Users } from "lucide-react"; import { parseLocation, type Location } from "../../../../shared/types/user"; +import { HoverImage } from "../../components/ui"; import { useWorld } from "../../store/worlds"; import { useNav } from "../navigation/NavContext"; import { locationLabel, regionLabels } from "../../lib/vrchat"; @@ -35,13 +36,7 @@ export function LocationSection({ location }: { location?: Location }) { className="group flex w-full items-center gap-3.5 rounded-lg p-1 text-left transition-colors hover:bg-surface-hover" >
- {img ? ( - - ) : null} + {img ? : null}
diff --git a/src/renderer/src/features/profile/ProfileView.tsx b/src/renderer/src/features/profile/ProfileView.tsx index acbe3ce..1203252 100644 --- a/src/renderer/src/features/profile/ProfileView.tsx +++ b/src/renderer/src/features/profile/ProfileView.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { type UserProfile } from "../../../../shared/types/user"; -import { Avatar, Banner, Fact, LinkPill, Section, Tabs, Tag } from "../../components/ui"; +import { Avatar, Banner, Fact, LinkPill, Section, Skeleton, Tabs, Tag } from "../../components/ui"; import { avatarOf, bannerOf, @@ -266,14 +266,14 @@ function ProfileSkeleton() { >
-
+
-
-
+ +
-
-
+ +
); } diff --git a/src/renderer/src/features/profile/WorldsSection.tsx b/src/renderer/src/features/profile/WorldsSection.tsx index e2b6955..f02d835 100644 --- a/src/renderer/src/features/profile/WorldsSection.tsx +++ b/src/renderer/src/features/profile/WorldsSection.tsx @@ -1,6 +1,6 @@ import { Circle, Star, Users } from "lucide-react"; import type { World } from "../../../../shared/types/world"; -import { CollapsibleCard, SkeletonGrid, Tag } from "../../components/ui"; +import { Card, CollapsibleCard, HoverImage, SkeletonGrid, Tag } from "../../components/ui"; import { compactNumber } from "../../lib/format"; import { useNav } from "../navigation/NavContext"; import { useUserWorlds } from "./useUserWorlds"; @@ -103,19 +103,9 @@ function WorldCard({ world }: { world: World }) { const { openWorld } = useNav(); const img = world.thumbnailImageUrl || world.imageUrl; return ( -
- + ); } diff --git a/src/renderer/src/features/search/SearchView.tsx b/src/renderer/src/features/search/SearchView.tsx index f08ffae..23354e6 100644 --- a/src/renderer/src/features/search/SearchView.tsx +++ b/src/renderer/src/features/search/SearchView.tsx @@ -4,7 +4,7 @@ import type { UserProfile } from "../../../../shared/types/user"; import type { World } from "../../../../shared/types/world"; import { api } from "../../lib/api"; import { compactNumber } from "../../lib/format"; -import { Avatar, Button, Field, Tabs, Tag } from "../../components/ui"; +import { Avatar, Button, Card, Field, HoverImage, Tabs, Tag } from "../../components/ui"; import { useNav } from "../navigation/NavContext"; import { avatarOf, trustMeta } from "../../lib/vrchat"; @@ -113,19 +113,9 @@ function UserResult({ user, onOpen }: { user: UserProfile; onOpen: () => void }) function WorldResult({ world, onOpen }: { world: World; onOpen: () => void }) { const img = world.thumbnailImageUrl || world.imageUrl; return ( - + ); } diff --git a/src/renderer/src/features/world/WorldView.tsx b/src/renderer/src/features/world/WorldView.tsx index bc4aa39..8630024 100644 --- a/src/renderer/src/features/world/WorldView.tsx +++ b/src/renderer/src/features/world/WorldView.tsx @@ -1,6 +1,6 @@ import { Globe, Heart, Tag as TagIcon, Users } from "lucide-react"; import type { World } from "../../../../shared/types/world"; -import { Banner, Fact, Section, StatTile, Tag } from "../../components/ui"; +import { Banner, Fact, Section, Skeleton, StatTile, Tag } from "../../components/ui"; import { api } from "../../lib/api"; import { useAsync } from "../../lib/useAsync"; import { compactNumber, formatDate, prettyTag } from "../../lib/format"; @@ -160,15 +160,15 @@ function WorldSkeleton() { >
-
+
-
-
+ +
{Array.from({ length: 4 }).map((_, i) => ( -
+ ))}
diff --git a/src/renderer/src/lib/useCopied.ts b/src/renderer/src/lib/useCopied.ts new file mode 100644 index 0000000..9c7877b --- /dev/null +++ b/src/renderer/src/lib/useCopied.ts @@ -0,0 +1,14 @@ +import { useRef, useState } from "react"; + +export function useCopied(): [boolean, (text: string) => void] { + const [copied, setCopied] = useState(false); + const timer = useRef>(undefined); + const copy = (text: string): void => { + void navigator.clipboard.writeText(text).then(() => { + setCopied(true); + clearTimeout(timer.current); + timer.current = setTimeout(() => setCopied(false), 1200); + }); + }; + return [copied, copy]; +}