diff --git a/app/api/get-repo-blobs/route.ts b/app/api/get-repo-blobs/route.ts
new file mode 100644
index 0000000..95660f1
--- /dev/null
+++ b/app/api/get-repo-blobs/route.ts
@@ -0,0 +1,56 @@
+import { octokit } from "@/components/github/octokit";
+import { getRepoBlobsSchema } from "@/components/github/schema";
+
+import { z } from "zod";
+
+const requestSchema = z.object({
+ owner: z.string(),
+ repo: z.string(),
+ branch: z.string(),
+ paths: z.array(z.string()),
+});
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+
+ const { owner, repo, branch, paths } = requestSchema.parse({
+ owner: searchParams.get("owner"),
+ repo: searchParams.get("repo"),
+ branch: searchParams.get("branch"),
+ paths: searchParams.getAll("path"),
+ });
+
+ const {
+ data: { tree },
+ } = await octokit.rest.git.getTree({
+ owner,
+ repo,
+ tree_sha: branch,
+ recursive: "true",
+ });
+
+ let response: { path: string; contents: string }[] = [];
+
+ const files = tree
+ .filter((i) => i.type === "blob")
+ .filter((i) => (i.path ? paths.includes(i.path) : false));
+
+ for (const file of files) {
+ const {
+ data: { content },
+ } = await octokit.rest.git.getBlob({
+ owner,
+ repo,
+ file_sha: file.sha!,
+ });
+
+ response.push({
+ path: file.path!,
+ contents: Buffer.from(content, "base64").toString("ascii"),
+ });
+ }
+
+ const parsed = getRepoBlobsSchema.parse(response);
+
+ return Response.json(parsed);
+}
diff --git a/app/api/get-repo-branches/route.ts b/app/api/get-repo-branches/route.ts
new file mode 100644
index 0000000..a90bfaa
--- /dev/null
+++ b/app/api/get-repo-branches/route.ts
@@ -0,0 +1,21 @@
+import { octokit } from "@/components/github/octokit";
+import { getRepoBranchesSchema } from "@/components/github/schema";
+
+import { z } from "zod";
+
+const requestSchema = z.object({ owner: z.string(), repo: z.string() });
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+
+ const { owner, repo } = requestSchema.parse({
+ owner: searchParams.get("owner"),
+ repo: searchParams.get("repo"),
+ });
+
+ const { data } = await octokit.rest.repos.listBranches({ owner, repo });
+
+ const parsed = getRepoBranchesSchema.parse(data.map((i) => i.name));
+
+ return Response.json(parsed);
+}
diff --git a/app/api/get-repo-info/route.ts b/app/api/get-repo-info/route.ts
new file mode 100644
index 0000000..511af1e
--- /dev/null
+++ b/app/api/get-repo-info/route.ts
@@ -0,0 +1,21 @@
+import { octokit } from "@/components/github/octokit";
+import { getRepoInfoSchema } from "@/components/github/schema";
+
+import { z } from "zod";
+
+const requestSchema = z.object({ owner: z.string(), repo: z.string() });
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+
+ const { owner, repo } = requestSchema.parse({
+ owner: searchParams.get("owner"),
+ repo: searchParams.get("repo"),
+ });
+
+ const { data } = await octokit.rest.repos.get({ owner, repo });
+
+ const parsed = getRepoInfoSchema.parse(data);
+
+ return Response.json(parsed);
+}
diff --git a/app/api/get-repo-tree/route.ts b/app/api/get-repo-tree/route.ts
new file mode 100644
index 0000000..092fb4b
--- /dev/null
+++ b/app/api/get-repo-tree/route.ts
@@ -0,0 +1,40 @@
+import { octokit } from "@/components/github/octokit";
+import { getRepoTreeSchema } from "@/components/github/schema";
+
+import { z } from "zod";
+
+const requestSchema = z.object({
+ owner: z.string(),
+ repo: z.string(),
+ branch: z.string(),
+});
+
+export async function GET(request: Request) {
+ const { searchParams } = new URL(request.url);
+
+ const { owner, repo, branch } = requestSchema.parse({
+ owner: searchParams.get("owner"),
+ repo: searchParams.get("repo"),
+ branch: searchParams.get("branch"),
+ });
+
+ const {
+ data: { default_branch },
+ } = await octokit.rest.repos.get({
+ owner,
+ repo,
+ });
+
+ const {
+ data: { tree },
+ } = await octokit.rest.git.getTree({
+ owner,
+ repo,
+ tree_sha: branch || default_branch,
+ recursive: "true",
+ });
+
+ const parsed = getRepoTreeSchema.parse(tree);
+
+ return Response.json(parsed);
+}
diff --git a/app/import/page.tsx b/app/import/page.tsx
new file mode 100644
index 0000000..5140ce2
--- /dev/null
+++ b/app/import/page.tsx
@@ -0,0 +1,10 @@
+import GitHub from "@/components/github";
+
+export default function Page() {
+ return (
+
+
Import GitHub repository
+
+
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
index 3af092e..7c06c24 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -87,8 +87,12 @@ export default function Home() {
))}
- ... enter a GitHub repo url:
-
+
+ ... enter a{" "}
+
+ GitHub repo url (click here)
+ {" "}
+
... or generate from a prompt:
diff --git a/components/github/file-selection.css b/components/github/file-selection.css
new file mode 100644
index 0000000..623875f
--- /dev/null
+++ b/components/github/file-selection.css
@@ -0,0 +1,63 @@
+.checkbox {
+ font-size: 16px;
+ user-select: none;
+ padding: 20px;
+ box-sizing: content-box;
+}
+
+.checkbox .tree,
+.checkbox .tree-node,
+.checkbox .tree-node-group {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.checkbox .tree-branch-wrapper,
+.checkbox .tree-node__leaf {
+ outline: none;
+}
+
+.checkbox .tree-node {
+ cursor: pointer;
+}
+
+.checkbox .tree-node .name:hover {
+ background: rgba(0, 0, 0, 0.1);
+}
+
+.checkbox .tree-node--focused .name {
+ background: rgba(0, 0, 0, 0.2);
+}
+
+.checkbox .tree-node {
+ display: inline-block;
+}
+
+.checkbox .tree-node > * {
+ display: inline;
+}
+
+.checkbox .checkbox-icon {
+ margin: 0 5px;
+ vertical-align: middle;
+}
+
+.checkbox button {
+ border: none;
+ background: transparent;
+ cursor: pointer;
+}
+
+.checkbox .arrow {
+ margin-left: 5px;
+ vertical-align: middle;
+}
+
+.checkbox .arrow--open {
+ transform: rotate(90deg);
+}
+
+.checkbox .tree-leaf-list-item {
+ margin-left: 21px;
+}
\ No newline at end of file
diff --git a/components/github/file-selection.tsx b/components/github/file-selection.tsx
new file mode 100644
index 0000000..ddf037c
--- /dev/null
+++ b/components/github/file-selection.tsx
@@ -0,0 +1,134 @@
+import React, { useMemo } from "react";
+import { FaSquare, FaCheckSquare, FaMinusSquare } from "react-icons/fa";
+import { IoMdArrowDropright } from "react-icons/io";
+import TreeView, { ITreeViewOnSelectProps } from "react-accessible-treeview";
+import clsx from "clsx";
+import { IconBaseProps } from "react-icons/lib";
+import "./file-selection.css";
+import { useRepoTree } from "./use-octokit";
+
+function FileSelection({
+ ownerrepo,
+ branch,
+ onSelect,
+}: {
+ ownerrepo?: string;
+ branch?: string;
+ onSelect?: (props: ITreeViewOnSelectProps) => void;
+}) {
+ const { data: repoData, isLoading } = useRepoTree(ownerrepo, branch);
+
+ const data = useMemo(() => {
+ if (!!repoData) {
+ const tree = repoData;
+
+ const root = {
+ id: 0,
+ children: tree
+ .filter((i) => !i.path?.includes("/"))
+ .map((i) => i.path!),
+ parent: null,
+ name: "",
+ };
+
+ const rest = tree.map((i) => ({
+ id: i.path!,
+ name: i.path?.split("/").pop() || "",
+ children: tree
+ .filter((j) => i.path === j.path?.split("/").slice(0, -1).join("/"))
+ .map((k) => k.path!),
+ parent:
+ tree.find((j) => j.path === i.path?.split("/").slice(0, -2).join("/"))
+ ?.path || root.id,
+ }));
+
+ return [root, ...rest];
+ }
+
+ return undefined;
+ }, [repoData]);
+
+ if (isLoading || !data) return
Loading...
;
+
+ return (
+
+
+
{
+ return (
+
+ {isBranch &&
}
+
{
+ handleSelect(e);
+ e.stopPropagation();
+ }}
+ variant={
+ isHalfSelected ? "some" : isSelected ? "all" : "none"
+ }
+ />
+ {element.name}
+
+ );
+ }}
+ />
+
+
+ );
+}
+
+const ArrowIcon = ({
+ isOpen,
+ className,
+}: {
+ isOpen: boolean;
+ className?: string;
+}) => {
+ const baseClass = "arrow";
+ const classes = clsx(
+ baseClass,
+ { [`${baseClass}--closed`]: !isOpen },
+ { [`${baseClass}--open`]: isOpen },
+ className
+ );
+ return
;
+};
+
+const CheckBoxIcon = ({
+ variant,
+ ...rest
+}: IconBaseProps & { variant: "all" | "none" | "some" | string }) => {
+ switch (variant) {
+ case "all":
+ return
;
+ case "none":
+ return
;
+ case "some":
+ return
;
+ default:
+ return null;
+ }
+};
+
+export default FileSelection;
diff --git a/components/github/index.tsx b/components/github/index.tsx
new file mode 100644
index 0000000..3f03619
--- /dev/null
+++ b/components/github/index.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import { useState } from "react";
+import { RepoUrlForm } from "./repo-url-form";
+import { RepoSelectFiles } from "./repo-select-files";
+import { X } from "lucide-react";
+import { RepoBranchSelection } from "./repo-branch-selection";
+import { RepoWorkspaceName } from "./repo-workspace-name";
+
+export default function GitHub() {
+ const [repo, setRepo] = useState
();
+ const [branch, setBranch] = useState();
+ const [selected, setSelected] = useState([]);
+
+ return (
+ <>
+ {repo ? (
+
+
Currently selected repo:
+
+ {repo}{" "}
+
+
+
+ ) : (
+ {
+ setRepo(repo);
+ setBranch(branch);
+ }}
+ />
+ )}
+ {repo ? (
+ <>
+ {branch ? (
+
+ You have selected {branch} branch.{" "}
+
+
+ ) : (
+ setBranch(branch)}
+ />
+ )}
+ {branch && selected.length === 0 ? (
+ {
+ setSelected(paths);
+ }}
+ />
+ ) : null}
+ >
+ ) : null}
+ {selected.length > 0 && repo && branch ? (
+
+ {selected.join(", ")} selected.{" "}
+
+
+
+ ) : null}
+ >
+ );
+}
diff --git a/components/github/octokit.ts b/components/github/octokit.ts
new file mode 100644
index 0000000..62e4f90
--- /dev/null
+++ b/components/github/octokit.ts
@@ -0,0 +1,28 @@
+import { Octokit } from "octokit";
+import { throttling } from "@octokit/plugin-throttling";
+import { getGitHubToken } from "@/lib/env";
+
+const MyOctokit = Octokit.plugin(throttling);
+
+export const octokit = new MyOctokit({
+ auth: getGitHubToken(),
+ throttle: {
+ onRateLimit: (retryAfter, options, octokit, retryCount) => {
+ octokit.log.warn(
+ `Request quota exhausted for request ${options.method} ${options.url}`
+ );
+
+ if (retryCount < 1) {
+ // only retries once
+ octokit.log.info(`Retrying after ${retryAfter} seconds!`);
+ return true;
+ }
+ },
+ onSecondaryRateLimit: (_retryAfter, options, octokit) => {
+ // does not retry, only logs a warning
+ octokit.log.warn(
+ `SecondaryRateLimit detected for request ${options.method} ${options.url}`
+ );
+ },
+ },
+});
diff --git a/components/github/repo-branch-selection.tsx b/components/github/repo-branch-selection.tsx
new file mode 100644
index 0000000..0f328c8
--- /dev/null
+++ b/components/github/repo-branch-selection.tsx
@@ -0,0 +1,139 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Check, ChevronsUpDown } from "lucide-react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { useRepoBranch, useRepoInfo } from "./use-octokit";
+import { useEffect, useMemo } from "react";
+
+const FormSchema = z.object({
+ branch: z.string({
+ required_error: "Please select a branch.",
+ }),
+});
+
+export function RepoBranchSelection({
+ repo,
+ onSubmit: _onSubmit,
+}: {
+ repo: string;
+ onSubmit?: (val: { branch?: string }) => void;
+}) {
+ const { data } = useRepoBranch(repo);
+ const { data: info } = useRepoInfo(repo);
+
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ });
+
+ function onSubmit(data: z.infer) {
+ _onSubmit?.(data);
+ }
+
+ const branches = useMemo(() => {
+ if (!data) return [];
+
+ return data.map((i) => ({ label: i, value: i }));
+ }, [data]);
+
+ useEffect(() => {
+ if (!!info) {
+ form.setValue("branch", info.default_branch);
+ }
+ }, [info]);
+
+ return (
+
+
+ );
+}
diff --git a/components/github/repo-select-files.tsx b/components/github/repo-select-files.tsx
new file mode 100644
index 0000000..8d4460f
--- /dev/null
+++ b/components/github/repo-select-files.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Loader2 } from "lucide-react";
+import FileSelection from "./file-selection";
+
+const FormSchema = z.object({
+ paths: z.array(z.string()).min(1, "At least 1 file should be selected!"),
+});
+
+export function RepoSelectFiles({
+ repo,
+ branch,
+ onSubmit: _onSubmit,
+}: {
+ repo?: string;
+ branch?: string;
+ onSubmit?: (val: string[]) => Promise;
+}) {
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ paths: [],
+ },
+ });
+
+ async function onSubmit(data: z.infer) {
+ form.clearErrors();
+ if (!!_onSubmit) {
+ await _onSubmit(data.paths);
+ }
+ }
+
+ return (
+
+
+ );
+}
diff --git a/components/github/repo-url-form.tsx b/components/github/repo-url-form.tsx
new file mode 100644
index 0000000..2a92889
--- /dev/null
+++ b/components/github/repo-url-form.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { Loader2 } from "lucide-react";
+import gh from "parse-github-url";
+
+const FormSchema = z.object({
+ url: z.string().refine((arg) => {
+ const { repo } = gh(arg) || {};
+
+ return !!repo;
+ }, "Is not a valid github url"),
+});
+
+export function RepoUrlForm({
+ onSubmit: _onSubmit,
+}: {
+ onSubmit?: (val: { repo?: string; branch?: string }) => Promise;
+}) {
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ url: "https://github.com/AElfProject/aelf-developer-tools",
+ },
+ });
+
+ async function onSubmit(data: z.infer) {
+ form.clearErrors();
+ if (!!_onSubmit) {
+ const { repo, branch } = gh(data.url) || {};
+ await _onSubmit({ repo: repo || undefined, branch: branch || undefined });
+ }
+ }
+
+ return (
+
+
+ );
+}
diff --git a/components/github/repo-workspace-name.tsx b/components/github/repo-workspace-name.tsx
new file mode 100644
index 0000000..b3d3f42
--- /dev/null
+++ b/components/github/repo-workspace-name.tsx
@@ -0,0 +1,128 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { useRouter } from "next/navigation";
+import { db } from "@/data/db";
+import { Loader2 } from "lucide-react";
+import { getRepoBlobsSchema } from "./schema";
+
+const FormSchema = z.object({
+ name: z.string().min(2, {
+ message: "Name must be at least 2 characters.",
+ }),
+ template: z.string(),
+});
+
+function formatError(err: unknown) {
+ const strErr = String(err);
+
+ if (strErr.includes("Key already exists in the object store")) {
+ return "This workspace name already in use.";
+ }
+
+ return strErr;
+}
+
+export function RepoWorkspaceName({
+ repo,
+ branch,
+ paths,
+}: {
+ repo: string;
+ branch: string;
+ paths: string[];
+}) {
+ const form = useForm>({
+ resolver: zodResolver(FormSchema),
+ defaultValues: {
+ name: "",
+ template: `github.com/${repo}/tree/${branch}`,
+ },
+ });
+
+ const router = useRouter();
+
+ async function onSubmit(data: z.infer) {
+ form.clearErrors();
+ try {
+ await db.workspaces.add({
+ name: data.name,
+ template: data.template,
+ dll: "",
+ });
+
+ const [owner, repoName] = repo.split("/");
+
+ const params = new URLSearchParams();
+
+ params.set("owner", owner);
+ params.set("repo", repoName);
+ params.set("branch", branch);
+ paths.forEach((path) => params.append("path", path));
+
+ const filesRes = await fetch(`/api/get-repo-blobs?${params.toString()}`);
+ const fileData = await filesRes.json();
+
+ const parsedData = getRepoBlobsSchema.parse(fileData);
+
+ await db.files.bulkAdd(
+ parsedData.map(({ path, contents }) => ({
+ path: `/workspace/${data.name}/${encodeURIComponent(path)}`,
+ contents,
+ }))
+ );
+ await router.push(`/workspace/${data.name}`);
+ } catch (err) {
+ form.setError("name", { message: formatError(err) });
+ }
+ }
+
+ return (
+
+
+ );
+}
diff --git a/components/github/schema.ts b/components/github/schema.ts
new file mode 100644
index 0000000..568933f
--- /dev/null
+++ b/components/github/schema.ts
@@ -0,0 +1,22 @@
+import { z } from "zod";
+
+export const getRepoInfoSchema = z.object({ default_branch: z.string() });
+
+export const getRepoBranchesSchema = z.array(z.string());
+
+export const getRepoBlobSchema = z.object({ content: z.string() });
+
+export const getRepoTreeSchema = z.array(
+ z.object({
+ path: z.string().optional(),
+ mode: z.string().optional(),
+ type: z.string().optional(),
+ sha: z.string().optional(),
+ size: z.number().optional(),
+ url: z.string().optional(),
+ })
+);
+
+export const getRepoBlobsSchema = z.array(
+ z.object({ path: z.string(), contents: z.string() })
+);
diff --git a/components/github/use-octokit.ts b/components/github/use-octokit.ts
new file mode 100644
index 0000000..18285f6
--- /dev/null
+++ b/components/github/use-octokit.ts
@@ -0,0 +1,53 @@
+"use client";
+import useSWR from "swr";
+import {
+ getRepoBranchesSchema,
+ getRepoInfoSchema,
+ getRepoTreeSchema,
+} from "./schema";
+
+export function useRepoTree(ownerrepo?: string, branch?: string) {
+ return useSWR(
+ ownerrepo ? `repo-tree-${ownerrepo}-${branch}` : undefined,
+ async () => {
+ const [owner, repo] = ownerrepo!.split("/");
+
+ const res = await fetch(
+ `/api/get-repo-tree?owner=${owner}&repo=${repo}&branch=${branch}`
+ );
+
+ const data = await res.json();
+
+ return getRepoTreeSchema.parse(data);
+ }
+ );
+}
+
+export function useRepoBranch(ownerrepo?: string) {
+ return useSWR(
+ ownerrepo ? `repo-branch-${ownerrepo}` : undefined,
+ async () => {
+ const [owner, repo] = ownerrepo!.split("/");
+
+ const res = await fetch(
+ `/api/get-repo-branches?owner=${owner}&repo=${repo}`
+ );
+
+ const data = await res.json();
+
+ return getRepoBranchesSchema.parse(data);
+ }
+ );
+}
+
+export function useRepoInfo(ownerrepo?: string) {
+ return useSWR(ownerrepo ? `repo-info-${ownerrepo}` : undefined, async () => {
+ const [owner, repo] = ownerrepo!.split("/");
+
+ const res = await fetch(`/api/get-repo-info?owner=${owner}&repo=${repo}`);
+
+ const data = await res.json();
+
+ return getRepoInfoSchema.parse(data);
+ });
+}
diff --git a/components/ui/command.tsx b/components/ui/command.tsx
new file mode 100644
index 0000000..1a37e67
--- /dev/null
+++ b/components/ui/command.tsx
@@ -0,0 +1,155 @@
+"use client"
+
+import * as React from "react"
+import { type DialogProps } from "@radix-ui/react-dialog"
+import { Command as CommandPrimitive } from "cmdk"
+import { Search } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Dialog, DialogContent } from "@/components/ui/dialog"
+
+const Command = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Command.displayName = CommandPrimitive.displayName
+
+interface CommandDialogProps extends DialogProps {}
+
+const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+ return (
+
+ )
+}
+
+const CommandInput = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+
+CommandInput.displayName = CommandPrimitive.Input.displayName
+
+const CommandList = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandList.displayName = CommandPrimitive.List.displayName
+
+const CommandEmpty = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>((props, ref) => (
+
+))
+
+CommandEmpty.displayName = CommandPrimitive.Empty.displayName
+
+const CommandGroup = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandGroup.displayName = CommandPrimitive.Group.displayName
+
+const CommandSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+CommandSeparator.displayName = CommandPrimitive.Separator.displayName
+
+const CommandItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+
+CommandItem.displayName = CommandPrimitive.Item.displayName
+
+const CommandShortcut = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => {
+ return (
+
+ )
+}
+CommandShortcut.displayName = "CommandShortcut"
+
+export {
+ Command,
+ CommandDialog,
+ CommandInput,
+ CommandList,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandShortcut,
+ CommandSeparator,
+}
diff --git a/components/ui/popover.tsx b/components/ui/popover.tsx
new file mode 100644
index 0000000..a0ec48b
--- /dev/null
+++ b/components/ui/popover.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as PopoverPrimitive from "@radix-ui/react-popover"
+
+import { cn } from "@/lib/utils"
+
+const Popover = PopoverPrimitive.Root
+
+const PopoverTrigger = PopoverPrimitive.Trigger
+
+const PopoverContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
+
+
+
+))
+PopoverContent.displayName = PopoverPrimitive.Content.displayName
+
+export { Popover, PopoverTrigger, PopoverContent }
diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx
new file mode 100644
index 0000000..521b94b
--- /dev/null
+++ b/components/ui/toast.tsx
@@ -0,0 +1,129 @@
+"use client"
+
+import * as React from "react"
+import * as ToastPrimitives from "@radix-ui/react-toast"
+import { cva, type VariantProps } from "class-variance-authority"
+import { X } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const ToastProvider = ToastPrimitives.Provider
+
+const ToastViewport = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastViewport.displayName = ToastPrimitives.Viewport.displayName
+
+const toastVariants = cva(
+ "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
+ {
+ variants: {
+ variant: {
+ default: "border bg-background text-foreground",
+ destructive:
+ "destructive group border-destructive bg-destructive text-destructive-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Toast = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, ...props }, ref) => {
+ return (
+
+ )
+})
+Toast.displayName = ToastPrimitives.Root.displayName
+
+const ToastAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastAction.displayName = ToastPrimitives.Action.displayName
+
+const ToastClose = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+ToastClose.displayName = ToastPrimitives.Close.displayName
+
+const ToastTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastTitle.displayName = ToastPrimitives.Title.displayName
+
+const ToastDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+ToastDescription.displayName = ToastPrimitives.Description.displayName
+
+type ToastProps = React.ComponentPropsWithoutRef
+
+type ToastActionElement = React.ReactElement
+
+export {
+ type ToastProps,
+ type ToastActionElement,
+ ToastProvider,
+ ToastViewport,
+ Toast,
+ ToastTitle,
+ ToastDescription,
+ ToastClose,
+ ToastAction,
+}
diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx
new file mode 100644
index 0000000..e223385
--- /dev/null
+++ b/components/ui/toaster.tsx
@@ -0,0 +1,35 @@
+"use client"
+
+import {
+ Toast,
+ ToastClose,
+ ToastDescription,
+ ToastProvider,
+ ToastTitle,
+ ToastViewport,
+} from "@/components/ui/toast"
+import { useToast } from "@/components/ui/use-toast"
+
+export function Toaster() {
+ const { toasts } = useToast()
+
+ return (
+
+ {toasts.map(function ({ id, title, description, action, ...props }) {
+ return (
+
+
+ {title && {title}}
+ {description && (
+ {description}
+ )}
+
+ {action}
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/components/ui/use-toast.ts b/components/ui/use-toast.ts
new file mode 100644
index 0000000..02e111d
--- /dev/null
+++ b/components/ui/use-toast.ts
@@ -0,0 +1,194 @@
+"use client"
+
+// Inspired by react-hot-toast library
+import * as React from "react"
+
+import type {
+ ToastActionElement,
+ ToastProps,
+} from "@/components/ui/toast"
+
+const TOAST_LIMIT = 1
+const TOAST_REMOVE_DELAY = 1000000
+
+type ToasterToast = ToastProps & {
+ id: string
+ title?: React.ReactNode
+ description?: React.ReactNode
+ action?: ToastActionElement
+}
+
+const actionTypes = {
+ ADD_TOAST: "ADD_TOAST",
+ UPDATE_TOAST: "UPDATE_TOAST",
+ DISMISS_TOAST: "DISMISS_TOAST",
+ REMOVE_TOAST: "REMOVE_TOAST",
+} as const
+
+let count = 0
+
+function genId() {
+ count = (count + 1) % Number.MAX_SAFE_INTEGER
+ return count.toString()
+}
+
+type ActionType = typeof actionTypes
+
+type Action =
+ | {
+ type: ActionType["ADD_TOAST"]
+ toast: ToasterToast
+ }
+ | {
+ type: ActionType["UPDATE_TOAST"]
+ toast: Partial
+ }
+ | {
+ type: ActionType["DISMISS_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+ | {
+ type: ActionType["REMOVE_TOAST"]
+ toastId?: ToasterToast["id"]
+ }
+
+interface State {
+ toasts: ToasterToast[]
+}
+
+const toastTimeouts = new Map>()
+
+const addToRemoveQueue = (toastId: string) => {
+ if (toastTimeouts.has(toastId)) {
+ return
+ }
+
+ const timeout = setTimeout(() => {
+ toastTimeouts.delete(toastId)
+ dispatch({
+ type: "REMOVE_TOAST",
+ toastId: toastId,
+ })
+ }, TOAST_REMOVE_DELAY)
+
+ toastTimeouts.set(toastId, timeout)
+}
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case "ADD_TOAST":
+ return {
+ ...state,
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
+ }
+
+ case "UPDATE_TOAST":
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === action.toast.id ? { ...t, ...action.toast } : t
+ ),
+ }
+
+ case "DISMISS_TOAST": {
+ const { toastId } = action
+
+ // ! Side effects ! - This could be extracted into a dismissToast() action,
+ // but I'll keep it here for simplicity
+ if (toastId) {
+ addToRemoveQueue(toastId)
+ } else {
+ state.toasts.forEach((toast) => {
+ addToRemoveQueue(toast.id)
+ })
+ }
+
+ return {
+ ...state,
+ toasts: state.toasts.map((t) =>
+ t.id === toastId || toastId === undefined
+ ? {
+ ...t,
+ open: false,
+ }
+ : t
+ ),
+ }
+ }
+ case "REMOVE_TOAST":
+ if (action.toastId === undefined) {
+ return {
+ ...state,
+ toasts: [],
+ }
+ }
+ return {
+ ...state,
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
+ }
+ }
+}
+
+const listeners: Array<(state: State) => void> = []
+
+let memoryState: State = { toasts: [] }
+
+function dispatch(action: Action) {
+ memoryState = reducer(memoryState, action)
+ listeners.forEach((listener) => {
+ listener(memoryState)
+ })
+}
+
+type Toast = Omit
+
+function toast({ ...props }: Toast) {
+ const id = genId()
+
+ const update = (props: ToasterToast) =>
+ dispatch({
+ type: "UPDATE_TOAST",
+ toast: { ...props, id },
+ })
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
+
+ dispatch({
+ type: "ADD_TOAST",
+ toast: {
+ ...props,
+ id,
+ open: true,
+ onOpenChange: (open) => {
+ if (!open) dismiss()
+ },
+ },
+ })
+
+ return {
+ id: id,
+ dismiss,
+ update,
+ }
+}
+
+function useToast() {
+ const [state, setState] = React.useState(memoryState)
+
+ React.useEffect(() => {
+ listeners.push(setState)
+ return () => {
+ const index = listeners.indexOf(setState)
+ if (index > -1) {
+ listeners.splice(index, 1)
+ }
+ }
+ }, [state])
+
+ return {
+ ...state,
+ toast,
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
+ }
+}
+
+export { useToast, toast }
diff --git a/package-lock.json b/package-lock.json
index 2843c0a..d8f5cb0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,9 +20,11 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
+ "@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@replit/codemirror-lang-csharp": "^6.2.0",
@@ -32,6 +34,7 @@
"aelf-sdk": "^3.4.14-beta.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"dexie": "^4.0.8",
"dexie-react-hooks": "^1.1.7",
@@ -43,9 +46,12 @@
"next-mdx-remote": "^5.0.0",
"next-themes": "^0.3.0",
"octokit": "^4.0.2",
+ "parse-github-url": "^1.0.3",
"react": "^18",
+ "react-accessible-treeview": "^2.9.1",
"react-dom": "^18",
"react-hook-form": "^7.52.1",
+ "react-icons": "^5.2.1",
"react-resizable-panels": "^2.0.20",
"react-terminal": "^1.4.4",
"reflect-metadata": "^0.2.2",
@@ -63,6 +69,7 @@
},
"devDependencies": {
"@types/node": "^20",
+ "@types/parse-github-url": "^1.0.3",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^10.0.0",
@@ -1758,6 +1765,43 @@
}
}
},
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz",
+ "integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-dismissable-layer": "1.1.0",
+ "@radix-ui/react-focus-guards": "1.1.0",
+ "@radix-ui/react-focus-scope": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.0",
+ "@radix-ui/react-portal": "1.1.1",
+ "@radix-ui/react-presence": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-slot": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.5.7"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-popper": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
@@ -1984,6 +2028,40 @@
}
}
},
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.1.tgz",
+ "integrity": "sha512-5trl7piMXcZiCq7MW6r8YYmu0bK5qDpTWz+FdEPdKyft2UixkspheYbjbrLXVN5NGKHFbOP7lm8eD0biiSqZqg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-collection": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-dismissable-layer": "1.1.0",
+ "@radix-ui/react-portal": "1.1.1",
+ "@radix-ui/react-presence": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-visually-hidden": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-toggle": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz",
@@ -2399,6 +2477,16 @@
"undici-types": "~5.26.4"
}
},
+ "node_modules/@types/parse-github-url": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/parse-github-url/-/parse-github-url-1.0.3.tgz",
+ "integrity": "sha512-7sTbCVmSVzK/iAsHGIxoqiyAnqix9opZm68lOvaU6DBx9EQ9kHMSp0y7Criu2OCsZ9wDllEyCRU+LU4hPRxXUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/prop-types": {
"version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
@@ -3480,6 +3568,13 @@
"node": ">=6"
}
},
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -3495,6 +3590,384 @@
"node": ">=6"
}
},
+ "node_modules/cmdk": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz",
+ "integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-dialog": "1.0.5",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/primitive": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
+ "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
+ "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-context": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
+ "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-dialog": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz",
+ "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-context": "1.0.1",
+ "@radix-ui/react-dismissable-layer": "1.0.5",
+ "@radix-ui/react-focus-guards": "1.0.1",
+ "@radix-ui/react-focus-scope": "1.0.4",
+ "@radix-ui/react-id": "1.0.1",
+ "@radix-ui/react-portal": "1.0.4",
+ "@radix-ui/react-presence": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-slot": "1.0.2",
+ "@radix-ui/react-use-controllable-state": "1.0.1",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.5.5"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
+ "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/primitive": "1.0.1",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-callback-ref": "1.0.1",
+ "@radix-ui/react-use-escape-keydown": "1.0.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
+ "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
+ "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-primitive": "1.0.3",
+ "@radix-ui/react-use-callback-ref": "1.0.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-id": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
+ "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-use-layout-effect": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-portal": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
+ "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-presence": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
+ "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-compose-refs": "1.0.1",
+ "@radix-ui/react-use-layout-effect": "1.0.1"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-primitive": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
+ "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-slot": "1.0.2"
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-slot": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
+ "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-compose-refs": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
+ "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
+ "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-use-callback-ref": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz",
+ "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-use-callback-ref": "1.0.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
+ "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/cmdk/node_modules/react-remove-scroll": {
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
+ "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.3",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/codemirror": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
@@ -7593,6 +8066,18 @@
"integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==",
"license": "MIT"
},
+ "node_modules/parse-github-url": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.3.tgz",
+ "integrity": "sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==",
+ "license": "MIT",
+ "bin": {
+ "parse-github-url": "cli.js"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
"node_modules/parse-numeric-range": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz",
@@ -7973,6 +8458,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-accessible-treeview": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/react-accessible-treeview/-/react-accessible-treeview-2.9.1.tgz",
+ "integrity": "sha512-UlmeFJtlh1SryN8kHLf4ICLlF4HeIpiacWwHh+7AzhmOmOs9vObXAjJWTiLpxP342TQ1QbCdSGq086Y8oD7NSw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "classnames": "^2.2.6",
+ "prop-types": "^15.7.2",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-device-detect": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.1.2.tgz",
@@ -8015,6 +8512,15 @@
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
+ "node_modules/react-icons": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
+ "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/package.json b/package.json
index 749246d..c1827dc 100644
--- a/package.json
+++ b/package.json
@@ -21,9 +21,11 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-menubar": "^1.1.1",
+ "@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
+ "@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@replit/codemirror-lang-csharp": "^6.2.0",
@@ -33,6 +35,7 @@
"aelf-sdk": "^3.4.14-beta.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
+ "cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"dexie": "^4.0.8",
"dexie-react-hooks": "^1.1.7",
@@ -44,9 +47,12 @@
"next-mdx-remote": "^5.0.0",
"next-themes": "^0.3.0",
"octokit": "^4.0.2",
+ "parse-github-url": "^1.0.3",
"react": "^18",
+ "react-accessible-treeview": "^2.9.1",
"react-dom": "^18",
"react-hook-form": "^7.52.1",
+ "react-icons": "^5.2.1",
"react-resizable-panels": "^2.0.20",
"react-terminal": "^1.4.4",
"reflect-metadata": "^0.2.2",
@@ -64,6 +70,7 @@
},
"devDependencies": {
"@types/node": "^20",
+ "@types/parse-github-url": "^1.0.3",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/uuid": "^10.0.0",