feat: improve onboarding

This commit is contained in:
2024-01-26 10:17:23 +07:00
parent 74426e13c8
commit 9ba584bf14
14 changed files with 222 additions and 125 deletions

View File

@@ -25,6 +25,7 @@
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",

View File

@@ -124,11 +124,11 @@ export function CreateAccountAddress() {
// add account to storage // add account to storage
await storage.createSetting("nsecbunker", "1"); await storage.createSetting("nsecbunker", "1");
const dbAccount = await storage.createAccount({ const newAccount = await storage.createAccount({
pubkey: account, pubkey: account,
privkey: localSigner.privateKey, privkey: localSigner.privateKey,
}); });
ark.account = dbAccount; ark.account = newAccount;
// get final signer with newly created account // get final signer with newly created account
const finalSigner = new NDKNip46Signer(bunker, account, localSigner); const finalSigner = new NDKNip46Signer(bunker, account, localSigner);
@@ -153,9 +153,11 @@ export function CreateAccountAddress() {
return ( return (
<div className="relative flex items-center justify-center w-full h-full"> <div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto"> <div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Create Account</h1> <h1 className="text-2xl font-semibold">
Let's set up your account on Nostr
</h1>
</div> </div>
{!services ? ( {!services ? (
<div className="flex items-center justify-center w-full"> <div className="flex items-center justify-center w-full">

View File

@@ -1,13 +1,16 @@
import { useArk } from "@lume/ark"; import { useArk } from "@lume/ark";
import { CheckIcon, EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage"; import { useStorage } from "@lume/storage";
import { onboardingAtom } from "@lume/utils"; import { onboardingAtom } from "@lume/utils";
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import * as Checkbox from "@radix-ui/react-checkbox";
import { desktopDir } from "@tauri-apps/api/path"; import { desktopDir } from "@tauri-apps/api/path";
import { save } from "@tauri-apps/plugin-dialog"; import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs"; import { writeTextFile } from "@tauri-apps/plugin-fs";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { getPublicKey, nip19 } from "nostr-tools"; import { getPublicKey, nip19 } from "nostr-tools";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -17,12 +20,18 @@ export function CreateAccountKeys() {
const setOnboarding = useSetAtom(onboardingAtom); const setOnboarding = useSetAtom(onboardingAtom);
const navigate = useNavigate(); const navigate = useNavigate();
const generateNostrKeys = async () => { const [key, setKey] = useState("");
const signer = NDKPrivateKeySigner.generate(); const [loading, setLoading] = useState(false);
const pubkey = getPublicKey(signer.privateKey); const [showKey, setShowKey] = useState(false);
const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false });
const npub = nip19.npubEncode(pubkey); const submit = async () => {
const nsec = nip19.nsecEncode(signer.privateKey); try {
setLoading(true);
const privkey = nip19.decode(key).data as string;
const signer = new NDKPrivateKeySigner(privkey);
const pubkey = getPublicKey(privkey);
ark.updateNostrSigner({ signer }); ark.updateNostrSigner({ signer });
@@ -38,52 +47,138 @@ export function CreateAccountKeys() {
await writeTextFile( await writeTextFile(
filePath, filePath,
`Nostr Account\nGenerated by Lume (lume.nu)\n---\nPublic key: ${npub}\nPrivate key: ${nsec}`, `Nostr Account\nGenerated by Lume (lume.nu)\n---\nPrivate key: ${key}`,
); );
await storage.createAccount({ const newAccount = await storage.createAccount({
pubkey: pubkey, pubkey: pubkey,
privkey: signer.privateKey, privkey: privkey,
}); });
ark.account = newAccount;
setLoading(false);
setOnboarding({ open: true, newUser: true }); setOnboarding({ open: true, newUser: true });
return navigate("/auth/onboarding", { replace: true }); return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setLoading(false);
toast.error(String(e));
}
}; };
useEffect(() => {
const privkey = NDKPrivateKeySigner.generate().privateKey;
setKey(nip19.nsecEncode(privkey));
}, []);
return ( return (
<div className="relative flex items-center justify-center w-full h-full"> <div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto"> <div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Create Account</h1> <h1 className="text-2xl font-semibold">
This is your new Account Key
</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Keep your key in safe place. If you lose this key, you will lose
access to your account.
</p>
</div> </div>
<div className="flex flex-col gap-3 mb-0"> <div className="flex flex-col gap-6 mb-0">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-6">
<label <div className="relative">
htmlFor="npub"
className="text-sm font-semibold uppercase text-neutral-600"
>
Public Key
</label>
<input <input
readOnly readOnly
type="text" value={key}
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800" type={showKey ? "text" : "password"}
className="pl-3 pr-14 w-full resize-none text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/> />
</div> <button
<div className="flex flex-col gap-2"> type="button"
<label onClick={() => setShowKey((state) => !state)}
htmlFor="nsec" className="absolute right-2 top-2 size-10 inline-flex items-center justify-center rounded-lg text-white bg-neutral-800 hover:bg-neutral-700"
className="text-sm font-semibold uppercase text-neutral-600"
> >
Private Key {showKey ? (
</label> <EyeOnIcon className="size-5" />
<input ) : (
readOnly <EyeOffIcon className="size-5" />
type="text" )}
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800" </button>
/>
</div> </div>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c1}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c1: !state.c1 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none"
id="confirm1"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-500"
htmlFor="confirm1"
>
I understand the risk of lost private key.
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c2}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c2: !state.c2 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none"
id="confirm2"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-500"
htmlFor="confirm2"
>
I will make sure keep it safe and not sharing with anyone.
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c3}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c3: !state.c3 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none"
id="confirm3"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-500"
htmlFor="confirm3"
>
I understand I cannot recover private key.
</label>
</div>
</div>
</div>
<button
type="button"
onClick={submit}
disabled={!confirm.c1 || !confirm.c2 || !confirm.c3}
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Save key & Continue"
)}
</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -21,29 +21,14 @@ export function CreateAccountScreen() {
return ( return (
<div className="relative flex items-center justify-center w-full h-full"> <div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto"> <div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold"> <h1 className="text-2xl font-semibold">Let's Get Started</h1>
Let's get you set up on Nostr.
</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500"> <p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Choose one of methods below to create your account Choose one of methods below to create your account
</p> </p>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<button
type="button"
onClick={() => setMethod("managed")}
className={cn(
"flex flex-col items-start px-4 py-3.5 bg-neutral-900 rounded-xl hover:bg-neutral-800",
method === "managed" ? "ring-1 ring-teal-500" : "",
)}
>
<p className="font-semibold">Managed by Provider</p>
<p className="text-sm font-medium text-neutral-500">
A 3rd party provider will handle your sign in keys for you.
</p>
</button>
<button <button
type="button" type="button"
onClick={() => setMethod("self")} onClick={() => setMethod("self")}
@@ -57,6 +42,19 @@ export function CreateAccountScreen() {
You create your keys and keep them safe. You create your keys and keep them safe.
</p> </p>
</button> </button>
<button
type="button"
onClick={() => setMethod("managed")}
className={cn(
"flex flex-col items-start px-4 py-3.5 bg-neutral-900 rounded-xl hover:bg-neutral-800",
method === "managed" ? "ring-1 ring-teal-500" : "",
)}
>
<p className="font-semibold">Managed by Provider</p>
<p className="text-sm font-medium text-neutral-500">
A 3rd party provider will handle your sign in keys for you.
</p>
</button>
<button <button
type="button" type="button"
onClick={next} onClick={next}

View File

@@ -50,7 +50,7 @@ export function LoginWithKey() {
return ( return (
<div className="relative flex items-center justify-center w-full h-full"> <div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto"> <div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Enter your Private Key</h1> <h1 className="text-2xl font-semibold">Enter your Private Key</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500"> <p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">

View File

@@ -66,7 +66,7 @@ export function LoginWithNsecbunker() {
return ( return (
<div className="relative flex items-center justify-center w-full h-full"> <div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto"> <div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold"> <h1 className="text-2xl font-semibold">
Enter your nsecbunker token Enter your nsecbunker token

View File

@@ -128,7 +128,7 @@ export function LoginWithOAuth() {
return ( return (
<div className="relative flex items-center justify-center w-full h-full"> <div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto"> <div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Enter your Nostr Address</h1> <h1 className="text-2xl font-semibold">Enter your Nostr Address</h1>
</div> </div>

View File

@@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
export function LoginScreen() { export function LoginScreen() {
return ( return (
<div className="relative flex items-center justify-center w-full h-full"> <div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-16 mx-auto"> <div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Welcome back, anon!</h1> <h1 className="text-2xl font-semibold">Welcome back, anon!</h1>
</div> </div>

View File

@@ -2,10 +2,7 @@ import { useArk } from "@lume/ark";
import { InfoIcon, LoaderIcon } from "@lume/icons"; import { InfoIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage"; import { useStorage } from "@lume/storage";
import { TranslateRegisterModal } from "@lume/ui"; import { TranslateRegisterModal } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKKind } from "@nostr-dev-kit/ndk";
import * as Switch from "@radix-ui/react-switch"; import * as Switch from "@radix-ui/react-switch";
import { useQueryClient } from "@tanstack/react-query";
import { import {
isPermissionGranted, isPermissionGranted,
requestPermission, requestPermission,
@@ -17,7 +14,6 @@ import { toast } from "sonner";
export function OnboardingScreen() { export function OnboardingScreen() {
const ark = useArk(); const ark = useArk();
const storage = useStorage(); const storage = useStorage();
const queryClient = useQueryClient();
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -61,32 +57,7 @@ export function OnboardingScreen() {
// get account contacts // get account contacts
await ark.getUserContacts(); await ark.getUserContacts();
// refetch newsfeed navigate("/", { replace: true });
await queryClient.prefetchInfiniteQuery({
queryKey: ["timeline-9999"],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: ark.account.contacts,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
});
navigate("/");
}; };
useEffect(() => { useEffect(() => {
@@ -117,7 +88,7 @@ export function OnboardingScreen() {
return ( return (
<div className="relative flex h-full w-full items-center justify-center"> <div className="relative flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-10"> <div className="mx-auto flex w-full max-w-md flex-col gap-8">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold"> <h1 className="text-2xl font-semibold">
You&apos;re almost ready to use Lume. You&apos;re almost ready to use Lume.

View File

@@ -145,7 +145,7 @@ export class Ark {
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST, cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
}); });
if (!profile) throw new Error("user not found"); if (!profile) return null;
return profile; return profile;
} catch { } catch {
throw new Error("user not found"); throw new Error("user not found");

View File

@@ -57,8 +57,8 @@ export function OnboardingInterestScreen() {
<div className="w-full flex-1 min-h-0 flex flex-col justify-between"> <div className="w-full flex-1 min-h-0 flex flex-col justify-between">
<div className="flex-1 min-h-0 overflow-y-auto px-8 py-8"> <div className="flex-1 min-h-0 overflow-y-auto px-8 py-8">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
{TOPICS.map((topic, index) => ( {TOPICS.map((topic) => (
<div key={topic.title + index} className="flex flex-col gap-4"> <div key={topic.title} className="flex flex-col gap-4">
<div className="w-full flex items-center justify-between"> <div className="w-full flex items-center justify-between">
<div className="inline-flex items-center gap-2.5"> <div className="inline-flex items-center gap-2.5">
<img <img
@@ -79,6 +79,7 @@ export function OnboardingInterestScreen() {
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
{topic.content.map((hashtag) => ( {topic.content.map((hashtag) => (
<button <button
key={hashtag}
type="button" type="button"
onClick={() => toggleHashtag(hashtag)} onClick={() => toggleHashtag(hashtag)}
className={cn( className={cn(

View File

@@ -35,19 +35,17 @@ export function OnboardingProfileScreen() {
navigate("/interests"); navigate("/interests");
} }
const oldProfile = await ark.getUserProfile(); const prevProfile = await ark.getUserProfile();
const newProfile: NDKUserProfile = {
const profile: NDKUserProfile = {
...data, ...data,
lud16: "", // temporary remove lud16 nip05: prevProfile?.nip05 || "",
nip05: oldProfile?.nip05 || "",
bio: data.about, bio: data.about,
image: picture, image: picture,
picture: picture, picture: picture,
}; };
const publish = await ark.createEvent({ const publish = await ark.createEvent({
content: JSON.stringify(profile), content: JSON.stringify(newProfile),
kind: NDKKind.Metadata, kind: NDKKind.Metadata,
tags: [], tags: [],
}); });
@@ -57,7 +55,7 @@ export function OnboardingProfileScreen() {
await storage.clearProfileCache(ark.account.pubkey); await storage.clearProfileCache(ark.account.pubkey);
await queryClient.setQueryData( await queryClient.setQueryData(
["user", ark.account.pubkey], ["user", ark.account.pubkey],
() => profile, () => newProfile,
); );
setLoading(false); setLoading(false);

View File

@@ -114,11 +114,11 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
</div> </div>
<div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900"> <div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900">
{isLoading ? ( {isLoading ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-44 w-full items-center justify-center">
<LoaderIcon className="size-4 animate-spin" /> <LoaderIcon className="size-4 animate-spin" />
</div> </div>
) : isError ? ( ) : isError ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-44 w-full items-center justify-center">
Error. Cannot get trending users Error. Cannot get trending users
</div> </div>
) : ( ) : (
@@ -171,9 +171,9 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
type="button" type="button"
onClick={submit} onClick={submit}
disabled={loading} disabled={loading}
className="inline-flex items-center justify-center gap-2 px-6 font-medium text-white transform bg-blue-500 rounded-full active:translate-y-1 w-36 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed" className="inline-flex items-center justify-center gap-2 px-6 font-medium shadow-xl shadow-neutral-500/50 text-white transform bg-blue-500 rounded-full active:translate-y-1 w-36 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed"
> >
Save Save & Go Back
</button> </button>
</div> </div>
</div> </div>

31
pnpm-lock.yaml generated
View File

@@ -117,6 +117,9 @@ importers:
'@radix-ui/react-avatar': '@radix-ui/react-avatar':
specifier: ^1.0.4 specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-checkbox':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-collapsible': '@radix-ui/react-collapsible':
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0) version: 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
@@ -2138,6 +2141,34 @@ packages:
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
/@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.8
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.48)(react@18.2.0)
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.48)(react@18.2.0)
'@types/react': 18.2.48
'@types/react-dom': 18.2.18
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0): /@radix-ui/react-collapsible@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==} resolution: {integrity: sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==}
peerDependencies: peerDependencies: