diff --git a/package.json b/package.json index 7abebc3..e66a0d9 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-tabs": "^1.0.4", "@react-hook/resize-observer": "^1.2.6", "@snort/system-react": "^1.0.8", "@testing-library/jest-dom": "^5.14.1", diff --git a/src/const.ts b/src/const.ts new file mode 100644 index 0000000..40e3568 --- /dev/null +++ b/src/const.ts @@ -0,0 +1,4 @@ +import { EventKind } from "@snort/system"; + +export const LIVE_STREAM = 30_311 as EventKind; +export const LIVE_STREAM_CHAT = 1_311 as EventKind; diff --git a/src/element/follow-button.tsx b/src/element/follow-button.tsx new file mode 100644 index 0000000..dab69c0 --- /dev/null +++ b/src/element/follow-button.tsx @@ -0,0 +1,4 @@ +// todo +export function FollowButton({ pubkey }: { pubkey: string }) { + return ; +} diff --git a/src/element/live-chat.css b/src/element/live-chat.css index ce361bc..681e023 100644 --- a/src/element/live-chat.css +++ b/src/element/live-chat.css @@ -187,10 +187,6 @@ margin: 0; } -.zap-icon { - color: #FFCB44; -} - .zap-container { position: relative; border-radius: 12px; @@ -211,11 +207,11 @@ } .zap-container .profile { - color: #FFCB44; + color: #FF8D2B; } .zap-container .zap-amount { - color: #FFCB44; + color: #FF8D2B; } .zap-content { diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index 7d2ba64..b7600db 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -9,7 +9,6 @@ import { } from "@snort/system"; import { useState, - useMemo, useEffect, type KeyboardEvent, type ChangeEvent, @@ -27,41 +26,27 @@ import Spinner from "./spinner"; import { useLogin } from "hooks/login"; import { useUserProfile } from "@snort/system-react"; import { formatSats } from "number"; +import useTopZappers from "hooks/top-zappers"; export interface LiveChatOptions { canWrite?: boolean; showHeader?: boolean; } -function totalZapped(pubkey: string, zaps: ParsedZap[]) { - return zaps - .filter((z) => (z.anonZap ? pubkey === "anon" : z.sender === pubkey)) - .reduce((acc, z) => acc + z.amount, 0); -} - function TopZappers({ zaps }: { zaps: ParsedZap[] }) { - const zappers = zaps - .map((z) => (z.anonZap ? "anon" : z.sender)) - .map((p) => p as string); - - const sortedZappers = useMemo(() => { - const sorted = [...new Set([...zappers])]; - sorted.sort((a, b) => totalZapped(b, zaps) - totalZapped(a, zaps)); - return sorted.slice(0, 3); - }, [zaps, zappers]); + const zappers = useTopZappers(zaps).slice(0, 3); return ( <>

Top zappers

- {sortedZappers.map((pk, idx) => { - const total = totalZapped(pk, zaps); + {zappers.map(({ pubkey, total }, idx) => { return ( -
- {pk === "anon" ? ( +
+ {pubkey === "anon" ? (

Anon

) : ( - + )}

{formatSats(total)}

@@ -132,7 +117,7 @@ function ChatMessage({ ev, link }: { ev: TaggedRawEvent; link: NostrLink }) { return (
- +
); } diff --git a/src/element/mention.tsx b/src/element/mention.tsx index b54ff0c..5f5dfaa 100644 --- a/src/element/mention.tsx +++ b/src/element/mention.tsx @@ -1,5 +1,7 @@ +import { Link } from "react-router-dom"; import { useUserProfile } from "@snort/system-react"; import { System } from "index"; +import { hexToBech32 } from "utils"; interface MentionProps { pubkey: string; @@ -8,13 +10,6 @@ interface MentionProps { export function Mention({ pubkey, relays }: MentionProps) { const user = useUserProfile(System, pubkey); - return ( - - {user?.name || pubkey} - - ); + const npub = hexToBech32("npub", pubkey); + return {user?.name || pubkey}; } diff --git a/src/element/profile.tsx b/src/element/profile.tsx index 8fc9204..f723858 100644 --- a/src/element/profile.tsx +++ b/src/element/profile.tsx @@ -1,7 +1,9 @@ import "./profile.css"; +import { Link } from "react-router-dom"; import { useUserProfile } from "@snort/system-react"; import { UserMetadata } from "@snort/system"; import { hexToBech32 } from "@snort/shared"; +import { Icon } from "element/icon"; import { System } from "index"; export interface ProfileOptions { @@ -12,13 +14,14 @@ export interface ProfileOptions { } export function getName(pk: string, user?: UserMetadata) { - const shortPubkey = hexToBech32("npub", pk).slice(0, 12); - if ((user?.display_name?.length ?? 0) > 0) { - return user?.display_name; - } + const npub = hexToBech32("npub", pk); + const shortPubkey = npub.slice(0, 12); if ((user?.name?.length ?? 0) > 0) { return user?.name; } + if ((user?.display_name?.length ?? 0) > 0) { + return user?.display_name; + } return shortPubkey; } @@ -33,17 +36,32 @@ export function Profile({ }) { const profile = useUserProfile(System, pubkey); - return ( -
- {(options?.showAvatar ?? true) && ( + const content = ( + <> + {(options?.showAvatar ?? true) && pubkey === "anon" ? ( + + ) : ( {profile?.name )} - {(options?.showName ?? true) && - (options?.overrideName ?? getName(pubkey, profile))} -
+ {(options?.showName ?? true) && ( + + {options?.overrideName ?? pubkey === "anon" + ? "Anon" + : getName(pubkey, profile)} + + )} + + ); + + return pubkey === "anon" ? ( +
{content}
+ ) : ( + + {content} + ); } diff --git a/src/element/send-zap.tsx b/src/element/send-zap.tsx index 84257f0..0931917 100644 --- a/src/element/send-zap.tsx +++ b/src/element/send-zap.tsx @@ -1,6 +1,6 @@ import "./send-zap.css"; import * as Dialog from "@radix-ui/react-dialog"; -import { useEffect, useState } from "react"; +import { useEffect, useState, ReactNode } from "react"; import { LNURL } from "@snort/shared"; import { NostrEvent, EventPublisher } from "@snort/system"; import { formatSats } from "../number"; @@ -15,6 +15,7 @@ interface SendZapsProps { ev?: NostrEvent; targetName?: string; onFinish: () => void; + button?: ReactNode; } function SendZaps({ lnurl, ev, targetName, onFinish }: SendZapsProps) { @@ -154,15 +155,20 @@ export function SendZapsDialog({ lnurl, ev, targetName, + button, }: Omit) { const [isOpen, setIsOpen] = useState(false); return ( - + {button ? ( + button + ) : ( + + )} diff --git a/src/element/tags.tsx b/src/element/tags.tsx new file mode 100644 index 0000000..4cd5c05 --- /dev/null +++ b/src/element/tags.tsx @@ -0,0 +1,26 @@ +import moment from "moment"; +import { TaggedRawEvent } from "@snort/system"; +import { StreamState } from "index"; +import { findTag } from "utils"; + +export function Tags({ ev }: { ev: TaggedRawEvent }) { + const status = findTag(ev, "status"); + const start = findTag(ev, "starts"); + return ( +
+ {status === StreamState.Planned && ( + + Starts {moment(Number(start) * 1000).fromNow()} + + )} + {ev.tags + .filter((a) => a[0] === "t") + .map((a) => a[1]) + .map((a) => ( + + {a} + + ))} +
+ ); +} diff --git a/src/element/text.tsx b/src/element/text.tsx index e722d55..56070f3 100644 --- a/src/element/text.tsx +++ b/src/element/text.tsx @@ -1,5 +1,5 @@ import { useMemo, type ReactNode } from "react"; -import { TaggedRawEvent, validateNostrLink } from "@snort/system"; +import { validateNostrLink } from "@snort/system"; import { splitByUrl } from "utils"; import { Emoji } from "./emoji"; import { HyperText } from "./hypertext"; @@ -74,11 +74,11 @@ function extractLinks(fragments: Fragment[]) { .flat(); } -export function Text({ ev }: { ev: TaggedRawEvent }) { +export function Text({ content, tags }: { content: string; tags: string[][] }) { // todo: RTL langugage support const element = useMemo(() => { - return {transformText([ev.content], ev.tags)}; - }, [ev]); + return {transformText([content], tags)}; + }, [content, tags]); return <>{element}; } diff --git a/src/hooks/live-chat.tsx b/src/hooks/live-chat.tsx index fec2c02..01a28fd 100644 --- a/src/hooks/live-chat.tsx +++ b/src/hooks/live-chat.tsx @@ -1,7 +1,13 @@ -import { NostrLink, RequestBuilder, EventKind, FlatNoteStore } from "@snort/system"; +import { + NostrLink, + RequestBuilder, + EventKind, + FlatNoteStore, +} from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import { System } from "index"; import { useMemo } from "react"; +import { LIVE_STREAM_CHAT } from "const"; export function useLiveChatFeed(link: NostrLink) { const sub = useMemo(() => { @@ -10,11 +16,11 @@ export function useLiveChatFeed(link: NostrLink) { leaveOpen: true, }); rb.withFilter() - .kinds([EventKind.ZapReceipt, 1311 as EventKind]) + .kinds([EventKind.ZapReceipt, LIVE_STREAM_CHAT]) .tag("a", [`${link.kind}:${link.author}:${link.id}`]) .limit(100); return rb; }, [link]); return useRequestBuilder(System, FlatNoteStore, sub); -} \ No newline at end of file +} diff --git a/src/hooks/login.ts b/src/hooks/login.ts index 1829200..77c8c07 100644 --- a/src/hooks/login.ts +++ b/src/hooks/login.ts @@ -2,5 +2,8 @@ import { Login } from "index"; import { useSyncExternalStore } from "react"; export function useLogin() { - return useSyncExternalStore(c => Login.hook(c), () => Login.snapshot()); -} \ No newline at end of file + return useSyncExternalStore( + (c) => Login.hook(c), + () => Login.snapshot() + ); +} diff --git a/src/hooks/profile.ts b/src/hooks/profile.ts new file mode 100644 index 0000000..fabd01a --- /dev/null +++ b/src/hooks/profile.ts @@ -0,0 +1,67 @@ +import { useMemo } from "react"; +import { + RequestBuilder, + ReplaceableNoteStore, + FlatNoteStore, + NostrLink, + EventKind, + parseZap, +} from "@snort/system"; +import { useRequestBuilder } from "@snort/system-react"; +import { LIVE_STREAM } from "const"; +import { findTag } from "utils"; +import { System } from "index"; + +export function useProfile(link: NostrLink, leaveOpen = false) { + const sub = useMemo(() => { + const b = new RequestBuilder(`profile:${link.id.slice(0, 12)}`); + b.withOptions({ + leaveOpen, + }) + .withFilter() + .kinds([LIVE_STREAM]) + .authors([link.id]); + return b; + }, [link, leaveOpen]); + + const { data: streamsData } = useRequestBuilder( + System, + ReplaceableNoteStore, + sub + ); + + const streams = Array.isArray(streamsData) + ? streamsData + : streamsData + ? [streamsData] + : []; + + const addresses = useMemo(() => { + return streams.map((e) => `${e.kind}:${e.pubkey}:${findTag(e, "d")}`); + }, [streamsData]); + + const zapsSub = useMemo(() => { + const b = new RequestBuilder(`profile-zaps:${link.id.slice(0, 12)}`); + b.withOptions({ + leaveOpen, + }) + .withFilter() + .kinds([EventKind.ZapReceipt]) + .tag("a", addresses); + return b; + }, [link, addresses, leaveOpen]); + + const { data: zapsData } = useRequestBuilder( + System, + FlatNoteStore, + zapsSub + ); + const zaps = (zapsData ?? []) + .map((ev) => parseZap(ev, System.ProfileLoader.Cache)) + .filter((z) => z && z.valid); + + return { + streams, + zaps, + }; +} diff --git a/src/hooks/top-zappers.ts b/src/hooks/top-zappers.ts new file mode 100644 index 0000000..1ce59fc --- /dev/null +++ b/src/hooks/top-zappers.ts @@ -0,0 +1,25 @@ +import { useMemo } from "react"; +import { ParsedZap } from "@snort/system"; + +function totalZapped(pubkey: string, zaps: ParsedZap[]) { + return zaps + .filter((z) => (z.anonZap ? pubkey === "anon" : z.sender === pubkey)) + .reduce((acc, z) => acc + z.amount, 0); +} + +export default function useTopZappers(zaps: ParsedZap[]) { + const zappers = zaps + .map((z) => (z.anonZap ? "anon" : z.sender)) + .map((p) => p as string); + + const sorted = useMemo(() => { + const pubkeys = [...new Set([...zappers])]; + const result = pubkeys.map((pubkey) => { + return { pubkey, total: totalZapped(pubkey, zaps) }; + }); + result.sort((a, b) => b.total - a.total); + return result; + }, [zaps, zappers]); + + return sorted; +} diff --git a/src/index.tsx b/src/index.tsx index 1483aaa..ae3af0d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,6 +7,7 @@ import { RouterProvider, createBrowserRouter } from "react-router-dom"; import { RootPage } from "./pages/root"; import { LayoutPage } from "pages/layout"; +import { ProfilePage } from "pages/profile-page"; import { StreamPage } from "pages/stream-page"; import { ChatPopout } from "pages/chat-popout"; import { LoginStore } from "login"; @@ -14,7 +15,7 @@ import { LoginStore } from "login"; export enum StreamState { Live = "live", Ended = "ended", - Planned = "planned" + Planned = "planned", } export const System = new NostrSystem({}); @@ -42,7 +43,11 @@ const router = createBrowserRouter([ element: , }, { - path: "/live/:id", + path: "/p/:npub", + element: , + }, + { + path: "/:id", element: , }, { diff --git a/src/login.ts b/src/login.ts index c856704..0603211 100644 --- a/src/login.ts +++ b/src/login.ts @@ -1,29 +1,31 @@ import { ExternalStore } from "@snort/shared"; export interface LoginSession { - pubkey: string + pubkey: string; + follows: string[]; } export class LoginStore extends ExternalStore { - #session?: LoginSession; + #session?: LoginSession; - constructor() { - super(); - const json = window.localStorage.getItem("session"); - if (json) { - this.#session = JSON.parse(json); - } + constructor() { + super(); + const json = window.localStorage.getItem("session"); + if (json) { + this.#session = JSON.parse(json); } + } - loginWithPubkey(pk: string) { - this.#session = { - pubkey: pk - }; - window.localStorage.setItem("session", JSON.stringify(this.#session)); - this.notifyChange(); - } + loginWithPubkey(pk: string) { + this.#session = { + pubkey: pk, + follows: [], + }; + window.localStorage.setItem("session", JSON.stringify(this.#session)); + this.notifyChange(); + } - takeSnapshot() { - return this.#session ? { ...this.#session } : undefined; - } -} \ No newline at end of file + takeSnapshot() { + return this.#session ? { ...this.#session } : undefined; + } +} diff --git a/src/pages/layout.css b/src/pages/layout.css index 321ad6a..81becb4 100644 --- a/src/pages/layout.css +++ b/src/pages/layout.css @@ -11,8 +11,7 @@ height: 100vh; } - -.page.home { +.page.only-content { display: grid; height: 100vh; grid-template-areas: @@ -84,6 +83,10 @@ header { gap: 24px; padding: 24px 0 32px 0; } + + .page.only-content { + grid-template-rows: 88px 1fr; + } } header .logo { @@ -172,3 +175,12 @@ button span.hide-on-mobile { max-height: 85vh; padding: 25px; } + +.zap-icon { + color: #FF8D2B; +} + +.tags { + display: flex; + gap: 8px; +} diff --git a/src/pages/layout.tsx b/src/pages/layout.tsx index 3a2f8d9..1f85581 100644 --- a/src/pages/layout.tsx +++ b/src/pages/layout.tsx @@ -70,8 +70,8 @@ export function LayoutPage() { return (
+ +
+ +

{formatSats(total)}

+
+
+ ); +} + +function TopZappers({ zaps }: { zaps: ParsedZap[] }) { + const zappers = useTopZappers(zaps); + return ( +
+ {zappers.map((z) => ( + + ))} +
+ ); +} + +const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp"; + +export function ProfilePage() { + const navigate = useNavigate(); + const params = useParams(); + const link = parseNostrLink(params.npub!); + const profile = useUserProfile(System, link.id); + const zapTarget = profile?.lud16 ?? profile?.lud06; + const { streams, zaps } = useProfile(link, true); + const liveEvent = useMemo(() => { + return streams.find((ev) => findTag(ev, "status") === StreamState.Live); + }, [streams]); + const pastStreams = useMemo(() => { + return streams.filter((ev) => findTag(ev, "status") === StreamState.Ended); + }, [streams]); + const futureStreams = useMemo(() => { + return streams.filter( + (ev) => findTag(ev, "status") === StreamState.Planned + ); + }, [streams]); + const isLive = Boolean(liveEvent); + + function goToLive() { + if (liveEvent) { + const d = + liveEvent.tags?.find((t: string[]) => t?.at(0) === "d")?.at(1) || ""; + const naddr = encodeTLV( + NostrPrefix.Address, + d, + undefined, + liveEvent.kind, + liveEvent.pubkey + ); + navigate(`/${naddr}`); + } + } + + // todo: follow + + return ( +
+
+ {profile?.name +
+ {profile?.picture && ( + {profile.name + )} +
+ {isLive ? ( +
+ + live +
+ ) : ( + offline + )} +
+
+ {zapTarget && ( + +
+ Zap + +
+ + } + targetName={profile?.name || link.id} + /> + )} +
+
+ {profile?.name &&

{profile.name}

} + {profile?.about && ( +

+ +

+ )} +
+ + + + Top Zappers + + + Past Streams + + + Schedule + + + + + + +
+ {pastStreams.map((ev) => ( +
+ + +
+ ))} +
+
+ +
+ {futureStreams.map((ev) => ( +
+ + +
+ ))} +
+
+
+
+
+
+ ); +} diff --git a/src/pages/root.tsx b/src/pages/root.tsx index 456edc7..4536b22 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -2,53 +2,84 @@ import "./root.css"; import { useMemo } from "react"; import { unixNow } from "@snort/shared"; -import { EventKind, ParameterizedReplaceableNoteStore, RequestBuilder } from "@snort/system"; +import { + ParameterizedReplaceableNoteStore, + RequestBuilder, +} from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import { StreamState, System } from ".."; import { VideoTile } from "../element/video-tile"; import { findTag } from "utils"; +import { LIVE_STREAM } from "const"; export function RootPage() { - const rb = useMemo(() => { - const rb = new RequestBuilder("root"); - rb.withOptions({ - leaveOpen: true - }).withFilter() - .kinds([30_311 as EventKind]) - .since(unixNow() - 86400); - return rb; - }, []); + const rb = useMemo(() => { + const rb = new RequestBuilder("root"); + rb.withOptions({ + leaveOpen: true, + }) + .withFilter() + .kinds([LIVE_STREAM]) + .since(unixNow() - 86400); + return rb; + }, []); - const feed = useRequestBuilder(System, ParameterizedReplaceableNoteStore, rb); - const feedSorted = useMemo(() => { - if (feed.data) { - return [...feed.data].sort((a, b) => { - const aStatus = findTag(a, "status")!; - const bStatus = findTag(b, "status")!; - if (aStatus === bStatus) { - return b.created_at > a.created_at ? 1 : -1; - } else { - return aStatus === "live" ? -1 : 1; - } - }); + const feed = useRequestBuilder( + System, + ParameterizedReplaceableNoteStore, + rb + ); + const feedSorted = useMemo(() => { + if (feed.data) { + return [...feed.data].sort((a, b) => { + const aStatus = findTag(a, "status")!; + const bStatus = findTag(b, "status")!; + if (aStatus === bStatus) { + return b.created_at > a.created_at ? 1 : -1; + } else { + return aStatus === "live" ? -1 : 1; } - return []; - }, [feed.data]) + }); + } + return []; + }, [feed.data]); - const live = feedSorted.filter(a => findTag(a, "status") === StreamState.Live); - const planned = feedSorted.filter(a => findTag(a, "status") === StreamState.Planned); - const ended = feedSorted.filter(a => findTag(a, "status") === StreamState.Ended); - return
-
- {live.map(e => )} -
- {planned.length > 0 && <>

Planned

-
- {planned.map(e => )} -
} - {ended.length > 0 && <>

Ended

-
- {ended.map(e => )} -
} + const live = feedSorted.filter( + (a) => findTag(a, "status") === StreamState.Live + ); + const planned = feedSorted.filter( + (a) => findTag(a, "status") === StreamState.Planned + ); + const ended = feedSorted.filter( + (a) => findTag(a, "status") === StreamState.Ended + ); + return ( +
+
+ {live.map((e) => ( + + ))} +
+ {planned.length > 0 && ( + <> +

Planned

+
+ {planned.map((e) => ( + + ))} +
+ + )} + {ended.length > 0 && ( + <> +

Ended

+
+ {ended.map((e) => ( + + ))} +
+ + )}
-} \ No newline at end of file + ); +} diff --git a/src/pages/stream-page.css b/src/pages/stream-page.css index 0d6e83e..139f721 100644 --- a/src/pages/stream-page.css +++ b/src/pages/stream-page.css @@ -43,6 +43,7 @@ .pill.live { color: inherit; + text-transform: uppercase; } @media (min-width: 1020px) { @@ -103,11 +104,6 @@ margin: 0 0 12px 0; } -.tags { - display: flex; - gap: 8px; -} - .actions { margin: 8px 0 0 0; display: flex; diff --git a/src/pages/stream-page.tsx b/src/pages/stream-page.tsx index 4db320d..e9eae89 100644 --- a/src/pages/stream-page.tsx +++ b/src/pages/stream-page.tsx @@ -2,7 +2,6 @@ import "./stream-page.css"; import { useRef } from "react"; import { parseNostrLink, EventPublisher } from "@snort/system"; import { useNavigate, useParams } from "react-router-dom"; -import moment from "moment"; import useEventFeed from "hooks/event-feed"; import { LiveVideoPlayer } from "element/live-video-player"; @@ -11,11 +10,12 @@ import { Profile, getName } from "element/profile"; import { LiveChat } from "element/live-chat"; import AsyncButton from "element/async-button"; import { useLogin } from "hooks/login"; -import { StreamState, System } from "index"; +import { System } from "index"; import { SendZapsDialog } from "element/send-zap"; import type { NostrLink } from "@snort/system"; import { useUserProfile } from "@snort/system-react"; import { NewStreamDialog } from "element/new-stream"; +import { Tags } from "element/tags"; function ProfileInfo({ link }: { link: NostrLink }) { const thisEvent = useEventFeed(link, true); @@ -24,9 +24,6 @@ function ProfileInfo({ link }: { link: NostrLink }) { const profile = useUserProfile(System, thisEvent.data?.pubkey); const zapTarget = profile?.lud16 ?? profile?.lud06; - const status = findTag(thisEvent.data, "status"); - const start = findTag(thisEvent.data, "starts"); - const isLive = status === "live"; const isMine = link.author === login?.pubkey; async function deleteStream() { @@ -45,22 +42,7 @@ function ProfileInfo({ link }: { link: NostrLink }) {

{findTag(thisEvent.data, "title")}

{findTag(thisEvent.data, "summary")}

-
- {status} - {status === StreamState.Planned && ( - - Starts {moment(Number(start) * 1000).fromNow()} - - )} - {thisEvent.data?.tags - .filter((a) => a[0] === "t") - .map((a) => a[1]) - .map((a) => ( - - {a} - - ))} -
+ {thisEvent?.data && } {isMine && (
{thisEvent.data && ( diff --git a/yarn.lock b/yarn.lock index b2fc368..6d10e5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1706,6 +1706,17 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-collection@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159" + integrity sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-compose-refs@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989" @@ -1741,6 +1752,13 @@ aria-hidden "^1.1.1" react-remove-scroll "2.5.5" +"@radix-ui/react-direction@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" + integrity sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-dismissable-layer@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz#883a48f5f938fa679427aa17fcba70c5494c6978" @@ -1803,6 +1821,22 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-slot" "1.0.2" +"@radix-ui/react-roving-focus@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974" + integrity sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-slot@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" @@ -1811,6 +1845,21 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" +"@radix-ui/react-tabs@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2" + integrity sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-callback-ref@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"