diff --git a/.env.example b/.env.example index f16f77609fe..37714174998 100644 --- a/.env.example +++ b/.env.example @@ -91,5 +91,34 @@ STARKNET_ADDRESS= STARKNET_PRIVATE_KEY= STARKNET_RPC_URL= + +# Farcaster +FARCASTER_HUB_URL= +FARCASTER_FID= +FARCASTER_PRIVATE_KEY= + +# Coinbase +COINBASE_COMMERCE_KEY= # from coinbase developer portal +COINBASE_API_KEY= # from coinbase developer portal +COINBASE_PRIVATE_KEY= # from coinbase developer portal +# if not configured it will be generated and written to runtime.character.settings.secrets.COINBASE_GENERATED_WALLET_ID and runtime.character.settings.secrets.COINBASE_GENERATED_WALLET_HEX_SEED +COINBASE_GENERATED_WALLET_ID= # not your address but the wallet id from generating a wallet through the plugin +COINBASE_GENERATED_WALLET_HEX_SEED= # not your address but the wallet hex seed from generating a wallet through the plugin and calling export + +# Conflux Configuration +CONFLUX_CORE_PRIVATE_KEY= +CONFLUX_CORE_SPACE_RPC_URL= +CONFLUX_ESPACE_PRIVATE_KEY= +CONFLUX_ESPACE_RPC_URL= +CONFLUX_MEME_CONTRACT_ADDRESS= + +#ZeroG +ZEROG_INDEXER_RPC= +ZEROG_EVM_RPC= +ZEROG_PRIVATE_KEY= +ZEROG_FLOW_ADDRESS= + + # Coinbase Commerce COINBASE_COMMERCE_KEY= + diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b9947abf7bf..b86208b8f87 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,8 +33,8 @@ jobs: echo "TEST_DATABASE_CLIENT=sqlite" > packages/core/.env.test echo "NODE_ENV=test" >> packages/core/.env.test - # - name: Run tests - # run: cd packages/core && pnpm test // YOLO FOR NOW + - name: Run tests + run: cd packages/core && pnpm test - name: Build packages run: pnpm run build diff --git a/.gitignore b/.gitignore index fdecd8b95f2..c1bf4fc6dc1 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ characters/ packages/core/src/providers/cache packages/core/src/providers/cache/* cache/* +packages/plugin-coinbase/src/plugins/transactions.csv +packages/plugin-coinbase/package-lock.json diff --git a/.husky/commit-msg b/.husky/commit-msg deleted file mode 100755 index 88770231272..00000000000 --- a/.husky/commit-msg +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env sh - -# Inform the user about commit message format requirements -echo "┌──────────────────────────────────────────────────────────────┐" -echo "│ ℹ️ Commit message must follow the format: 'type: description' │" -echo "│ Valid types: feat, fix, docs, style, refactor, test, chore │" -echo "│ Example: 'feat: add new login feature' │" -echo "└──────────────────────────────────────────────────────────────┘" -echo "" - -# Run commitlint to validate the commit message -npx --no -- commitlint --edit ${1} diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index afb45bee423..00000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,2 +0,0 @@ -pnpm run prettier-check -pnpm run lint \ No newline at end of file diff --git a/agent/package.json b/agent/package.json index 2eb890ba8a5..f9756b5d435 100644 --- a/agent/package.json +++ b/agent/package.json @@ -21,17 +21,19 @@ "@ai16z/client-twitter": "workspace:*", "@ai16z/eliza": "workspace:*", "@ai16z/plugin-bootstrap": "workspace:*", + "@ai16z/plugin-conflux": "workspace:*", "@ai16z/plugin-image-generation": "workspace:*", "@ai16z/plugin-node": "workspace:*", "@ai16z/plugin-solana": "workspace:*", + "@ai16z/plugin-0g": "workspace:*", "@ai16z/plugin-starknet": "workspace:*", "@ai16z/plugin-coinbase": "workspace:*", - "readline": "^1.3.0", - "ws": "^8.18.0", + "readline": "1.3.0", + "ws": "8.18.0", "yargs": "17.7.2" }, "devDependencies": { "ts-node": "10.9.2", - "tsup": "^8.3.5" + "tsup": "8.3.5" } } diff --git a/agent/src/index.ts b/agent/src/index.ts index 8f000f18a7a..9d657f80ee8 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -23,9 +23,14 @@ import { validateCharacterConfig, } from "@ai16z/eliza"; import { bootstrapPlugin } from "@ai16z/plugin-bootstrap"; +import { confluxPlugin } from "@ai16z/plugin-conflux"; import { solanaPlugin } from "@ai16z/plugin-solana"; +import { zgPlugin } from "@ai16z/plugin-0g"; import { nodePlugin } from "@ai16z/plugin-node"; -import { coinbaseCommercePlugin } from "@ai16z/plugin-coinbase"; +import { + coinbaseCommercePlugin, + coinbaseMassPaymentsPlugin, +} from "@ai16z/plugin-coinbase"; import Database from "better-sqlite3"; import fs from "fs"; import readline from "readline"; @@ -229,6 +234,10 @@ export async function initializeClients( return clients; } +function getSecret(character: Character, secret: string) { + return character.settings.secrets?.[secret] || process.env[secret]; +} + export function createAgent( character: Character, db: IDatabaseAdapter, @@ -248,12 +257,19 @@ export function createAgent( character, plugins: [ bootstrapPlugin, + getSecret(character, "CONFLUX_CORE_PRIVATE_KEY") + ? confluxPlugin + : null, nodePlugin, - character.settings.secrets?.WALLET_PUBLIC_KEY ? solanaPlugin : null, - character.settings.secrets?.COINBASE_COMMERCE_KEY || - process.env.COINBASE_COMMERCE_KEY + getSecret(character, "WALLET_PUBLIC_KEY") ? solanaPlugin : null, + getSecret(character, "ZEROG_PRIVATE_KEY") ? zgPlugin : null, + getSecret(character, "COINBASE_COMMERCE_KEY") ? coinbaseCommercePlugin : null, + getSecret(character, "COINBASE_API_KEY") && + getSecret(character, "COINBASE_PRIVATE_KEY") + ? coinbaseMassPaymentsPlugin + : null, ].filter(Boolean), providers: [], actions: [], diff --git a/client/README.md b/client/README.md deleted file mode 100644 index b6897e3f0a9..00000000000 --- a/client/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: - -- Configure the top-level `parserOptions` property like this: - -```js -export default tseslint.config({ - languageOptions: { - // other options... - parserOptions: { - project: ["./tsconfig.node.json", "./tsconfig.app.json"], - tsconfigRootDir: import.meta.dirname, - }, - }, -}); -``` - -- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` -- Optionally add `...tseslint.configs.stylisticTypeChecked` -- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: - -```js -// eslint.config.js -import react from "eslint-plugin-react"; - -export default tseslint.config({ - // Set the react version - settings: { react: { version: "18.3" } }, - plugins: { - // Add the react plugin - react, - }, - rules: { - // other rules... - // Enable its recommended rules - ...react.configs.recommended.rules, - ...react.configs["jsx-runtime"].rules, - }, -}); -``` diff --git a/client/index.html b/client/index.html index e0ef3be8332..342f8872933 100644 --- a/client/index.html +++ b/client/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Eliza
diff --git a/client/package.json b/client/package.json index aad8c3c91f0..3c467263ece 100644 --- a/client/package.json +++ b/client/package.json @@ -1,5 +1,5 @@ { - "name": "eliza", + "name": "eliza-client", "private": true, "version": "0.0.0", "type": "module", @@ -11,32 +11,37 @@ }, "dependencies": { "@ai16z/eliza": "workspace:*", - "@radix-ui/react-slot": "^1.1.0", - "class-variance-authority": "^0.7.0", + "@radix-ui/react-dialog": "1.1.2", + "@radix-ui/react-separator": "1.1.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-tooltip": "1.1.4", + "@tanstack/react-query": "5.61.0", + "class-variance-authority": "0.7.0", "clsx": "2.1.0", - "lucide-react": "^0.460.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "tailwind-merge": "^2.5.4", - "tailwindcss-animate": "^1.0.7", - "vite-plugin-top-level-await": "^1.4.4", - "vite-plugin-wasm": "^3.3.0" + "lucide-react": "0.460.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-router-dom": "6.22.1", + "tailwind-merge": "2.5.4", + "tailwindcss-animate": "1.0.7", + "vite-plugin-top-level-await": "1.4.4", + "vite-plugin-wasm": "3.3.0" }, "devDependencies": { - "@eslint/js": "^9.13.0", + "@eslint/js": "9.15.0", "@types/node": "22.8.4", "@types/react": "18.3.12", "@types/react-dom": "18.3.1", - "@vitejs/plugin-react": "^4.3.3", - "autoprefixer": "^10.4.20", - "eslint": "^9.13.0", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.14", - "globals": "^15.11.0", - "postcss": "^8.4.49", - "tailwindcss": "^3.4.15", + "@vitejs/plugin-react": "4.3.3", + "autoprefixer": "10.4.20", + "eslint": "9.13.0", + "eslint-plugin-react-hooks": "5.0.0", + "eslint-plugin-react-refresh": "0.4.14", + "globals": "15.11.0", + "postcss": "8.4.49", + "tailwindcss": "3.4.15", "typescript": "~5.6.2", - "typescript-eslint": "^8.11.0", - "vite": "^5.4.10" + "typescript-eslint": "8.11.0", + "vite": "link:@tanstack/router-plugin/vite" } } diff --git a/client/src/Agent.tsx b/client/src/Agent.tsx new file mode 100644 index 00000000000..f3094f14ebb --- /dev/null +++ b/client/src/Agent.tsx @@ -0,0 +1,10 @@ +export default function Agent() { + return ( +
+

+ Select an option from the sidebar to configure, view, or chat + with your ELIZA agent +

+
+ ); +} diff --git a/client/src/Agents.tsx b/client/src/Agents.tsx new file mode 100644 index 00000000000..06e2c56b495 --- /dev/null +++ b/client/src/Agents.tsx @@ -0,0 +1,47 @@ +import { useQuery } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { useNavigate } from "react-router-dom"; +import "./App.css"; + +type Agent = { + id: string; + name: string; +}; + +function Agents() { + const navigate = useNavigate(); + const { data: agents, isLoading } = useQuery({ + queryKey: ["agents"], + queryFn: async () => { + const res = await fetch("/api/agents"); + const data = await res.json(); + return data.agents as Agent[]; + }, + }); + + return ( +
+

Select your agent:

+ + {isLoading ? ( +
Loading agents...
+ ) : ( +
+ {agents?.map((agent) => ( + + ))} +
+ )} +
+ ); +} + +export default Agents; diff --git a/client/src/App.css b/client/src/App.css index f44fb79ad33..d6055f0d020 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,7 +1,6 @@ #root { max-width: 1280px; margin: 0 auto; - padding: 2rem; text-align: center; } diff --git a/client/src/App.tsx b/client/src/App.tsx index f48537f0cbb..c5b0826f12e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,71 +1,10 @@ -import { useState } from "react"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; import "./App.css"; -import { stringToUuid } from "@ai16z/eliza"; - -type TextResponse = { - text: string; - user: string; -}; +import Agents from "./Agents"; function App() { - const [input, setInput] = useState(""); - const [response, setResponse] = useState([]); - const [loading, setLoading] = useState(false); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - - try { - const res = await fetch(`/api/${stringToUuid("Eliza")}/message`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - text: input, - userId: "user", - roomId: `default-room-${stringToUuid("Eliza")}`, - }), - }); - - const data: TextResponse[] = await res.json(); - - console.log(data); - setResponse(data); - setInput(""); - } catch (error) { - console.error("Error:", error); - setResponse([{ text: "An error occurred", user: "system" }]); - } finally { - setLoading(false); - } - }; - return (
-

Chat with Eliza

-
- setInput(e.target.value)} - placeholder="Enter your message..." - className="w-full" - /> - -
- - {(loading || response) && ( -
- {response.map((r) => ( -

{r.text}

- ))} -
- )} +
); } diff --git a/client/src/Character.tsx b/client/src/Character.tsx new file mode 100644 index 00000000000..bdb53882adf --- /dev/null +++ b/client/src/Character.tsx @@ -0,0 +1,7 @@ +export default function Character() { + return ( +
+

WIP

+
+ ); +} diff --git a/client/src/Chat.tsx b/client/src/Chat.tsx new file mode 100644 index 00000000000..b32cc0b83ed --- /dev/null +++ b/client/src/Chat.tsx @@ -0,0 +1,104 @@ +import { useState } from "react"; +import { useParams } from "react-router-dom"; +import { useMutation } from "@tanstack/react-query"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import "./App.css"; + +type TextResponse = { + text: string; + user: string; +}; + +export default function Chat() { + const { agentId } = useParams(); + const [input, setInput] = useState(""); + const [messages, setMessages] = useState([]); + + const mutation = useMutation({ + mutationFn: async (text: string) => { + const res = await fetch(`/api/${agentId}/message`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text, + userId: "user", + roomId: `default-room-${agentId}`, + }), + }); + return res.json() as Promise; + }, + onSuccess: (data) => { + setMessages((prev) => [...prev, ...data]); + }, + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim()) return; + + // Add user message immediately to state + const userMessage: TextResponse = { + text: input, + user: "user", + }; + setMessages((prev) => [...prev, userMessage]); + + mutation.mutate(input); + setInput(""); + }; + + return ( +
+
+
+ {messages.length > 0 ? ( + messages.map((message, index) => ( +
+
+ {message.text} +
+
+ )) + ) : ( +
+ No messages yet. Start a conversation! +
+ )} +
+
+ +
+
+
+ setInput(e.target.value)} + placeholder="Type a message..." + className="flex-1" + disabled={mutation.isPending} + /> + +
+
+
+
+ ); +} diff --git a/client/src/Layout.tsx b/client/src/Layout.tsx new file mode 100644 index 00000000000..70c79f74032 --- /dev/null +++ b/client/src/Layout.tsx @@ -0,0 +1,12 @@ +import { SidebarProvider } from "@/components/ui/sidebar"; +import { AppSidebar } from "@/components/app-sidebar"; +import { Outlet } from "react-router-dom"; + +export default function Layout() { + return ( + + + + + ); +} diff --git a/client/src/components/app-sidebar.tsx b/client/src/components/app-sidebar.tsx new file mode 100644 index 00000000000..5245ad8febd --- /dev/null +++ b/client/src/components/app-sidebar.tsx @@ -0,0 +1,56 @@ +import { Calendar, Home, Inbox, Search, Settings } from "lucide-react"; +import { useParams } from "react-router-dom"; + +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarTrigger, +} from "@/components/ui/sidebar"; + +// Menu items. +const items = [ + { + title: "Chat", + url: "chat", + icon: Inbox, + }, + { + title: "Character Overview", + url: "character", + icon: Calendar, + }, +]; + +export function AppSidebar() { + const { agentId } = useParams(); + + return ( + + + + Application + + + {items.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + + ); +} diff --git a/client/src/components/ui/separator.tsx b/client/src/components/ui/separator.tsx new file mode 100644 index 00000000000..2af4ec891eb --- /dev/null +++ b/client/src/components/ui/separator.tsx @@ -0,0 +1,33 @@ +"use client"; + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "@/lib/utils"; + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/client/src/components/ui/sheet.tsx b/client/src/components/ui/sheet.tsx new file mode 100644 index 00000000000..e18e295c73c --- /dev/null +++ b/client/src/components/ui/sheet.tsx @@ -0,0 +1,136 @@ +import * as React from "react"; +import * as SheetPrimitive from "@radix-ui/react-dialog"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Sheet = SheetPrimitive.Root; + +const SheetTrigger = SheetPrimitive.Trigger; + +const SheetClose = SheetPrimitive.Close; + +const SheetPortal = SheetPrimitive.Portal; + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + } +); + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + + + Close + + {children} + + +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetHeader.displayName = "SheetHeader"; + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetFooter.displayName = "SheetFooter"; + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +}; diff --git a/client/src/components/ui/sidebar.tsx b/client/src/components/ui/sidebar.tsx new file mode 100644 index 00000000000..ab5862ab35a --- /dev/null +++ b/client/src/components/ui/sidebar.tsx @@ -0,0 +1,786 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { VariantProps, cva } from "class-variance-authority"; +import { PanelLeft } from "lucide-react"; + +import { useIsMobile } from "@/hooks/use-mobile"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Separator } from "@/components/ui/separator"; +import { Sheet, SheetContent } from "@/components/ui/sheet"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +const SIDEBAR_COOKIE_NAME = "sidebar:state"; +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +const SIDEBAR_WIDTH = "16rem"; +const SIDEBAR_WIDTH_MOBILE = "18rem"; +const SIDEBAR_WIDTH_ICON = "3rem"; +const SIDEBAR_KEYBOARD_SHORTCUT = "b"; + +type SidebarContext = { + state: "expanded" | "collapsed"; + open: boolean; + setOpen: (open: boolean) => void; + openMobile: boolean; + setOpenMobile: (open: boolean) => void; + isMobile: boolean; + toggleSidebar: () => void; +}; + +const SidebarContext = React.createContext(null); + +function useSidebar() { + const context = React.useContext(SidebarContext); + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider."); + } + + return context; +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + defaultOpen?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref + ) => { + const isMobile = useIsMobile(); + const [openMobile, setOpenMobile] = React.useState(false); + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen); + const open = openProp ?? _open; + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = + typeof value === "function" ? value(open) : value; + if (setOpenProp) { + setOpenProp(openState); + } else { + _setOpen(openState); + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; + }, + [setOpenProp, open] + ); + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile + ? setOpenMobile((open) => !open) + : setOpen((open) => !open); + }, [isMobile, setOpen, setOpenMobile]); + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault(); + toggleSidebar(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [toggleSidebar]); + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed"; + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + ] + ); + + return ( + + +
+ {children} +
+
+
+ ); + } +); +SidebarProvider.displayName = "SidebarProvider"; + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + side?: "left" | "right"; + variant?: "sidebar" | "floating" | "inset"; + collapsible?: "offcanvas" | "icon" | "none"; + } +>( + ( + { + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props + }, + ref + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); + + if (collapsible === "none") { + return ( +
+ {children} +
+ ); + } + + if (isMobile) { + return ( + + +
+ {children} +
+
+
+ ); + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ); + } +); +Sidebar.displayName = "Sidebar"; + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( + + ); +}); +SidebarTrigger.displayName = "SidebarTrigger"; + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar(); + + return ( +