initial commit in remote repository

Signed-off-by: Uncle Fatso <uncle.fatso@ghostchain.io>
This commit is contained in:
Uncle Fatso 2025-07-22 13:53:22 +03:00
commit 6906ca83b7
Signed by: f4ts0
GPG Key ID: 565F4F2860226EBB
154 changed files with 47407 additions and 0 deletions

8
.eslintignore Normal file
View File

@ -0,0 +1,8 @@
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
dist
# don't lint .cache
.cache
webpack.*

25
.eslintrc.cjs Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
parserOptions: {
tsconfigRootDir: __dirname,
project: ["./tsconfig.json"],
},
settings: { react: { version: "detect" } },
extends: ["react-app", "prettier"],
rules: {
"import/no-extraneous-dependencies": [
"error",
{
devDependencies: ["**/*.test.ts", "**/*.spec.ts", "**/*.bench.ts"],
},
],
"@typescript-eslint/no-redeclare": "off",
},
env: {
browser: true,
},
globals: {
chrome: true,
},
}

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
/tests
# production
/build
/dist
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/public/manifest.json
test-results
playwright-report
# NOTE: removed temporarily
/tests

74
README.md Normal file
View File

@ -0,0 +1,74 @@
# GHOST Wallet - Light Client Wallet
![GHOST Wallet Logo](./assets/img/ghostWallet-Featured-Image.png)
## Overview
Welcome to GHOST Wallet - the ultimate light client wallet! GHOST Wallet leverages the power of a light client that implements the JSON RPC to provide a highly resilient, efficient, and user-friendly wallet experience. This extension will be able to instantly connect any DApp to GHOST and CASPER chains without the hassle of waiting for synchronization.
## Getting Started
### Download
You can download pre-build extension for browser of your choice [here](https://google.com).
- **Chromium-based Browsers** - Use [official guide](https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world?#load-unpacked) to install
- **Firefox-based Browsers** - To install extension temporary check official [guide here](https://brave.com/)
__**NOTE: if you are using [Brave Browser](https://brave.com/) go to `Options` right after the installation and follow the instructions**__
### Prerequisites
- **pnpm** - [Fast, disk space efficient package manager](https://pnpm.io/)
### Installation
Clone the repository and install dependencies:
```bash
pnpm install
# to build chrome extension
pnpm build:chrome
# to build firefox extension
pnpm build:firefox
```
### Running the Wallet
Start the development server:
```bash
pnpm dev
```
In another terminal:
```bash
pnpm start
```
This will open the extension in a browser window.
## How it Works
GHOST Wallet runs a single light client instance inside of the user's browser.
When a dapp connects to GHOST Wallet, its connection to the blockchain is
forwarded to that light client instance. Inside the extension, we use low level
API libraries such as the `observable-client`, `substrate-client`, and `json-rpc-provider` to
maintain a connection to light client.
If light client were to crash, GHOST Wallet will automatically re-connect.
However, from the dapp perspective, you will be disconnected and it will be your
responsibility to re-connect back to GHOST Wallet.
## Giving Feedback
If you have trouble integrating this wallet template or you have questions,
please open an issue.
## Acknowledgments
- [Substrate](https://docs.substrate.io/) - The blockchain framework that powers this extension.
- [Smoldot](https://github.com/smol-dot/smoldot) - The light client used for connecting to blockchain.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
assets/icons/icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
assets/icons/icon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

BIN
assets/icons/icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/icons/icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

View File

@ -0,0 +1,44 @@
{
"author": "Ghost Team <someone@ghostchain.io>",
"description": "Wallet for Ghost and Casper blockchains light clients",
"homepage_url": "https://github.com/ghostchain/ghost-wallet",
"name": "Ghost Wallet",
"short_name": "ghost-wallet",
"version": "0.0.0",
"manifest_version": 3,
"permissions": ["notifications", "storage", "tabs", "alarms"],
"background": {
"service_worker": "background/background.js",
"type": "module"
},
"action": {
"default_title": "Ghost Wallet",
"default_popup": "ui/assets/wallet-popup.html"
},
"options_ui": {
"page": "ui/assets/options.html",
"open_in_tab": true
},
"content_scripts": [
{
"js": ["content/content.js"],
"matches": ["http://*/*", "https://*/*"],
"run_at": "document_start"
}
],
"icons": {
"16": "./icons/icon-16.png",
"32": "./icons/icon-32.png",
"48": "./icons/icon-48.png",
"128": "./icons/icon-128.png"
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"web_accessible_resources": [
{
"resources": ["inpage/inpage.js"],
"matches": ["http://*/*", "https://*/*"]
}
]
}

View File

@ -0,0 +1,49 @@
{
"author": "Ghost Team <someone@ghostchain.io>",
"description": "Wallet for Ghost and Casper blockchains light clients",
"homepage_url": "https://github.com/ghostchain/ghost-wallet",
"name": "Ghost Wallet",
"short_name": "ghost-wallet",
"version": "0.0.0",
"manifest_version": 3,
"permissions": ["notifications", "storage", "tabs", "alarms"],
"background": {
"scripts": ["background/background.js"],
"type": "module"
},
"action": {
"default_title": "Ghost Wallet",
"default_popup": "ui/assets/wallet-popup.html"
},
"options_ui": {
"page": "ui/assets/options.html",
"open_in_tab": true
},
"content_scripts": [
{
"js": ["content/content.js"],
"matches": ["http://*/*", "https://*/*"],
"run_at": "document_start"
}
],
"icons": {
"16": "./icons/icon-16.png",
"32": "./icons/icon-32.png",
"48": "./icons/icon-48.png",
"128": "./icons/icon-128.png"
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"browser_specific_settings": {
"gecko": {
"id": "{9b4d20ed-b18a-4237-b5d0-ca71c2ce2060}"
}
},
"web_accessible_resources": [
{
"resources": ["inpage/inpage.js"],
"matches": ["http://*/*", "https://*/*"]
}
]
}

11
assets/options.html Normal file
View File

@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<title>Ghost Wallet Options</title>
<meta charset="utf-8" />
</head>
<body style="background-color: #f9f9f9">
<div id="options"></div>
<script type="module" src="../src/open-options.tsx"></script>
</body>
</html>

11
assets/wallet-popup.html Normal file
View File

@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<title>Ghost Wallet</title>
<meta charset="utf-8" />
</head>
<body>
<div id="popup"></div>
<script type="module" src="../src/wallet-popup.tsx"></script>
</body>
</html>

17
components.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.json",
"css": "src/style.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

20679
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

170
package.json Normal file
View File

@ -0,0 +1,170 @@
{
"name": "ghost-chain-extension",
"version": "0.0.26",
"description": "Browser extension to manage ghost blockchain light clients.",
"main": "dist/src/index.js",
"author": "Uncle f4ts0 <f4ts0@ghostchain.io>",
"maintainers": [
"Uncle f4ts0 <fatso@ghostchain.io>",
"Uncle 57r3tch <stretch@ghostchain.io>",
"Uncle 5t1nky <stinky@ghostchain.io>"
],
"type": "module",
"private": true,
"keywords": [],
"scripts": {
"prep": "mkdir dist && cp -r ./assets/icons ./dist && cp -r ./assets/chainspecs ./dist",
"prep:chrome": "pnpm prep && node scripts/generateManifest.js ./assets/manifest-v3-chrome.json ./dist/manifest.json",
"prep:firefox": "pnpm prep && node scripts/generateManifest.js ./assets/manifest-v3-firefox.json ./dist/manifest.json",
"build": "tsc --noEmit && pnpm build:chrome && pnpm build:firefox",
"build:chrome": "pnpm clean && pnpm prep:chrome && concurrently \"pnpm:build-*\" && node scripts/checkExtensionScriptSizes.js",
"build:firefox": "pnpm clean && pnpm prep:firefox && concurrently \"pnpm:build-*\" && node scripts/checkExtensionScriptSizes.js",
"build-content": "set INPUT=content && pnpm exec vite build --config vite.script.config.js",
"build-background": "set INPUT=background && pnpm exec vite build --config vite.script.config.js",
"build-inpage": "set INPUT=inpage && pnpm exec vite build --config vite.script.config.js",
"build-ui": "vite build --config vite.ui.config.js",
"dev": "pnpm dev:chrome",
"dev:chrome": "pnpm clean && pnpm prep:chrome && concurrently \"pnpm:dev-*\"",
"dev:firefox": "pnpm clean && pnpm prep:firefox && concurrently \"pnpm:dev-*\"",
"dev-content": "pnpm build-content --mode development --watch",
"dev-background": "pnpm build-background --mode development --watch",
"dev-inpage": "pnpm build-inpage --mode development --watch",
"dev-ui": "pnpm build-ui --mode development --watch",
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
"test": "vitest run",
"start": "web-ext run --source-dir ./dist -t chromium",
"start:firefox": "web-ext run --source-dir ./dist -t firefox-desktop",
"clean": "rm -rf dist",
"deep-clean": "pnpm clean && rm -rf node_modules",
"playwright": "playwright test --ui",
"playwright:install": "playwright install --with-deps chromium",
"playwright:chromium": "playwright test --project=chromium"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,md}": "prettier --write"
},
"devDependencies": {
"@changesets/cli": "^2.27.9",
"@playwright/test": "^1.48.2",
"@total-typescript/tsconfig": "^1.0.4",
"@types/chrome": "^0.0.270",
"@types/node": "^20.14.10",
"@types/qrcode.react": "^1.0.5",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"@types/react-router-dom": "^5.3.3",
"@typescript-eslint/parser": "^7.11.0",
"@vitejs/plugin-react-swc": "^3.7.2",
"@webext-core/storage": "^1.2.0",
"autoprefixer": "^10.4.19",
"concurrently": "^9.1.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-import": "^2.29.0",
"globals": "^16.0.0",
"http-server": "^14.1.1",
"husky": "^9.1.6",
"jsdom": "^26.0.0",
"lint-staged": "^15.4.3",
"nodemon": "^3.1.4",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"tailwindcss": "^3.4.17",
"tshy": "^3.0.2",
"tslib": "^2.6.2",
"tsup": "^8.3.0",
"turbo": "^2.2.3",
"typedoc": "^0.26.11",
"typescript": "5.6.2",
"vite": "^6.1.6",
"vite-plugin-web-extension": "^4.4.3",
"vite-tsconfig-paths": "^5.0.1",
"vitest": "^2.1.9",
"web-ext": "^8.2.0",
"wxt": "^0.17.12"
},
"dependencies": {
"@headlessui/react": "^2.1.10",
"@hookform/resolvers": "^3.9.1",
"@noble/ciphers": "^1.0.0",
"@noble/hashes": "^1.5.0",
"@polkadot-api/codegen": "~0.13.2",
"@polkadot-api/json-rpc-provider": "~0.0.4",
"@polkadot-api/metadata-builders": "~0.13.0",
"@polkadot-api/observable-client": "~0.8.6",
"@polkadot-api/polkadot-signer": "~0.1.6",
"@polkadot-api/signer": "~0.1.15",
"@polkadot-api/substrate-bindings": "~0.15.0",
"@polkadot-api/substrate-client": "~0.3.0",
"@polkadot-api/utils": "~0.1.2",
"@polkadot-api/view-builder": "~0.4.3",
"@polkadot-labs/hdkd": "^0.0.11",
"@polkadot-labs/hdkd-helpers": "^0.0.11",
"@polkadot-labs/schnorrkel-wasm": "^0.0.7",
"@polkadot/extension-inject": "^0.58.4",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@react-rxjs/core": "^0.10.7",
"@react-rxjs/utils": "^0.9.7",
"@substrate/connect-discovery": "^0.2.2",
"@substrate/discovery": "^0.2.2",
"@substrate/light-client-extension-helpers": "^2.7.6",
"@substrate/smoldot-discovery-connector": "^0.3.11",
"@zag-js/clipboard": "^0.47.0",
"@zag-js/react": "^0.47.0",
"@zag-js/tabs": "^0.74.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.5.1",
"input-otp": "^1.2.4",
"lucide-react": "^0.468.0",
"next-themes": "^0.4.1",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.52.2",
"react-icons": "^5.3.0",
"react-json-view": "^1.21.3",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^6.27.0",
"rxjs": "^7.8.1",
"smoldot": "^2.0.34",
"sonner": "^1.7.2",
"swr": "^2.2.5",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"usehooks-ts": "^3.1.0",
"vaul": "^0.9.1",
"zod": "^3.23.8"
},
"packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e"
}

53
playwright.config.ts Normal file
View File

@ -0,0 +1,53 @@
/// <reference types="node" />
import { defineConfig, devices } from "@playwright/test"
const isCI = !!process.env.CI
const dappUrl = isCI ? "http://localhost:4173" : "http://localhost:5173"
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: isCI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: dappUrl,
permissions: ["clipboard-read"],
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "chrome",
use: { ...devices["Desktop Chrome"], channel: "chrome" },
},
],
/* Run your local dev server before starting the tests */
webServer: {
command: isCI ? "pnpm preview --strictPort" : "pnpm dev --strictPort",
cwd: "../demo",
url: dappUrl,
timeout: 120 * 1000,
reuseExistingServer: !isCI,
},
})

15474
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,21 @@
import fs from "node:fs/promises"
import path from "node:path"
const getFilePathsWithExtension = async (directory, extension, out = []) => {
for (const file of await fs.readdir(directory)) {
const filePath = path.join(directory, file)
const stats = await fs.stat(filePath)
if (stats.isDirectory())
await getFilePathsWithExtension(filePath, extension, out)
else if (stats.isFile() && file.endsWith(extension)) out.push(filePath)
}
return out
}
const MAX_SIZE = 1024 * 1024 * 4
for (const filePath of await getFilePathsWithExtension("dist", "js")) {
const stats = await fs.stat(filePath)
if (stats.size > MAX_SIZE)
throw new Error(`${filePath} size is larger than 4MB`)
}

View File

@ -0,0 +1,21 @@
import fs from "node:fs/promises"
import path from "node:path"
import url from "node:url"
const [src, dst] = process.argv.slice(2)
const manifest = JSON.parse(await fs.readFile(src, { encoding: "utf-8" }))
const pkg = JSON.parse(
await fs.readFile(
path.resolve(
path.dirname(url.fileURLToPath(import.meta.url)),
"../package.json",
),
),
)
manifest.version = pkg.version
await fs.writeFile(dst, JSON.stringify(manifest, undefined, 2), {
encoding: "utf8",
})

View File

@ -0,0 +1,367 @@
import {
type RpcMethodHandlers,
type RpcMessage,
type RpcMethodMiddleware,
createRpc,
RpcError,
} from "@substrate/light-client-extension-helpers/utils"
import { createTx } from "@substrate/light-client-extension-helpers/tx-helper"
import { ss58Address, ss58Decode } from "@polkadot-labs/hdkd-helpers"
import { toHex, fromHex } from "@polkadot-api/utils"
import { getPolkadotSigner } from "@polkadot-api/signer"
import type { BackgroundRpcSpec } from "./types"
import { createKeyring } from "./keyring"
import { UserSignedExtensionName } from "../types/UserSignedExtension"
import { createClient } from "@polkadot-api/substrate-client"
import { getObservableClient } from "@polkadot-api/observable-client"
import { filter, firstValueFrom, map, mergeMap, take } from "rxjs"
import * as pjs from "./pjs"
import { Bytes, Variant } from "@polkadot-api/substrate-bindings"
import { InPageRpcSpec } from "../inpage/types"
import { Context, InternalSignRequest } from "./rpc/types"
import {
CHANNEL_ID,
wellKnownPrefixByGenesisHash,
wellKnownDecimalsByGenesisHash
} from "../constants"
import {
addChainSpecHandler,
listChainSpecsHandler,
removeChainSpecHandler,
} from "./rpc/chainspec"
const isUserSignedExtensionName = (s: string): s is UserSignedExtensionName => {
return (
s === "CheckMortality" ||
s === "ChargeTransactionPayment" ||
s === "ChargeAssetTxPayment"
)
}
const keyring = createKeyring()
let nextSignRequestId = 0
export const createBackgroundRpc = (
sendMessage: (message: RpcMessage) => void
) => {
const getAccounts: RpcMethodHandlers<
BackgroundRpcSpec,
Context
>["getAccounts"] = async ([chainId], { lightClientPageHelper }) => {
const chains = await lightClientPageHelper.getChains()
const chain = chains.find(({ genesisHash }) => genesisHash === chainId)
if (!chain) throw new Error("unknown chain")
return (await keyring.getAccounts(chain.genesisHash)).map(
({ publicKey }) => ({
address: ss58Address(
publicKey,
wellKnownPrefixByGenesisHash[chain.genesisHash]
),
})
)
}
const notifyOnAccountsChanged = async (context: Context) =>
context.notifyOnAccountsChanged(
(
await Promise.all(
(await context.lightClientPageHelper.getChains()).map(
({ genesisHash }) => getAccounts([genesisHash], context)
),
)
).flatMap((accounts) => accounts),
)
const handlers: RpcMethodHandlers<BackgroundRpcSpec, Context> = {
getAccounts,
async createTx(
[chainId, from, callData],
{ lightClientPageHelper, signRequests, port },
) {
const url = port.sender?.url
if (!url) throw new Error("unknown url")
const chains = await lightClientPageHelper.getChains()
const chain = chains.find(({ genesisHash }) => genesisHash === chainId)
if (!chain) throw new Error("unknown chain")
const id = nextSignRequestId++
const client = getObservableClient(createClient(chain.provider))
const chainHead$ = client.chainHead$()
const userSignedExtensionNames = await firstValueFrom(
chainHead$.best$.pipe(
mergeMap((blockInfo) =>
chainHead$.getRuntimeContext$(blockInfo.hash).pipe(
take(1),
map(({ lookup: { metadata } }) =>
metadata.extrinsic.signedExtensions
.map(({ identifier }) => identifier)
.filter(isUserSignedExtensionName)
),
),
),
filter(Boolean)
)
)
try {
const signRequest = new Promise<
Parameters<InternalSignRequest["resolve"]>[0]
>(
(resolve, reject) =>
(signRequests[id] = {
resolve,
reject,
chainId,
url,
address: ss58Address(
from,
wellKnownPrefixByGenesisHash[chain.genesisHash]
),
callData,
userSignedExtensions: {
type: "names",
names: userSignedExtensionNames,
},
})
)
const window = await chrome.windows.create({
focused: true,
height: 640,
width: 530,
left: 0,
top: 0,
type: "popup",
url: chrome.runtime.getURL(
`ui/assets/wallet-popup.html#/sign-request/${id}`
),
})
const removeWindow = () => chrome.windows.remove(window.id!)
port.onDisconnect.addListener(removeWindow)
const onWindowsRemoved = (windowId: number) => {
if (windowId !== window.id) return
const signRequest = signRequests[id]
if (!signRequest) return
signRequest.reject()
}
chrome.windows.onRemoved.addListener(onWindowsRemoved)
try {
const { userSignedExtensions } = await signRequest
const [keypair, scheme] = await keyring.getKeypair(chainId, from)
const signer = getPolkadotSigner(
keypair.publicKey,
scheme,
keypair.sign
)
const mortality = userSignedExtensions.CheckMortality ?? {
mortal: true,
period: 64,
}
const decimals = wellKnownDecimalsByGenesisHash[chainId] ?? 0
const asset = userSignedExtensions.ChargeAssetTxPayment?.asset
const tip =
(asset
? userSignedExtensions.ChargeAssetTxPayment?.tip
: userSignedExtensions.ChargeTransactionPayment) ?? 0n
const tipWithDecimals = BigInt(tip * Math.pow(10, decimals))
const tx = await createTx(chain.provider)({
signer,
callData: fromHex(callData),
hinted: {
mortality,
asset,
tipWithDecimals,
},
})
return toHex(tx)
} finally {
delete signRequests[id]
chrome.windows.remove(window.id!)
port.onDisconnect.removeListener(removeWindow)
chrome.windows.onRemoved.removeListener(onWindowsRemoved)
}
} finally {
chainHead$.unfollow()
client.destroy()
}
},
async pjsSignPayload(
[payload],
{ port, lightClientPageHelper, signRequests }
) {
const url = port.sender?.url
if (!url) throw new Error("unknown url")
const chains = await lightClientPageHelper.getChains()
const chain = chains.find(
({ genesisHash }) => genesisHash === payload.genesisHash
)
if (!chain) throw new Error("unknown chain")
const id = nextSignRequestId++
const signRequest = new Promise<
Parameters<InternalSignRequest["resolve"]>[0]
>(
(resolve, reject) =>
(signRequests[id] = {
resolve,
reject,
chainId: payload.genesisHash,
url,
address: payload.address,
callData: payload.method,
userSignedExtensions: {
type: "values",
values: pjs.getUserSignedExtensions(payload),
},
})
)
const window = await chrome.windows.create({
focused: true,
height: 640,
width: 530,
left: 0,
top: 0,
type: "popup",
url: chrome.runtime.getURL(
`ui/assets/wallet-popup.html#/sign-request/${id}`
),
})
const removeWindow = () => chrome.windows.remove(window.id!)
port.onDisconnect.addListener(removeWindow)
const onWindowsRemoved = (windowId: number) => {
if (windowId !== window.id) return
const signRequest = signRequests[id]
if (!signRequest) return
signRequest.reject()
}
chrome.windows.onRemoved.addListener(onWindowsRemoved)
try {
await signRequest
} finally {
delete signRequests[id]
chrome.windows.remove(window.id!)
port.onDisconnect.removeListener(removeWindow)
chrome.windows.onRemoved.removeListener(onWindowsRemoved)
}
const signaturePayload = await pjs.getSignaturePayload(
chain.provider,
payload
)
const multiSignatureEncoder = Variant({
Ed25519: Bytes(64),
Sr25519: Bytes(64),
Ecdsa: Bytes(65),
}).enc
const [keypair, scheme] = await keyring.getKeypair(
payload.genesisHash,
toHex(ss58Decode(payload.address)[0])
)
return toHex(
multiSignatureEncoder({
type: scheme,
value: keypair.sign(signaturePayload),
})
)
},
async getSignRequests(_, { signRequests }) {
return signRequests
},
async approveSignRequest([id, userSignedExtensions], { signRequests }) {
signRequests[id]?.resolve({
userSignedExtensions,
})
},
async cancelSignRequest([id], { signRequests }) {
signRequests[id]?.reject()
},
async lockKeyring() {
return keyring.lock()
},
async resetKeyring([password]) {
return keyring.reset(password)
},
async unlockKeyring([password]) {
return keyring.unlock(password)
},
async changePassword([currentPassword, newPassword]) {
return keyring.changePassword(currentPassword, newPassword)
},
async createPassword([password]) {
return keyring.setup(password)
},
async insertCryptoKey([args], context) {
const existingKey = await keyring.getCryptoKey(args.name)
if (existingKey)
throw new Error(`crypto key "${args.name}" already exists`)
await keyring.insertCryptoKey(args)
notifyOnAccountsChanged(context)
},
async updateCryptoKey([args], context) {
const existingKey = await keyring.getCryptoKey(args.name)
if (!existingKey)
throw new Error(`crypto key "${args.name}" does not exist`)
await keyring.updateCryptoKey(args)
notifyOnAccountsChanged(context)
},
async getCryptoKey([name]) {
return keyring.getCryptoKey(name)
},
async getCryptoKeys() {
return keyring.getCryptoKeys()
},
async revealCryptoKey([name, index]) {
return keyring.revealCryptoKey(name, index)
},
async clearCryptoKeys([name], context) {
await keyring.clearCryptoKeys(name)
notifyOnAccountsChanged(context)
},
async clearCryptoKey([name, index], context) {
await keyring.clearCryptoKey(name, index)
notifyOnAccountsChanged(context)
},
async getKeyringState() {
return {
isLocked: await keyring.isLocked(),
hasPassword: await keyring.hasPassword(),
}
},
getChainSpecs: listChainSpecsHandler,
addChainSpec: addChainSpecHandler,
removeChainSpec: removeChainSpecHandler,
}
type Method = keyof BackgroundRpcSpec
const ALLOWED_WEB_METHODS: Method[] = [
"createTx",
"getAccounts",
"pjsSignPayload",
]
const allowedMethodsMiddleware: RpcMethodMiddleware<Context> = async (
next,
request,
context
) => {
const { port } = context
if (
port.name === CHANNEL_ID &&
!ALLOWED_WEB_METHODS.includes(request.method as Method)
)
throw new RpcError("Method not found", -32601)
return next(request, context)
}
return createRpc(sendMessage, handlers, [
allowedMethodsMiddleware,
]).withClient<InPageRpcSpec>()
}

View File

@ -0,0 +1,39 @@
/**
* Tracks when a service worker was last alive and extends the service worker
* lifetime by writing the current time to extension storage every 20 seconds.
* You should still prepare for unexpected termination - for example, if the
* extension process crashes or your extension is manually stopped at
* chrome://serviceworker-internals.
*
* @link {https://developer.chrome.com/docs/extensions/develop/migrate/to-service-workers}
*/
let heartbeatInterval: NodeJS.Timeout | number = 0
async function runHeartbeat() {
await chrome.storage.local.set({ "last-heartbeat": new Date().getTime() })
}
/**
* Starts the heartbeat interval which keeps the service worker alive. Call
* this sparingly when you are doing work which requires persistence, and call
* stopHeartbeat once that work is complete.
*/
export async function startHeartbeat() {
// Run the heartbeat once at service worker startup.
runHeartbeat().then(() => {
// Then again every 20 seconds.
heartbeatInterval = setInterval(runHeartbeat, 20 * 1000)
})
}
export async function stopHeartbeat() {
clearInterval(heartbeatInterval)
}
/**
* Returns the last heartbeat stored in extension storage, or undefined if
* the heartbeat has never run before.
*/
export async function getLastHeartbeat() {
return (await chrome.storage.local.get("last-heartbeat"))["last-heartbeat"]
}

118
src/background/index.ts Normal file
View File

@ -0,0 +1,118 @@
import {
InputChain,
register,
} from "@substrate/light-client-extension-helpers/background"
import { start } from "@substrate/light-client-extension-helpers/smoldot"
import { createBackgroundRpc } from "./createBackgroundRpc"
import * as storage from "./storage"
import type { Account } from "./types"
import { startHeartbeat } from "./heartbeat"
import { CHANNEL_ID } from "../constants"
const { lightClientPageHelper, addOnAddChainByUserListener } = register({
smoldotClient: start({ maxLogLevel: 4 }),
getWellKnownChainSpecs: () =>
// Note that this list doesn't necessarily always have to match the list of well-known
// chains in `@substrate/connect`. The list of well-known chains is not part of the stability
// guarantees of the connect <-> extension protocol and is thus allowed to change
// between versions of the extension. For this reason, we don't use the `WellKnownChain`
// enum from `@substrate/connect` but instead manually make the list in that enum match
// the list present here.
Promise.all(
[
"./chainspecs/casper_staging_testnet.json"
].map((path) =>
fetch(chrome.runtime.getURL(path)).then((response) => response.text()),
),
),
})
const signRequests = {}
type BackgroundRpc = ReturnType<typeof createBackgroundRpc>
const connectedRpcs: BackgroundRpc[] = []
const notifyOnAccountsChanged = (accounts: Account[]) =>
connectedRpcs.forEach((rpc) => rpc.notify("onAccountsChanged", [accounts]))
const subscribeOnAccountsChanged = (rpc: BackgroundRpc) => {
connectedRpcs.push(rpc)
return () => {
connectedRpcs.splice(connectedRpcs.indexOf(rpc), 1)
}
}
chrome.runtime.onConnect.addListener((port) => {
if (!port.name.startsWith(CHANNEL_ID)) return
const rpc = createBackgroundRpc((msg) => port.postMessage(msg))
port.onMessage.addListener((msg) =>
rpc.handle(msg, {
lightClientPageHelper,
signRequests,
port,
notifyOnAccountsChanged,
}),
)
port.onDisconnect.addListener(subscribeOnAccountsChanged(rpc))
})
chrome.runtime.onInstalled.addListener(async ({ reason }) => {
const self = await chrome.management.getSelf()
if (reason === chrome.runtime.OnInstalledReason.INSTALL) {
const keystore = await storage.get("keystore")
if (
keystore ||
// don't pop up a new tab in development so playwright can reliably
// run tests on the extension
self.installType === "development"
)
return
chrome.tabs.create({
url: chrome.runtime.getURL(`ui/assets/wallet-popup.html#/welcome`),
})
}
})
addOnAddChainByUserListener(async (inputChain) => {
const isRelayChain = !inputChain.relayChainGenesisHash
const existingChain = await lightClientPageHelper.getChain(
inputChain.genesisHash,
)
if (isRelayChain && !existingChain) {
await waitForAddChainApproval(inputChain)
const persistedChain = await lightClientPageHelper.getChain(
inputChain.genesisHash,
)
if (!persistedChain) {
throw new Error("User rejected")
}
}
})
const waitForAddChainApproval = async (inputChain: InputChain) => {
const window = await chrome.windows.create({
focused: true,
width: 400,
height: 600,
left: 150,
top: 150,
type: "popup",
url: chrome.runtime.getURL(
`ui/assets/wallet-popup.html#/add-chain-by-user?params=${encodeURIComponent(JSON.stringify(inputChain))}`,
),
})
const onWindowsRemoved = (windowId: number) => {
if (windowId !== window.id) return
resolveWindowClosed()
}
chrome.windows.onRemoved.addListener(onWindowsRemoved)
const { promise: windowClosedPromise, resolve: resolveWindowClosed } =
Promise.withResolvers<void>()
await windowClosedPromise
}
startHeartbeat()

357
src/background/keyring.ts Normal file
View File

@ -0,0 +1,357 @@
import {
type CreateDeriveFn,
ecdsaCreateDerive,
ed25519CreateDerive,
sr25519CreateDerive,
} from "@polkadot-labs/hdkd"
import {
sr25519,
ed25519,
ecdsa,
KeyPair,
Curve,
} from "@polkadot-labs/hdkd-helpers"
import { KeystoreMeta, keystoreV4, type KeystoreV4WithMeta } from "./keystore"
import { assert } from "./utils"
import * as storage from "./storage"
import {
InsertCryptoKeyArgs,
UpdateCryptoKeyArgs,
RemoveCryptoKeyArgs,
KeystoreAccount,
RevealCryptoKeyArgs,
} from "./types"
import { toHex, fromHex } from "@polkadot-api/utils"
import { wellKnownChainIdByGenesisHash } from "../constants"
const createDeriveFnMap: Record<string, CreateDeriveFn> = {
Sr25519: sr25519CreateDerive,
Ed25519: ed25519CreateDerive,
Ecdsa: ecdsaCreateDerive,
}
const curveFnMap: Record<string, Curve> = {
Sr25519: sr25519,
Ed25519: ed25519,
Ecdsa: ecdsa,
}
const createKeyPair = (privateKey: string, scheme: string): KeyPair => {
const curve = curveFnMap[scheme]
if (!curve) throw new Error("unsupported signature scheme")
return {
publicKey: curve.getPublicKey(privateKey),
sign(message) {
return curve.sign(message, privateKey)
},
}
}
export const createKeyring = () => {
const getKeystore = () => storage.get("keystore")
const setKeystore = (keystore: KeystoreV4WithMeta) =>
storage.set("keystore", keystore)
const removeKeystore = () => storage.remove("keystore")
const getKeystoreAccounts = (
keystoreMeta: KeystoreMeta,
): KeystoreAccount[] => {
switch (keystoreMeta.type) {
case "KeysetKeystore":
return keystoreMeta.derivationPaths.map((d) => ({
...d,
type: "Keyset",
}))
case "KeypairKeystore":
return [
{
type: "Keypair",
publicKey: keystoreMeta.publicKey,
},
]
}
}
const getCryptoKeys = async () => {
const keys = (await getKeystore())?.meta ?? []
return keys.map((meta) => ({
name: meta.name,
scheme: meta.scheme,
accounts: getKeystoreAccounts(meta),
createdAt: meta.createdAt,
}))
}
const decodeSecrets = (secrets: Uint8Array) =>
JSON.parse(new TextDecoder().decode(secrets)) as string[]
const encodeSecrets = (secrets: string[]) =>
new TextEncoder().encode(JSON.stringify(secrets))
const getAccounts = async (chainId: string) => {
const keystore = await getKeystore()
if (!keystore) return []
return keystore.meta
.flatMap(getKeystoreAccounts)
.filter(
(account) =>
(account.type === "Keyset" && account.chainId === chainId) ||
account.type !== "Keyset",
)
}
const insertCryptoKey = async (args: InsertCryptoKeyArgs) => {
const keystore = await getKeystore()
assert(keystore, "keyring must be setup")
assert(currentPassword, "keyring must be unlocked")
assert(
["Sr25519", "Ed25519", "Ecdsa"].includes(args.scheme),
"invalid signature scheme",
)
const secrets = decodeSecrets(keystoreV4.decrypt(keystore, currentPassword))
const secret = args.type === "Keyset" ? args.miniSecret : args.privatekey
const newKeystore = keystoreV4.create(
currentPassword,
encodeSecrets([...secrets, secret]),
)
const newCryptoKey =
args.type === "Keyset"
? {
type: "KeysetKeystore" as const,
derivationPaths: args.derivationPaths,
}
: args.type === "Keypair"
? {
type: "KeypairKeystore" as const,
publicKey: toHex(
createKeyPair(args.privatekey, args.scheme).publicKey,
),
}
: undefined
if (!newCryptoKey) throw new Error("invalid keystore type")
setKeystore({
...newKeystore,
meta: [
...keystore.meta,
{
name: args.name,
scheme: args.scheme,
createdAt: args.createdAt,
...newCryptoKey,
},
],
})
}
const updateCryptoKey = async (args: UpdateCryptoKeyArgs) => {
const keystore = await getKeystore()
assert(keystore, "keyring must be setup")
assert(currentPassword, "keyring must be unlocked")
const cryptoKeyIndex = keystore.meta.findIndex((k) => k.name === args.name)
assert(cryptoKeyIndex != -1, "keyring name not found")
const uniqueNetworks = new Map()
keystore.meta[cryptoKeyIndex].derivationPaths.forEach((obj: any) =>
uniqueNetworks.set(wellKnownChainIdByGenesisHash[obj.chainId], {
network: wellKnownChainIdByGenesisHash[obj.chainId],
chainId: obj.chainId,
})
)
const secrets = decodeSecrets(keystoreV4.decrypt(keystore, currentPassword))
const derive = createDeriveFnMap[keystore.meta[cryptoKeyIndex].scheme](secrets[cryptoKeyIndex])
const newDerivationPaths = Array.from(uniqueNetworks.values())
.map(({ network, chainId }) => {
return {
chainId,
path: `//${network}/${args.deviation}`,
publicKey: toHex(derive(`//${network}/${args.deviation}`).publicKey),
}
})
keystore.meta[cryptoKeyIndex].derivationPaths.push(...newDerivationPaths)
setKeystore(keystore)
}
const clearCryptoKey = async (name: string, index: number) => {
const keystore = await getKeystore()
assert(keystore, "keyring must be setup")
assert(currentPassword, "keyring must be unlocked")
const cryptoKeyIndex = keystore.meta.findIndex(obj => obj.name === name)
assert(cryptoKeyIndex != -1, "keyring name not found")
const secrets = decodeSecrets(keystoreV4.decrypt(keystore, currentPassword))
switch (keystore.meta.at(cryptoKeyIndex)?.type) {
case "KeysetKeystore":
keystore.meta.at(cryptoKeyIndex)?.derivationPaths.splice(index, 1)
setKeystore(keystore)
break
case "KeypairKeystore":
secrets.splice(cryptoKeyIndex, 1)
keystore.meta.splice(cryptoKeyIndex, 1)
const newKeystore = keystoreV4.create(
currentPassword,
encodeSecrets(secrets),
)
setKeystore({
...newKeystore,
meta: keystore.meta,
})
break
default:
throw new Error("invalid keystore type")
}
}
const clearCryptoKeys = async (args: RemoveCryptoKeyArgs) => {
const keystore = await getKeystore()
assert(keystore, "keyring must be setup")
assert(currentPassword, "keyring must be unlocked")
const cryptoKeyIndex = keystore.meta.findIndex(obj => obj.name === args.name)
assert(cryptoKeyIndex != -1, "keyring name not found")
const secrets = decodeSecrets(keystoreV4.decrypt(keystore, currentPassword))
secrets.splice(cryptoKeyIndex, 1)
const newKeystore = keystoreV4.create(
currentPassword,
encodeSecrets(secrets),
)
keystore.meta.splice(cryptoKeyIndex, 1)
setKeystore({
...newKeystore,
meta: keystore.meta,
})
}
const revealCryptoKey = async (name: string, index: number) => {
const keystore = await getKeystore()
assert(keystore, "keyring must be setup")
assert(currentPassword, "keyring must be unlocked")
const cryptoKeyIndex = keystore.meta.findIndex((k) => k.name === name)
assert(cryptoKeyIndex != -1, "keyring name not found")
const secret = decodeSecrets(
keystoreV4.decrypt(keystore, currentPassword)
).at(cryptoKeyIndex)
assert(secret, "secret key should exists")
// TODO: need to get secret from secret and derivation path
return `0x${secret}`
}
let currentPassword: string | undefined
return {
async unlock(password: string) {
const keystore = await getKeystore()
assert(keystore, "keyring must be setup")
if (!keystoreV4.verifyPassword(keystore, password))
throw new Error("invalid password")
currentPassword = password
},
async lock() {
assert(await getKeystore(), "keyring must be setup")
assert(currentPassword, "keyring must be unlocked")
currentPassword = undefined
},
async isLocked() {
return !currentPassword || !(await getKeystore())
},
async reset(password: string) {
const keystore = await getKeystore()
assert(keystore, "keyring must be setup")
assert(currentPassword, "keyring must be unlocked")
if (!keystoreV4.verifyPassword(keystore, password))
throw new Error("invalid password")
currentPassword = undefined
await removeKeystore()
},
async changePassword(password: string, newPassword: string) {
const keystore = await getKeystore()
assert(keystore, "keyring must be setup")
assert(currentPassword, "keyring must be unlocked")
if (!keystoreV4.verifyPassword(keystore, password))
throw new Error("invalid password")
currentPassword = newPassword
await setKeystore({
...keystoreV4.create(
newPassword,
keystoreV4.decrypt(keystore, password),
),
meta: keystore.meta,
})
},
async setup(password: string) {
assert(!(await getKeystore()), "keyring is already setup")
await setKeystore({
...keystoreV4.create(
password,
new TextEncoder().encode(JSON.stringify([])),
),
meta: [],
})
currentPassword = password
},
async hasPassword() {
return !!(await getKeystore())
},
getAccounts,
insertCryptoKey,
updateCryptoKey,
getCryptoKeys,
async getCryptoKey(name: string) {
return (await getCryptoKeys())?.find((m) => m.name === name)
},
revealCryptoKey,
clearCryptoKey,
clearCryptoKeys,
async getKeypair(chainId: string, publicKey: string) {
assert(currentPassword, "keyring must be unlocked")
const keystore = await getKeystore()
assert(keystore, "keyring must be setup")
const keysetIndex = keystore.meta.findIndex((keyset) => {
switch (keyset.type) {
case "KeysetKeystore":
return keyset.derivationPaths.some(
(d) => d.chainId === chainId && d.publicKey === publicKey,
)
case "KeypairKeystore":
return keyset.publicKey === publicKey
default:
throw new Error("invalid keystore type")
}
})
if (keysetIndex === -1) {
throw new Error("unknown account")
}
const secret = decodeSecrets(
keystoreV4.decrypt(keystore, currentPassword),
)[keysetIndex]
const cryptoKey = keystore.meta[keysetIndex]
switch (cryptoKey.type) {
case "KeysetKeystore": {
const { derivationPaths, scheme } = cryptoKey
const derivationPath = derivationPaths.find(
(d) => d.publicKey === publicKey,
)!
const createDeriveFn = createDeriveFnMap[scheme]
if (!createDeriveFn) throw new Error("invalid signature scheme")
return [
createDeriveFn(secret)(derivationPath.path),
scheme as "Sr25519" | "Ed25519" | "Ecdsa",
] as const
}
case "KeypairKeystore": {
let keypair = createKeyPair(secret, cryptoKey.scheme)
return [keypair, cryptoKey.scheme] as const
}
}
},
}
}

View File

@ -0,0 +1,3 @@
export * as keystoreV4 from "./keystoreV4"
export type * from "./keystoreV4"
export type * from "./types"

View File

@ -0,0 +1,114 @@
import { expect, it, describe } from "vitest"
import { type KeystoreV4, verifyPassword, decrypt, create } from "./keystoreV4"
import { hexToBytes, randomBytes } from "@noble/hashes/utils"
type TestVector = {
password: string
encodedPassword: string
secret: string
keystoreJson: KeystoreV4
}
// From https://eips.ethereum.org/EIPS/eip-2335#test-cases
const testVectors: TestVector[] = [
{
password: "𝔱𝔢𝔰𝔱𝔭𝔞𝔰𝔰𝔴𝔬𝔯𝔡🔑",
encodedPassword: "7465737470617373776f7264f09f9491",
secret: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
keystoreJson: {
crypto: {
kdf: {
function: "scrypt",
params: {
dklen: 32,
n: 262144,
p: 1,
r: 8,
salt: "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3",
},
message: "",
},
checksum: {
function: "sha256",
params: {},
message: "d2217fe5f3e9a1e34581ef8a78f7c9928e436d36dacc5e846690a5581e8ea484",
},
cipher: {
function: "aes-128-ctr",
params: {
iv: "264daa3f303d7259501c93d997d84fe6",
},
message: "06ae90d55fe0a6e9c5c3bc5b170827b2e5cce3929ed3f116c2811e6366dfe20f",
},
},
description: "This is a test keystore that uses scrypt to secure the secret.",
pubkey: "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07",
path: "m/12381/60/3141592653/589793238",
uuid: "1d85ae20-35c5-4611-98e8-aa14a633906f",
version: 4,
} as KeystoreV4,
},
{
password: "𝔱𝔢𝔰𝔱𝔭𝔞𝔰𝔰𝔴𝔬𝔯𝔡🔑",
encodedPassword: "7465737470617373776f7264f09f9491",
secret: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
keystoreJson: {
crypto: {
kdf: {
function: "pbkdf2",
params: {
dklen: 32,
c: 262144,
prf: "hmac-sha256",
salt: "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3",
},
message: "",
},
checksum: {
function: "sha256",
params: {},
message: "8a9f5d9912ed7e75ea794bc5a89bca5f193721d30868ade6f73043c6ea6febf1",
},
cipher: {
function: "aes-128-ctr",
params: {
iv: "264daa3f303d7259501c93d997d84fe6",
},
message: "cee03fde2af33149775b7223e7845e4fb2c8ae1792e5f99fe9ecf474cc8c16ad",
},
},
description: "This is a test keystore that uses PBKDF2 to secure the secret.",
pubkey: "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07",
path: "m/12381/60/0/0",
uuid: "64625def-3331-4eea-ab6f-782f3ed16a83",
version: 4,
} as KeystoreV4,
},
]
describe.each(testVectors)("test vector", (testVector) => {
it("should verifyPassword", () => {
expect(verifyPassword(testVector.keystoreJson, testVector.password)).toBe(
true,
)
expect(verifyPassword(testVector.keystoreJson, "invalid password")).toBe(
false,
)
})
it("should decrypt", () => {
expect(decrypt(testVector.keystoreJson, testVector.password)).toStrictEqual(
hexToBytes(testVector.secret),
)
expect(() => decrypt(testVector.keystoreJson, "invalid password")).toThrow()
})
})
it("should create keystore", () => {
const secret = randomBytes(32)
const keystore = create("123456", secret)
expect(verifyPassword(keystore, "123456")).toBe(true)
expect(verifyPassword(keystore, "invalid password")).toBe(false)
expect(decrypt(keystore, "123456")).toStrictEqual(secret)
expect(() => decrypt(keystore, "invalid password")).toThrow()
})

View File

@ -0,0 +1,195 @@
import { scrypt } from "@noble/hashes/scrypt"
import { pbkdf2 } from "@noble/hashes/pbkdf2"
import { sha256 } from "@noble/hashes/sha256"
import { ctr as aesCtr } from "@noble/ciphers/aes"
import {
bytesToHex,
concatBytes,
hexToBytes,
randomBytes,
} from "@noble/hashes/utils"
import { crypto } from "@noble/hashes/crypto"
import { managedNonce } from "@noble/ciphers/webcrypto"
import { xsalsa20poly1305 } from "@noble/ciphers/salsa"
export type Cipher = {
encrypt(plaintext: Uint8Array): Uint8Array
decrypt(ciphertext: Uint8Array): Uint8Array
}
export type KeystoreV4 = {
version: 4
uuid: string
description?: string
crypto: {
kdf: KdfModule
checksum: ChecksumModule
cipher: CipherModule
}
}
type KdfModule = ScryptKdfModule | Pbkdf2KdfModule
type ScryptKdfModule = {
function: "scrypt"
params: {
dklen: number
n: number
p: number
r: number
salt: string
}
message: string
}
type Pbkdf2KdfModule = {
function: "pbkdf2"
params: {
dklen: number
c: number
prf: string
salt: string
}
message: string
}
type ChecksumModule = {
function: "sha256"
params: {}
message: string
}
type CipherModule = Aes128CtrCipherModule | Xsalsa20Poly1305CipherModule
type Aes128CtrCipherModule = {
function: "aes-128-ctr"
params: {
iv: string
}
message: string
}
type Xsalsa20Poly1305CipherModule = {
function: "xsalsa20-poly1305"
params: {}
message: string
}
export const create = (password: string, secret: Uint8Array): KeystoreV4 => {
const kdf = {
function: "scrypt" as const,
params: {
dklen: 32,
n: 2 ** 16,
r: 8,
p: 1,
salt: bytesToHex(randomBytes(32)),
},
message: "",
}
const key = deriveKey(kdf, password)
const ciphertext = getCipher_(
{
function: "xsalsa20-poly1305",
params: {},
message: "",
},
key,
).encrypt(secret)
return {
version: 4,
uuid: crypto.randomUUID(),
crypto: {
kdf,
checksum: {
function: "sha256",
params: {},
message: bytesToHex(computeChecksum(key.slice(16, 32), ciphertext)),
},
cipher: {
function: "xsalsa20-poly1305",
params: {},
message: bytesToHex(ciphertext),
},
},
}
}
const controlCodeFilter = (charCode: number) =>
charCode > 0x1f && !(charCode >= 0x7f && charCode <= 0x9f)
const encodePassword = (password: string) =>
new TextEncoder().encode(
password
.normalize("NFKD")
.split("")
.filter((char) => controlCodeFilter(char.charCodeAt(0)))
.join(""),
)
const deriveKey = (kdf: KdfModule, password: string) => {
if (kdf.function === "scrypt") {
const { salt, dklen: dkLen, n: N, p, r } = kdf.params
return scrypt(encodePassword(password), hexToBytes(salt), {
dkLen,
N,
p,
r,
})
} else if (kdf.function === "pbkdf2") {
const { salt, prf, dklen: dkLen, c } = kdf.params
if (prf !== "hmac-sha256") throw new Error("Invalid prf")
return pbkdf2(sha256, encodePassword(password), hexToBytes(salt), {
dkLen,
c,
})
}
throw new Error("Invalid key derivation function")
}
const computeChecksum = (key: Uint8Array, ciphertext: Uint8Array) =>
sha256(concatBytes(key, ciphertext))
const verifyChecksum = (
checksum: ChecksumModule,
key: Uint8Array,
ciphertext: Uint8Array,
) => {
if (checksum.function !== "sha256")
throw new Error("Invalid checksum function")
return checksum.message === bytesToHex(computeChecksum(key, ciphertext))
}
export const verifyPassword = (
{ crypto: { kdf, cipher, checksum } }: KeystoreV4,
password: string,
) => {
const decryptionKey = deriveKey(kdf, password)
const ciphertext = hexToBytes(cipher.message)
return verifyChecksum(checksum, decryptionKey.slice(16, 32), ciphertext)
}
export const decrypt = (keystore: KeystoreV4, password: string) => {
const ciphertext = hexToBytes(keystore.crypto.cipher.message)
return getCipher(keystore, password).decrypt(ciphertext)
}
const getCipher_ = (cipher: CipherModule, key: Uint8Array) => {
if (cipher.function === "xsalsa20-poly1305")
return managedNonce(xsalsa20poly1305)(key.slice(0, 32))
else if (cipher.function === "aes-128-ctr")
return aesCtr(key.slice(0, 16), hexToBytes(cipher.params.iv))
throw new Error("Invalid cipher function")
}
export const getCipher = (
{ crypto: { kdf, checksum, cipher } }: KeystoreV4,
password: string,
): Cipher => {
const ciphertext = hexToBytes(cipher.message)
const key = deriveKey(kdf, password)
if (!verifyChecksum(checksum, key.slice(16, 32), ciphertext))
throw new Error("Invalid password")
return getCipher_(cipher, key)
}

View File

@ -0,0 +1,29 @@
import { KeystoreV4 } from "./keystoreV4"
export type DerivationPath = {
chainId: string
path: string
publicKey: string
}
export type BaseKeystore = {
name: string
scheme: "Sr25519" | "Ed25519" | "Ecdsa"
createdAt: number
}
export type KeysetKeystore = BaseKeystore & {
type: "KeysetKeystore"
derivationPaths: DerivationPath[]
}
export type KeypairKeystore = BaseKeystore & {
type: "KeypairKeystore"
publicKey: string
}
export type KeystoreMeta = KeysetKeystore | KeypairKeystore
export type KeystoreV4WithMeta = KeystoreV4 & {
meta: KeystoreMeta[]
}

117
src/background/pjs.ts Normal file
View File

@ -0,0 +1,117 @@
import { createClient } from "@polkadot-api/substrate-client"
import { getObservableClient } from "@polkadot-api/observable-client"
import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders"
import {
Bytes,
Struct,
_void,
compact,
u32,
Option,
u16,
} from "@polkadot-api/substrate-bindings"
import { fromHex, mergeUint8 } from "@polkadot-api/utils"
import { filter, firstValueFrom } from "rxjs"
import { blake2b256 } from "@polkadot-labs/hdkd-helpers"
import type { Pjs } from "./types"
import { UserSignedExtensions } from "../types/UserSignedExtension"
import type { JsonRpcProvider } from "@polkadot-api/json-rpc-provider"
export const getSignaturePayload = async (
provider: JsonRpcProvider,
payload: Pjs.SignerPayloadJSON,
) => {
const { metadata$, unfollow } = getObservableClient(
createClient(provider),
).chainHead$()
const metadata = await firstValueFrom(
metadata$.pipe(filter(Boolean)),
).finally(unfollow)
const dynamicBuilder = getDynamicBuilder(getLookupFn(metadata))
const [extra, additionalSigned] = metadata.extrinsic.signedExtensions.reduce<
[extra: Uint8Array[], additionalSigned: Uint8Array[]]
>(
(
[extra, additionalSigned],
{ identifier, type: extraTy, additionalSigned: additionalSignedTy },
) => {
switch (identifier) {
case "CheckSpecVersion": {
additionalSigned.push(u32.enc(Number(payload.specVersion)))
break
}
case "CheckTxVersion": {
additionalSigned.push(u32.enc(Number(payload.transactionVersion)))
break
}
case "CheckGenesis": {
additionalSigned.push(fromHex(payload.genesisHash))
break
}
case "CheckMortality": {
extra.push(fromHex(payload.era))
additionalSigned.push(fromHex(payload.blockHash))
break
}
case "CheckNonce": {
extra.push(compact.enc(Number(payload.nonce)))
break
}
case "ChargeTransactionPayment": {
extra.push(compact.enc(BigInt(payload.tip)))
break
}
case "ChargeAssetTxPayment": {
extra.push(
Struct({
tip: compact,
asset: Option(Bytes(Infinity)),
}).enc({
tip: BigInt(payload.tip),
// TODO: update when PJS adds support
asset: undefined,
}),
)
break
}
default: {
if (
dynamicBuilder.buildDefinition(extraTy) === _void &&
dynamicBuilder.buildDefinition(additionalSignedTy) === _void
)
break
throw new Error(`unsupported signed-extension: ${identifier}`)
}
}
return [extra, additionalSigned]
},
[[], []],
)
const signaturePayload = mergeUint8(
fromHex(payload.method),
...extra,
...additionalSigned,
)
return signaturePayload.length > 256
? blake2b256(signaturePayload)
: signaturePayload
}
export const getUserSignedExtensions = (payload: Pjs.SignerPayloadJSON) => {
const userSignedExtensions: Partial<UserSignedExtensions> = {}
const mortality = fromHex(payload.era)
userSignedExtensions.CheckMortality =
// Ser mortality encoding https://spec.polkadot.network/id-extrinsics#sect-mortality-encoding
mortality.length === 1
? { mortal: false }
: { mortal: true, period: 2 << u16.dec(mortality) % (1 << 4) }
if (payload.signedExtensions.includes("ChargeTransactionPayment"))
userSignedExtensions.ChargeTransactionPayment = Number(payload.tip)
else if (payload.signedExtensions.includes("ChargeAssetTxPayment"))
userSignedExtensions.ChargeAssetTxPayment = {
// @ts-expect-error FIXME: bigint needs to be serialized
tip: Number(payload.tip),
}
return userSignedExtensions
}

View File

@ -0,0 +1,68 @@
import { RpcMethodHandlers } from "@substrate/light-client-extension-helpers/utils"
import { z } from "zod"
import { BackgroundRpcSpec } from "../types"
import { wellKnownGenesisHashByChainId } from "../../constants"
import { Context } from "./types"
const chainSpecSchema = z.object({
name: z.string(),
id: z.string(),
relay_chain: z.string().optional(),
})
export const listChainSpecsHandler: RpcMethodHandlers<
BackgroundRpcSpec,
Context
>["getChainSpecs"] = async (_, { lightClientPageHelper }) => {
const chains = await lightClientPageHelper.getChains()
const chainSpecs = chains.map((chain) => {
const parsed = chainSpecSchema.parse(JSON.parse(chain.chainSpec))
return {
...parsed,
genesisHash: chain.genesisHash,
isWellKnown: !!wellKnownGenesisHashByChainId[parsed.id],
raw: chain.chainSpec,
}
})
return chainSpecs
}
export const addChainSpecHandler: RpcMethodHandlers<
BackgroundRpcSpec,
Context
>["addChainSpec"] = async ([chainSpec], { lightClientPageHelper }) => {
const chainSpecParsed = chainSpecSchema.parse(JSON.parse(chainSpec))
const relayChainChainSpec = await lightClientPageHelper
.getChains()
.then((chains) =>
chains.map((chain) => ({
...chainSpecSchema.parse(JSON.parse(chain.chainSpec)),
genesisHash: chain.genesisHash,
})),
)
.then((chains) =>
chains.find((chain) => chain.id === chainSpecParsed.relay_chain),
)
if (chainSpecParsed.relay_chain && !relayChainChainSpec) {
throw new Error("relay chain not found")
}
if (relayChainChainSpec?.relay_chain) {
throw new Error("relay chain cannot be a parachain")
}
await lightClientPageHelper.persistChain(
chainSpec,
relayChainChainSpec?.genesisHash,
)
}
export const removeChainSpecHandler: RpcMethodHandlers<
BackgroundRpcSpec,
Context
>["removeChainSpec"] = async ([genesisHash], { lightClientPageHelper }) => {
await lightClientPageHelper.deleteChain(genesisHash)
}

View File

@ -0,0 +1,19 @@
import type { LightClientPageHelper } from "@substrate/light-client-extension-helpers/background"
import { UserSignedExtensions } from "../../types/UserSignedExtension"
import { Account, SignRequest } from "../types"
export type SignResponse = {
userSignedExtensions: Partial<UserSignedExtensions>
}
export type InternalSignRequest = {
resolve: (props: SignResponse) => void
reject: (reason?: any) => void
} & SignRequest
export type Context = {
lightClientPageHelper: LightClientPageHelper
signRequests: Record<string, InternalSignRequest>
port: chrome.runtime.Port
notifyOnAccountsChanged: (accounts: Account[]) => void
}

29
src/background/storage.ts Normal file
View File

@ -0,0 +1,29 @@
import type { KeystoreV4WithMeta } from "./keystore"
const STORAGE_PREFIX = "ghost-extension/"
type StorageConfig = {
keystore: KeystoreV4WithMeta
}
type StorageKey = keyof StorageConfig
const getKey = (key: StorageKey) => `${STORAGE_PREFIX}${key}`
export const remove = (keyOrKeys: StorageKey | StorageKey[]): Promise<void> =>
chrome.storage.local.remove(
Array.isArray(keyOrKeys) ? keyOrKeys.map(getKey) : getKey(keyOrKeys),
)
export const get = async <K extends StorageKey>(
key: K,
): Promise<StorageConfig[K] | undefined> => {
const key_ = getKey(key)
const { [key_]: value } = await chrome.storage.local.get([key_])
return value
}
export const set = <K extends StorageKey>(
key: K,
value: StorageConfig[K],
): Promise<void> => chrome.storage.local.set({ [getKey(key)]: value })

122
src/background/types.ts Normal file
View File

@ -0,0 +1,122 @@
import type { Injected } from "@polkadot/extension-inject/types"
import {
UserSignedExtensionName,
UserSignedExtensions,
} from "../types/UserSignedExtension"
/**
* 1:1 representation of chain spec JSON format with addition fields
*/
export type ChainSpec = {
name: string
id: string
genesisHash: string
relay_chain?: string
isWellKnown: boolean
raw: string
}
export type Account = {
address: string
}
export type KeystoreAccount =
| ({ type: "Keyset" } & DerivationPath)
| { type: "Keypair"; publicKey: string }
export type DerivationPath = {
chainId: string
path: string
publicKey: string
}
export type CryptoKey = {
name: string
scheme: "Sr25519" | "Ed25519" | "Ecdsa"
accounts: KeystoreAccount[]
createdAt: number
}
export type SignRequest = {
url: string
chainId: string
address: string
callData: string
userSignedExtensions:
| { type: "names"; names: UserSignedExtensionName[] }
| { type: "values"; values: Partial<UserSignedExtensions> }
}
export type InsertCryptoKeyArgs = {
name: string
scheme: "Sr25519" | "Ed25519" | "Ecdsa"
createdAt: number
} & (
| {
type: "Keyset"
miniSecret: string
derivationPaths: DerivationPath[]
}
| {
type: "Keypair"
privatekey: string
}
)
type KeyringState = {
isLocked: boolean
hasPassword: boolean
}
export type UpdateCryptoKeyArgs = {
name: string,
deviation: number,
}
export type RemoveCryptoKeyArgs = {
name: string,
}
export type RevealCryptoKeyArgs = {
meta: string,
index: number,
}
export namespace Pjs {
export type SignerPayloadJSON = Parameters<
Exclude<Injected["signer"]["signPayload"], undefined>
>[0]
export type SignerPayloadRaw = Parameters<
Exclude<Injected["signer"]["signRaw"], undefined>
>[0]
}
export type BackgroundRpcSpec = {
getAccounts(chainId: string): Promise<Account[]>
createTx(chainId: string, from: string, callData: string): Promise<string>
pjsSignPayload(payload: Pjs.SignerPayloadJSON): Promise<string>
// private methods
getSignRequests(): Promise<Record<string, SignRequest>>
approveSignRequest(
id: string,
userSignedExtensions: Partial<UserSignedExtensions>,
): Promise<void>
cancelSignRequest(id: string): Promise<void>
lockKeyring(): Promise<void>
resetKeyring(password: string): Promise<void>
unlockKeyring(password: string): Promise<void>
changePassword(currentPassword: string, newPassword: string): Promise<void>
createPassword(password: string): Promise<void>
getCryptoKeys(): Promise<CryptoKey[]>
insertCryptoKey(args: InsertCryptoKeyArgs): Promise<void>
updateCryptoKey(args: UpdateCryptoKeyArgs): Promise<void>
getCryptoKey(name: string): Promise<CryptoKey | undefined>
revealCryptoKey(name: string, index: number): Promise<string>
clearCryptoKey(name: string, index: number): Promise<void>
clearCryptoKeys(args: RemoveCryptoKeyArgs): Promise<void>
getKeyringState(): Promise<KeyringState>
getChainSpecs(): Promise<ChainSpec[]>
addChainSpec(chainSpec: string): Promise<void>
removeChainSpec(genesisHash: string): Promise<void>
}

3
src/background/utils.ts Normal file
View File

@ -0,0 +1,3 @@
export function assert(condition: unknown, msg?: string): asserts condition {
if (!condition) throw new Error(msg)
}

View File

@ -0,0 +1,38 @@
.networkSelect {
display: grid;
grid-template-areas: "select";
align-items: center;
position: relative;
min-width: 15ch;
max-width: 30ch;
border: 1px solid #999;
border-radius: 0.25rem;
padding: 0.25rem 0.5rem;
font-size: 1rem;
cursor: pointer;
line-height: 1.2;
margin-bottom: 1rem;
}
.networkSelect select {
cursor: pointer;
background-color: white;
}
.networkSelect::after {
grid-area: select;
}
.networkSelect:focus + .focus {
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
border: 2px solid var(--select-focus);
border-radius: inherit;
}

View File

@ -0,0 +1,269 @@
import { useEffect, useState } from "react"
import { MdDeleteOutline } from "react-icons/md"
import * as environment from "../environment"
import "./Bootnodes.css"
import { Title, Switch } from "."
import { helper } from "@substrate/light-client-extension-helpers/extension-page"
import { wellKnownGenesisHashByChainId } from "../constants"
import {
Select,
SelectValue,
SelectTrigger,
SelectContent,
SelectGroup,
SelectItem,
} from "@/components/ui/select"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
interface BootnodesType {
checked: boolean
bootnode: string
}
const getBootNodes = async (chainId: string) =>
(await helper.getChains()).find(
({ genesisHash }) => genesisHash === wellKnownGenesisHashByChainId[chainId],
)?.bootNodes ?? []
const setBootNodes = async (chainId: string, bootNodes: string[]) =>
helper.setBootNodes(wellKnownGenesisHashByChainId[chainId], bootNodes)
// Add to localstorage the given bootnode for the given chain
const saveToLocalStorage = async (
chainName: string,
bootnode: string,
add: boolean,
def: string[],
) => {
if (def.length === 0) throw new Error("Default Bootnodes should exist.")
let res: string[]
const chainBootnodes = await getBootNodes(chainName)
res = chainBootnodes && Object.keys(chainBootnodes).length > 0
? [...chainBootnodes]
: [...def]
add ? res.push(bootnode) : res.splice(res.indexOf(bootnode), 1)
await setBootNodes(chainName, res)
}
export const Bootnodes = () => {
const [selectedChain, setSelectedChain] = useState<string>("casper_staging_testnet")
const [defaultBn, setDefaultBn] = useState<BootnodesType[]>([])
const [customBn, setCustomBn] = useState<BootnodesType[]>([])
const [customBnInput, setCustomBnInput] = useState<string>("")
const [selectedChainDefaultBn, setSelectedChainDefaultBn] = useState<string[]>([])
const [addMessage, setAddMessage] = useState<any>(undefined)
const [bootnodeMsgClass, setBootnodeMsgClass] = useState<string>()
useEffect(() => {
if (addMessage && !addMessage?.error) {
setBootnodeMsgClass("pb-2 text-accent")
setCustomBnInput("")
} else {
setBootnodeMsgClass("pb-2 text-destructive")
}
}, [addMessage])
useEffect(() => {
Promise.all([
getBootNodes(selectedChain),
environment.getDefaultBootnodes(selectedChain),
]).then(([bootnodes, defaultBootnodes]) => {
console.assert(defaultBootnodes, `Invalid chain name: ${selectedChain}`)
defaultBootnodes ??= []
setSelectedChainDefaultBn(defaultBootnodes)
const tmpDef: BootnodesType[] = []
const tmpCust: BootnodesType[] = []
// When bootnodes do not exist assign and save the local ones
if (!bootnodes?.length) {
setBootNodes(selectedChain, defaultBootnodes)
defaultBootnodes.forEach((b) => {
tmpDef.push({ bootnode: b, checked: true })
})
} else {
bootnodes?.forEach((b) => {
defaultBootnodes?.length && defaultBootnodes?.includes(b)
? tmpDef.push({ bootnode: b, checked: true })
: tmpCust.push({ bootnode: b, checked: true })
})
}
setDefaultBn(tmpDef)
setCustomBn(tmpCust)
})
}, [selectedChain])
const checkMultiAddr = (addr: string) => {
const ws = /\/(ip4|ip6|dns4|dns6|dns)\/([a-zA-Z0-9.-]+)\/tcp\/[0-9]{1,5}(\/(ws|wss|tls\/ws))?\/p2p\/[a-zA-Z1-9^Il0O]+/i
const webrtc = /\/(ip4|ip6)\/(.*?)\/udp\/(.*?)\/webrtc\/certhash\/(.*?)\/p2p\/[a-zA-Z1-9^Il0O]+/i
if (!ws.test(addr) && !webrtc.test(addr))
throw new Error("Provided multiaddress is not correct.")
}
const alterBootnodes = async (
bootnode: string,
add: boolean,
defaultBootnode: boolean,
) => {
// if bootnode belongs to the list (default) then it does not need to be validated as it
// comes from the chainspecs. It can be saved to the local storage at once.
try {
if (!defaultBootnode) {
// verify bootnode validity
checkMultiAddr(customBnInput)
}
// Check if bootnode already exists in the default and custom lists
if (
selectedChainDefaultBn?.includes(customBnInput) ||
customBn.map((c) => c.bootnode).includes(customBnInput)
) {
setAddMessage({ error: true, message: "Bootnode already exists in the list." })
} else {
await saveToLocalStorage(
selectedChain,
bootnode,
add,
selectedChainDefaultBn,
)
}
const tmp = defaultBootnode ? [...defaultBn] : [...customBn]
const i = tmp.findIndex((b) => b.bootnode === bootnode)
if (i !== -1) {
tmp[i].checked = add
} else {
tmp.push({ bootnode, checked: true })
}
defaultBootnode ? setDefaultBn(tmp) : setCustomBn(tmp)
setCustomBnInput("")
} catch (err) {
setAddMessage({ error: true, message: (err as Error).message.replace(/^\w/, (c) => c.toUpperCase()) })
}
}
return (
<section className="container">
<h2 className="pb-2 space-y-2 text-3xl font-semibold md:pb-4">
Networks
</h2>
<div className="p-4 bg-muted rounded">
<Select
onValueChange={(networkName) => {
setSelectedChain(networkName)
setCustomBnInput("")
setAddMessage(undefined)
}}
name={selectedChain}
value={selectedChain}
>
<SelectTrigger className="sm:w-[250px] w-[100%]" data-testid="scheme-select">
<SelectValue placeholder="Select Network" />
</SelectTrigger>
<SelectContent className="sm:w-[250px] w-[100%]">
<SelectGroup>
<SelectItem data-testid="scheme-casper_staging_testnet" value="casper_staging_testnet">
Casper
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Title className="mt-8">Bootnodes</Title>
<Title titleType="small">Default</Title>
<div className="mb-8">
{selectedChainDefaultBn?.map((bn) => (
<div className="flex items-center mb-2 leading-4 wrap">
<div className="sm:w-11/12 w-1/2 overflow-hidden text-ellipsis whitespace-nowrap">
{bn}
</div>
<Switch
className="w-1/2 sm:w-1/12"
bootnode={bn}
alterBootnodes={alterBootnodes}
defaultBootnode={true}
isChecked={defaultBn.map((d) => d.bootnode).includes(bn)}
/>
</div>
))}
</div>
<Title titleType="small" className="text-accent">Custom</Title>
<div className="mb-8">
{customBn.map((c) => (
<div className="flex items-center mb-2 leading-4">
<div className="sm:w-11/12 w-1/2 overflow-hidden text-ellipsis whitespace-nowrap">
{c.bootnode}
</div>
<div className="flex w-1/2 sm:w-1/12 ml-8">
<Button
variant="secondary"
className="flex items-center text-white h-6 bg-red-600 rounded-full hover:bg-red-700"
onClick={async () => {
try {
await saveToLocalStorage(
selectedChain,
c.bootnode,
false,
selectedChainDefaultBn,
)
} catch (e) {
console.log(e)
}
setCustomBn(customBn.filter((f) => f.bootnode !== c.bootnode))
}}
>
<MdDeleteOutline className="text-lg" />
</Button>
</div>
</div>
))}
</div>
<Title>Add custom Bootnode</Title>
<div className="flex flex-col">
<div className="flex sm:flex-row flex-col gap-6 justify-between mb-4">
<Input
type="text"
placeholder="Enter bootnode address"
className="sm:w-5/6 w-full"
value={customBnInput}
onChange={(v) => {
addMessage && setAddMessage(undefined)
setCustomBnInput(v.target.value)
}}
/>
<Button
variant="default"
size="full"
className="sm:w-1/6 w-full"
disabled={!customBnInput}
onClick={() => {
if (
selectedChainDefaultBn?.includes(customBnInput) ||
customBn.map((c) => c.bootnode).includes(customBnInput)
) {
setAddMessage({
error: true,
message: "Bootnode already exists in the list.",
})
} else {
alterBootnodes(
customBnInput,
true,
selectedChainDefaultBn?.includes(customBnInput),
)
}
}}
>
Add
</Button>
</div>
<p className={bootnodeMsgClass}>
{addMessage && Object.keys(addMessage) ? addMessage.message : ""}
</p>
</div>
</div>
</section>
)
}

View File

@ -0,0 +1,106 @@
import { useEffect, useState } from "react"
import * as environment from "../environment"
import { Button } from "@/components/ui/button"
interface Props {
isOptions?: boolean
show: boolean
}
const openInNewTab = (url: string): void => {
const newWindow = window.open(url, "_blank", "noopener,noreferrer")
if (newWindow) newWindow.opener = null
}
export const BraveModal = ({ show, isOptions }: Props) => {
const [showModal, setShowModal] = useState<boolean>(show)
useEffect(() => {
setShowModal(show)
}, [show])
return (
<div
id="defaultModal"
className={`${isOptions ? "absolute " : ""} ${
showModal ? "" : "hidden "
}overflow-y-auto overflow-x-hidden fixed bottom-0 right-0 left-0 z-50 w-full h-modal`}
>
<div className="relative w-full h-full md:h-auto">
<div className={"border-2 border-solid relative bg-muted shadow rounded-t-lg"}>
<div className="flex justify-between items-center py-2 px-4 rounded-t">
<h5 className="text-base font-semibold text-accent">
Attention Brave Users
</h5>
<Button
size="icon"
variant="ghost"
type="button"
onClick={() => setShowModal(false)}
>
<svg
aria-hidden="true"
className="w-3 h-3"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
></path>
</svg>
<span className="sr-only">Close modal</span>
</Button>
</div>
<div className="py-2 px-4 space-y-2">
<p className="text-xs leading-relaxed text-primary">
Due to a{" "}
<a
rel="noreferrer"
target="_blank"
className="font-bold underline hover:text-accent"
href="https://github.com/brave/brave-browser/issues/19990"
>
recent Brave update (1.36.109)
</a>
, some results may not display correctly. Disabling, in Brave
settings, the{" "}
<span className="font-bold"> Restrict Websocket Pool </span>flag
and restart browser will help.
</p>
</div>
<div
className={`${
isOptions ? "justify-start" : "justify-between"
}" flex pt-2 pb-4 px-4 space-x-2 border-gray-200 dark:border-gray-600"`}
>
<Button
variant="default"
size="sm"
type="button"
onClick={() => {
// TODO: this should produce a react-style event instead of setting the value directly
setShowModal(false)
environment.set({ type: "braveSetting" }, true)
}}
>
Dismiss
</Button>
<Button
variant="default"
size="sm"
onClick={() => openInNewTab("https://github.com/brave/brave-browser/issues/19990")}
type="button"
>
Learn More
</Button>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,26 @@
import { FunctionComponent } from "react"
import { MdOutlineGridView } from "react-icons/md"
import "../main.css"
interface Props {
isWellKnown?: boolean
children?: string
}
export const IconWeb3: FunctionComponent<Props> = ({
children,
isWellKnown,
}) => {
return (
<>
{isWellKnown && children ? (
<>
<MdOutlineGridView />
<span className="flex items-center text-xl icon">{children}</span>
</>
) : (
<MdOutlineGridView />
)}
</>
)
}

View File

@ -0,0 +1,9 @@
import React from "react"
export type Props = {
children?: React.ReactNode
}
export const Layout: React.FC<Props> = ({ children }) => {
return <div className="bg-muted mx-auto px-6 py-8">{children}</div>
}

View File

@ -0,0 +1,39 @@
import React from "react"
import { cn } from "@/lib/utils"
export type Props = {
innerClassName?: string
className?: string
children?: React.ReactNode
}
/**
* This Layout is the new layout but is called Layout2 until all pages
* are migrated to the new layout.
*/
export const Layout2: React.FC<Props> = ({ children, className, innerClassName }) => {
return (
<main
className={cn(
"flex items-center justify-center",
"min-h-screen",
"font-sans",
className,
)}
>
<div
className={cn(
"bg-background",
"w-[400px] h-[600px]",
"flex flex-col",
"shadow-sm",
"rounded-none lg:rounded",
innerClassName
)}
>
{children}
</div>
</main>
)
}

68
src/components/Logo.tsx Normal file
View File

@ -0,0 +1,68 @@
const HeaderIcon: React.FC<IconProps> = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 722.49 211.56">
<defs>
<style>{`
.uuid-78f6a6af-10fe-4831-9ced-7187d1432e48 {
isolation: isolate;
}
.uuid-268138db-7fdf-4ed3-88f2-b8d32bba180e {
mix-blend-mode: color-burn;
}`}
</style>
</defs>
<g class="uuid-78f6a6af-10fe-4831-9ced-7187d1432e48">
<g id="uuid-c93f97e5-2f4a-47f0-9a31-d2f07858ae05" data-name="Layer 2">
<g id="uuid-441c66b0-dfc2-459a-90e6-28d9bd55620b" data-name="ghostWallet">
<g id="uuid-e61fd7bf-260d-4b17-b1fb-bb8f42140ffa" data-name="Wallet">
<g>
<path d="m499.19,76.35c1.07,3.6,2.2,7.35,3.4,11.25,1.2,3.9,2.45,7.8,3.75,11.7,1.3,3.9,2.6,7.73,3.9,11.5,1.3,3.77,2.55,7.32,3.75,10.65,1-3.6,1.97-7.52,2.9-11.75.93-4.23,1.87-8.63,2.8-13.2.93-4.57,1.85-9.22,2.75-13.95.9-4.73,1.75-9.4,2.55-14h13c-2.33,12.53-4.93,24.58-7.79,36.15-2.86,11.57-6.06,22.62-9.59,33.15h-12.12c-5.17-13.47-10.17-28.17-15-44.1-2.47,8.07-4.97,15.73-7.51,23-2.54,7.27-5.07,14.3-7.61,21.1h-12.02c-3.59-10.53-6.8-21.58-9.63-33.15-2.83-11.57-5.4-23.62-7.73-36.15h13.5c.8,4.53,1.65,9.17,2.55,13.9.9,4.73,1.83,9.38,2.8,13.95.97,4.57,1.95,8.98,2.95,13.25,1,4.27,2,8.2,3,11.8,1.27-3.4,2.55-6.98,3.85-10.75,1.3-3.77,2.58-7.6,3.85-11.5,1.27-3.9,2.48-7.78,3.65-11.65,1.17-3.87,2.25-7.6,3.25-11.2h10.8Z"/>
<path d="m557.79,84.25c4,0,7.37.5,10.1,1.5,2.73,1,4.92,2.4,6.55,4.2,1.63,1.8,2.8,3.98,3.5,6.55.7,2.57,1.05,5.38,1.05,8.45v31.7c-1.87.4-4.68.88-8.45,1.45-3.77.57-8.02.85-12.75.85-3.13,0-6-.3-8.6-.9-2.6-.6-4.82-1.57-6.65-2.9-1.83-1.33-3.27-3.07-4.3-5.2-1.03-2.13-1.55-4.77-1.55-7.9s.58-5.53,1.75-7.6c1.17-2.07,2.75-3.75,4.75-5.05,2-1.3,4.32-2.23,6.95-2.8,2.63-.57,5.38-.85,8.25-.85,1.33,0,2.73.08,4.2.25,1.46.17,3.03.45,4.7.85v-2c0-1.4-.17-2.73-.5-4-.33-1.27-.92-2.38-1.75-3.35-.83-.97-1.93-1.72-3.3-2.25-1.37-.53-3.08-.8-5.15-.8-2.8,0-5.37.2-7.7.6-2.33.4-4.23.87-5.7,1.4l-1.5-9.8c1.53-.53,3.77-1.07,6.7-1.6,2.93-.53,6.07-.8,9.4-.8Zm1,45.1c3.73,0,6.57-.2,8.5-.6v-13.4c-.67-.2-1.63-.4-2.9-.6-1.27-.2-2.67-.3-4.2-.3-1.33,0-2.68.1-4.05.3-1.37.2-2.6.57-3.7,1.1-1.1.53-1.98,1.28-2.65,2.25-.67.97-1,2.18-1,3.65,0,2.87.9,4.85,2.7,5.95s4.23,1.65,7.3,1.65Z"/>
<path d="m606.99,138.85c-3.6-.07-6.58-.47-8.95-1.2-2.37-.73-4.25-1.78-5.65-3.15-1.4-1.37-2.38-3.07-2.95-5.1-.57-2.03-.85-4.35-.85-6.95v-60.2l12.1-2v59.9c0,1.47.12,2.7.35,3.7.23,1,.65,1.85,1.25,2.55.6.7,1.42,1.23,2.45,1.6,1.03.37,2.35.65,3.95.85l-1.7,10Z"/>
<path d="m631.69,138.85c-3.6-.07-6.58-.47-8.95-1.2-2.37-.73-4.25-1.78-5.65-3.15-1.4-1.37-2.38-3.07-2.95-5.1-.57-2.03-.85-4.35-.85-6.95v-60.2l12.1-2v59.9c0,1.47.12,2.7.35,3.7.23,1,.65,1.85,1.25,2.55.6.7,1.42,1.23,2.45,1.6,1.03.37,2.35.65,3.95.85l-1.7,10Z"/>
<path d="m635.29,111.95c0-4.6.68-8.63,2.05-12.1,1.37-3.47,3.18-6.35,5.45-8.65,2.27-2.3,4.87-4.03,7.8-5.2,2.93-1.17,5.93-1.75,9-1.75,7.2,0,12.82,2.23,16.85,6.7,4.03,4.47,6.05,11.13,6.05,20,0,.67-.02,1.42-.05,2.25-.03.83-.08,1.58-.15,2.25h-34.5c.33,4.2,1.82,7.45,4.45,9.75,2.63,2.3,6.45,3.45,11.45,3.45,2.93,0,5.62-.27,8.05-.8,2.43-.53,4.35-1.1,5.75-1.7l1.6,9.9c-.67.33-1.58.68-2.75,1.05-1.17.37-2.5.7-4,1s-3.12.55-4.85.75c-1.73.2-3.5.3-5.3.3-4.6,0-8.6-.68-12-2.05-3.4-1.37-6.2-3.27-8.4-5.7-2.2-2.43-3.83-5.3-4.9-8.6-1.07-3.3-1.6-6.92-1.6-10.85Zm35.1-5.4c0-1.67-.23-3.25-.7-4.75-.47-1.5-1.15-2.8-2.05-3.9-.9-1.1-2-1.97-3.3-2.6-1.3-.63-2.85-.95-4.65-.95s-3.5.35-4.9,1.05c-1.4.7-2.58,1.62-3.55,2.75-.97,1.13-1.72,2.43-2.25,3.9-.53,1.47-.9,2.97-1.1,4.5h22.5Z"/>
<path d="m689.69,71.75l12.1-2v15.8h18.6v10.1h-18.6v21.3c0,4.2.67,7.2,2,9,1.33,1.8,3.6,2.7,6.8,2.7,2.2,0,4.15-.23,5.85-.7,1.7-.47,3.05-.9,4.05-1.3l2,9.6c-1.4.6-3.23,1.22-5.5,1.85-2.27.63-4.93.95-8,.95-3.73,0-6.85-.5-9.35-1.5s-4.48-2.45-5.95-4.35c-1.47-1.9-2.5-4.2-3.1-6.9-.6-2.7-.9-5.78-.9-9.25v-45.3Z"/>
</g>
<g>
<path d="m499.19,76.35c1.07,3.6,2.2,7.35,3.4,11.25,1.2,3.9,2.45,7.8,3.75,11.7,1.3,3.9,2.6,7.73,3.9,11.5,1.3,3.77,2.55,7.32,3.75,10.65,1-3.6,1.97-7.52,2.9-11.75.93-4.23,1.87-8.63,2.8-13.2.93-4.57,1.85-9.22,2.75-13.95.9-4.73,1.75-9.4,2.55-14h13c-2.33,12.53-4.93,24.58-7.79,36.15-2.86,11.57-6.06,22.62-9.59,33.15h-12.12c-5.17-13.47-10.17-28.17-15-44.1-2.47,8.07-4.97,15.73-7.51,23-2.54,7.27-5.07,14.3-7.61,21.1h-12.02c-3.59-10.53-6.8-21.58-9.63-33.15-2.83-11.57-5.4-23.62-7.73-36.15h13.5c.8,4.53,1.65,9.17,2.55,13.9.9,4.73,1.83,9.38,2.8,13.95.97,4.57,1.95,8.98,2.95,13.25,1,4.27,2,8.2,3,11.8,1.27-3.4,2.55-6.98,3.85-10.75,1.3-3.77,2.58-7.6,3.85-11.5,1.27-3.9,2.48-7.78,3.65-11.65,1.17-3.87,2.25-7.6,3.25-11.2h10.8Z"/>
<path d="m557.79,84.25c4,0,7.37.5,10.1,1.5,2.73,1,4.92,2.4,6.55,4.2,1.63,1.8,2.8,3.98,3.5,6.55.7,2.57,1.05,5.38,1.05,8.45v31.7c-1.87.4-4.68.88-8.45,1.45-3.77.57-8.02.85-12.75.85-3.13,0-6-.3-8.6-.9-2.6-.6-4.82-1.57-6.65-2.9-1.83-1.33-3.27-3.07-4.3-5.2-1.03-2.13-1.55-4.77-1.55-7.9s.58-5.53,1.75-7.6c1.17-2.07,2.75-3.75,4.75-5.05,2-1.3,4.32-2.23,6.95-2.8,2.63-.57,5.38-.85,8.25-.85,1.33,0,2.73.08,4.2.25,1.46.17,3.03.45,4.7.85v-2c0-1.4-.17-2.73-.5-4-.33-1.27-.92-2.38-1.75-3.35-.83-.97-1.93-1.72-3.3-2.25-1.37-.53-3.08-.8-5.15-.8-2.8,0-5.37.2-7.7.6-2.33.4-4.23.87-5.7,1.4l-1.5-9.8c1.53-.53,3.77-1.07,6.7-1.6,2.93-.53,6.07-.8,9.4-.8Zm1,45.1c3.73,0,6.57-.2,8.5-.6v-13.4c-.67-.2-1.63-.4-2.9-.6-1.27-.2-2.67-.3-4.2-.3-1.33,0-2.68.1-4.05.3-1.37.2-2.6.57-3.7,1.1-1.1.53-1.98,1.28-2.65,2.25-.67.97-1,2.18-1,3.65,0,2.87.9,4.85,2.7,5.95s4.23,1.65,7.3,1.65Z"/>
<path d="m606.99,138.85c-3.6-.07-6.58-.47-8.95-1.2-2.37-.73-4.25-1.78-5.65-3.15-1.4-1.37-2.38-3.07-2.95-5.1-.57-2.03-.85-4.35-.85-6.95v-60.2l12.1-2v59.9c0,1.47.12,2.7.35,3.7.23,1,.65,1.85,1.25,2.55.6.7,1.42,1.23,2.45,1.6,1.03.37,2.35.65,3.95.85l-1.7,10Z"/>
<path d="m631.69,138.85c-3.6-.07-6.58-.47-8.95-1.2-2.37-.73-4.25-1.78-5.65-3.15-1.4-1.37-2.38-3.07-2.95-5.1-.57-2.03-.85-4.35-.85-6.95v-60.2l12.1-2v59.9c0,1.47.12,2.7.35,3.7.23,1,.65,1.85,1.25,2.55.6.7,1.42,1.23,2.45,1.6,1.03.37,2.35.65,3.95.85l-1.7,10Z"/>
<path d="m635.29,111.95c0-4.6.68-8.63,2.05-12.1,1.37-3.47,3.18-6.35,5.45-8.65,2.27-2.3,4.87-4.03,7.8-5.2,2.93-1.17,5.93-1.75,9-1.75,7.2,0,12.82,2.23,16.85,6.7,4.03,4.47,6.05,11.13,6.05,20,0,.67-.02,1.42-.05,2.25-.03.83-.08,1.58-.15,2.25h-34.5c.33,4.2,1.82,7.45,4.45,9.75,2.63,2.3,6.45,3.45,11.45,3.45,2.93,0,5.62-.27,8.05-.8,2.43-.53,4.35-1.1,5.75-1.7l1.6,9.9c-.67.33-1.58.68-2.75,1.05-1.17.37-2.5.7-4,1s-3.12.55-4.85.75c-1.73.2-3.5.3-5.3.3-4.6,0-8.6-.68-12-2.05-3.4-1.37-6.2-3.27-8.4-5.7-2.2-2.43-3.83-5.3-4.9-8.6-1.07-3.3-1.6-6.92-1.6-10.85Zm35.1-5.4c0-1.67-.23-3.25-.7-4.75-.47-1.5-1.15-2.8-2.05-3.9-.9-1.1-2-1.97-3.3-2.6-1.3-.63-2.85-.95-4.65-.95s-3.5.35-4.9,1.05c-1.4.7-2.58,1.62-3.55,2.75-.97,1.13-1.72,2.43-2.25,3.9-.53,1.47-.9,2.97-1.1,4.5h22.5Z"/>
<path d="m689.69,71.75l12.1-2v15.8h18.6v10.1h-18.6v21.3c0,4.2.67,7.2,2,9,1.33,1.8,3.6,2.7,6.8,2.7,2.2,0,4.15-.23,5.85-.7,1.7-.47,3.05-.9,4.05-1.3l2,9.6c-1.4.6-3.23,1.22-5.5,1.85-2.27.63-4.93.95-8,.95-3.73,0-6.85-.5-9.35-1.5s-4.48-2.45-5.95-4.35c-1.47-1.9-2.5-4.2-3.1-6.9-.6-2.7-.9-5.78-.9-9.25v-45.3Z"/>
</g>
</g>
<g id="uuid-13b91de4-3fc3-44cf-bfc6-f264768961b4" data-name="ghost">
<path d="m258.52,132.56c0,8.33-2.12,14.43-6.35,18.3-4.23,3.87-10.72,5.8-19.45,5.8-3.2,0-6.32-.27-9.35-.8-3.03-.53-5.78-1.23-8.25-2.1l2.2-10.3c2.07.87,4.42,1.57,7.05,2.1,2.63.53,5.48.8,8.55.8,4.87,0,8.33-1,10.4-3,2.07-2,3.1-4.97,3.1-8.9v-2c-1.2.6-2.78,1.2-4.75,1.8-1.97.6-4.22.9-6.75.9-3.33,0-6.38-.53-9.15-1.6-2.77-1.07-5.13-2.63-7.1-4.7-1.97-2.07-3.5-4.65-4.6-7.75-1.1-3.1-1.65-6.68-1.65-10.75,0-3.8.58-7.3,1.75-10.5,1.17-3.2,2.87-5.93,5.1-8.2,2.23-2.27,4.95-4.03,8.15-5.3,3.2-1.27,6.83-1.9,10.9-1.9s7.67.3,11.2.9c3.53.6,6.53,1.23,9,1.9v45.3Zm-33.7-22.2c0,5.13,1.12,8.88,3.35,11.25,2.23,2.37,5.12,3.55,8.65,3.55,1.93,0,3.75-.27,5.45-.8,1.7-.53,3.08-1.17,4.15-1.9v-27.1c-.87-.2-1.93-.38-3.2-.55-1.27-.17-2.87-.25-4.8-.25-4.4,0-7.77,1.45-10.1,4.35-2.33,2.9-3.5,6.72-3.5,11.45Z"/>
<path d="m266.42,137.85V62.25l12.1-2v25.9c1.33-.47,2.88-.87,4.65-1.2,1.77-.33,3.52-.5,5.25-.5,4.2,0,7.68.58,10.45,1.75,2.77,1.17,4.98,2.8,6.65,4.9,1.67,2.1,2.85,4.62,3.55,7.55.7,2.93,1.05,6.2,1.05,9.8v29.4h-12.1v-27.5c0-2.8-.18-5.18-.55-7.15-.37-1.97-.97-3.57-1.8-4.8-.83-1.23-1.95-2.13-3.35-2.7-1.4-.57-3.13-.85-5.2-.85-1.6,0-3.23.17-4.9.5-1.67.33-2.9.63-3.7.9v41.6h-12.1Z"/>
<path d="m366.92,111.65c0,4.13-.6,7.9-1.8,11.3-1.2,3.4-2.9,6.3-5.1,8.7-2.2,2.4-4.85,4.27-7.95,5.6-3.1,1.33-6.52,2-10.25,2s-7.13-.67-10.2-2c-3.07-1.33-5.7-3.2-7.9-5.6s-3.92-5.3-5.15-8.7c-1.23-3.4-1.85-7.17-1.85-11.3s.62-7.88,1.85-11.25c1.23-3.37,2.96-6.25,5.2-8.65,2.23-2.4,4.88-4.25,7.95-5.55,3.07-1.3,6.43-1.95,10.1-1.95s7.05.65,10.15,1.95c3.1,1.3,5.75,3.15,7.95,5.55,2.2,2.4,3.92,5.29,5.15,8.65,1.23,3.37,1.85,7.12,1.85,11.25Zm-12.4,0c0-5.2-1.12-9.32-3.35-12.35-2.23-3.03-5.35-4.55-9.35-4.55s-7.12,1.52-9.35,4.55c-2.23,3.03-3.35,7.15-3.35,12.35s1.12,9.43,3.35,12.5c2.23,3.07,5.35,4.6,9.35,4.6s7.12-1.53,9.35-4.6c2.23-3.07,3.35-7.23,3.35-12.5Z"/>
<path d="m387.52,129.15c3.2,0,5.53-.38,7-1.15,1.47-.77,2.2-2.08,2.2-3.95,0-1.73-.78-3.17-2.35-4.3-1.57-1.13-4.15-2.37-7.75-3.7-2.2-.8-4.22-1.65-6.05-2.55-1.83-.9-3.42-1.95-4.75-3.15-1.33-1.2-2.38-2.65-3.15-4.35-.77-1.7-1.15-3.78-1.15-6.25,0-4.8,1.77-8.58,5.3-11.35,3.53-2.77,8.33-4.15,14.4-4.15,3.07,0,6,.29,8.8.85,2.8.57,4.9,1.12,6.3,1.65l-2.2,9.8c-1.33-.6-3.04-1.15-5.1-1.65-2.07-.5-4.47-.75-7.2-.75-2.47,0-4.47.42-6,1.25-1.53.83-2.3,2.12-2.3,3.85,0,.87.15,1.63.45,2.3.3.67.82,1.28,1.55,1.85.73.57,1.7,1.13,2.9,1.7,1.2.57,2.67,1.15,4.4,1.75,2.87,1.07,5.3,2.12,7.3,3.15,2,1.04,3.65,2.2,4.95,3.5s2.25,2.79,2.85,4.45c.6,1.67.9,3.67.9,6,0,5-1.85,8.78-5.55,11.35-3.7,2.57-8.98,3.85-15.85,3.85-4.6,0-8.3-.38-11.1-1.15-2.8-.77-4.77-1.38-5.9-1.85l2.1-10.1c1.8.73,3.95,1.43,6.45,2.1,2.5.67,5.35,1,8.55,1Z"/>
<path d="m414.52,71.75l12.1-2v15.8h18.6v10.1h-18.6v21.3c0,4.2.67,7.2,2,9,1.33,1.8,3.6,2.7,6.8,2.7,2.2,0,4.15-.23,5.85-.7,1.7-.47,3.05-.9,4.05-1.3l2,9.6c-1.4.6-3.23,1.22-5.5,1.85-2.27.63-4.93.95-8,.95-3.73,0-6.85-.5-9.35-1.5s-4.48-2.45-5.95-4.35c-1.47-1.9-2.5-4.2-3.1-6.9-.6-2.7-.9-5.78-.9-9.25v-45.3Z"/>
</g>
<g id="uuid-31d568be-350a-4bf8-9ecd-aa0b82fa524a" data-name="Solid">
<path id="uuid-74c4d7ce-4375-4dd5-a130-f5450bf30268" data-name="Inner" class="uuid-268138db-7fdf-4ed3-88f2-b8d32bba180e" d="m92.11,176.98c-.13,0-.27,0-.4-.02l-.36-.05-5.28-1.06-1.67-.44c-.41-.11-.79-.21-1.17-.31-.77-.2-1.53-.4-2.29-.62l-.09-.03-3.37-1.11c-4.93-1.77-9.24-3.78-13.19-6.15-8.55-5.19-15.79-12.3-20.95-20.58-5.08-8.09-8.49-17.66-9.84-27.64-.69-5.18-.85-10.17-.47-14.83.4-4.83,1.23-9.01,2.53-12.78.52-1.51,1.67-2.74,3.13-3.37,1.47-.63,3.15-.63,4.61.03,2.76,1.24,4.08,4.42,3.01,7.24-1.07,2.82-1.82,6.16-2.24,9.93-.43,3.88-.42,8.09.04,12.51.91,8.55,3.57,16.78,7.69,23.79,4.16,7.13,10.14,13.35,17.29,17.98,3.34,2.14,7.07,4.01,11.38,5.7l3.1,1.12c.63.21,1.27.39,1.9.58.4.12.81.23,1.21.36l1.5.44,5.03,1.17c1.99.56,3.21,2.49,2.9,4.6-.3,2.06-2.03,3.56-4.03,3.56Zm60.42-101.05c1.37-.81,2.34-2.17,2.67-3.73.33-1.56-.01-3.21-.94-4.51-2.62-3.69-5.84-7.07-9.55-10.03-3.98-3.2-8.44-6-13.27-8.31-9.41-4.51-19.81-6.93-30.08-6.99-5.27-.05-10.45.57-15.47,1.78-5,1.21-9.88,3.07-14.53,5.53-4.38,2.32-8.66,5.23-12.74,8.69-3.59,3.14-7.01,6.72-10.45,10.95l-.3.43c-1.02,1.7-.62,3.9.95,5.23,1.61,1.36,3.94,1.35,5.44-.03l.29-.3c3.24-3.76,6.41-6.91,9.66-9.6,3.63-2.91,7.43-5.35,11.3-7.26,4.03-1.99,8.23-3.46,12.49-4.37,4.29-.91,8.72-1.29,13.17-1.15,8.78.3,17.6,2.6,25.52,6.68,4.02,2.06,7.7,4.51,10.96,7.28,2.85,2.4,5.28,5.09,7.22,7.99,1.09,1.64,2.9,2.53,4.74,2.53,1,0,2-.26,2.92-.81Zm-33.71,101.23c8.14-2.46,15.81-6.42,22.8-11.75,7.53-5.76,13.88-12.95,18.38-20.77,4.67-8.09,7.51-17.26,8.22-26.49.72-8.52-.39-17.67-3.31-27.18l-.1-.31-.15-.29c-.93-1.81-3.03-2.66-4.99-2.02-1.98.65-3.19,2.65-2.82,4.65l.07.29c2.34,8.54,3.09,16.6,2.23,23.99-.85,7.85-3.5,15.56-7.67,22.29-4.03,6.55-9.65,12.51-16.24,17.24-5.99,4.29-12.77,7.53-19.61,9.37-2.96.79-4.78,3.87-4.05,6.86.37,1.52,1.36,2.83,2.72,3.61.87.5,1.86.75,2.85.75.56,0,1.12-.08,1.66-.25Z"/>
<path id="uuid-3a66e28c-1db5-41c3-8097-56a1d6ecbc3c" data-name="Outer" class="uuid-268138db-7fdf-4ed3-88f2-b8d32bba180e" d="m200.31,158.83c-.09,9.87-8.19,17.83-18.06,17.74-.96,0-1.9-.09-2.81-.25-5.68,6.4-12.12,12.17-19.09,17.06-11.88,8.36-25.54,14.12-39.48,16.66-5.79,1.07-11.71,1.57-17.66,1.52-8.37-.08-16.78-1.26-24.91-3.52-12.48-3.38-25.13-9.66-37.58-18.66l-.11-.08c-1.95-1.51-2.34-4.3-.88-6.34,1.43-2.02,4.17-2.59,6.22-1.3l.1.07c11.78,7.87,23.53,13.2,34.93,15.85,12.43,2.98,25.38,3.13,37.47.44,12.06-2.66,23.75-8.1,33.82-15.74,5.47-4.14,10.51-8.91,14.97-14.13-1.75-2.79-2.75-6.09-2.71-9.62.09-9.87,8.19-17.83,18.07-17.74,9.87.09,17.83,8.19,17.74,18.07Zm-158.13-.57c-1.77-4.44-5.17-7.93-9.56-9.81-3.27-1.4-6.72-1.75-9.99-1.2-3.68-7.45-6.35-15.61-7.88-24.17-2.6-14.4-1.83-29.01,2.22-42.26,2.09-6.84,5.06-13.35,8.83-19.36,3.73-5.95,8.33-11.5,13.65-16.51,5.03-4.74,10.89-9.13,17.34-13.02,5.98-3.48,12.52-6.58,19.97-9.47l.59-.23.49-.39c1.58-1.26,2.18-3.4,1.49-5.32-.8-2.21-3.1-3.48-5.36-2.95l-.42.12c-8.04,2.82-15.13,5.92-21.75,9.49-7.27,4.07-13.88,8.71-19.62,13.78-6.19,5.45-11.59,11.58-16.05,18.2-4.54,6.74-8.19,14.1-10.84,21.87C.14,92.06-1.28,108.75,1.16,125.3c1.56,10.65,4.68,20.85,9.16,30.18-.46.74-.87,1.52-1.22,2.35-3.9,9.07.31,19.62,9.39,23.51,2.12.91,4.32,1.38,6.5,1.44,7.14.22,14.03-3.88,17.02-10.83,1.89-4.39,1.95-9.26.18-13.7Zm55.75-123.09c1.44.4,2.91.61,4.37.64,3.2.07,6.37-.73,9.23-2.35,2.92-1.67,5.25-4.07,6.82-6.95,7.24,1.49,14.46,3.9,21.38,7.19,12.16,5.82,23,14.16,31.34,24.13,8.54,10.16,14.66,22.39,17.7,35.41,2.93,12.08,3.22,25.66.87,40.35l-.05.52c-.07,2.31,1.61,4.33,3.91,4.7.22.04.44.05.66.06,1.83.04,3.52-.96,4.33-2.63l.24-.48.1-.53c3.11-15.89,3.27-30.81.5-44.31-2.92-14.81-9.39-28.93-18.71-40.82-9.1-11.67-21.11-21.6-34.75-28.72-8.45-4.37-17.37-7.59-26.38-9.58-.34-.94-.75-1.87-1.26-2.76-2.37-4.15-6.21-7.14-10.82-8.4-4.61-1.26-9.44-.66-13.59,1.71-4.15,2.37-7.14,6.21-8.4,10.82-1.26,4.61-.66,9.44,1.71,13.59,2.37,4.15,6.21,7.14,10.82,8.4Z"/>
</g>
<g id="uuid-591b2e65-6fc2-422c-8835-abb238a7ba2d" data-name="Wallet Icon">
<path d="m103.3,132.26c-4.84,0-8.8-3.96-8.8-8.8v-22.41c0-4.84,3.96-8.8,8.8-8.8h30.04c-1.67-4.82-6.25-8.31-11.62-8.31h-45.37c-6.77,0-12.31,5.54-12.31,12.31v31.37c0,6.77,5.54,12.31,12.31,12.31h45.37c5.14,0,9.56-3.19,11.4-7.69h-29.82Z"/>
<path d="m132.49,96.24h-25.96c-3.88,0-7.05,3.17-7.05,7.05v17.95c0,3.88,3.17,7.05,7.05,7.05h25.96c3.88,0,7.05-3.17,7.05-7.05v-17.95c0-3.88-3.17-7.05-7.05-7.05Zm-23.45,20.71c-2.49,0-4.5-2.01-4.5-4.5s2.01-4.5,4.5-4.5,4.5,2.01,4.5,4.5-2.01,4.5-4.5,4.5Z"/>
</g>
</g>
</g>
</g>
</svg>
);
interface LogoProps {
cName?: string
}
const Logo = ({ cName }: LogoProps) => {
return (
<div className={cName}>
<HeaderIcon />
</div>
)
}
export default Logo

View File

@ -0,0 +1,15 @@
import React from "react"
type MenuContentProps = {
children: React.ReactNode
}
export const MenuContent = ({ children }: MenuContentProps) => (
<div className="flex flex-col w-full min-w-0 mb-6 break-words rounded">
<div className="flex-auto px-4 py-5">
<div className="tab-content tab-space">
<div className="block">{children}</div>
</div>
</div>
</div>
)

49
src/components/Switch.tsx Normal file
View File

@ -0,0 +1,49 @@
import { useEffect, useState } from "react"
type manipulateBootnodeType = (
bootnode: string,
add: boolean,
defaultBootnode: boolean,
) => void
interface SwitchProps {
bootnode: string
alterBootnodes: manipulateBootnodeType
defaultBootnode: boolean
isChecked: boolean
}
const Switch = ({
bootnode,
alterBootnodes,
defaultBootnode,
isChecked,
}: SwitchProps) => {
const [checked, setChecked] = useState<boolean>(isChecked)
useEffect(() => {
setChecked(isChecked)
}, [isChecked])
return (
<div className="flex w-1/12 ml-8" key={bootnode}>
<label className="inline-flex relative items-center mr-5 cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={checked}
readOnly
/>
<div
onClick={() => {
alterBootnodes(bootnode, !checked, defaultBootnode)
setChecked(!checked)
}}
className="w-11 h-6 bg-background rounded-full peer peer-focus:ring-accent peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-muted after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-foreground"
></div>
</label>
</div>
)
}
export { Switch }

30
src/components/Title.tsx Normal file
View File

@ -0,0 +1,30 @@
import { ReactNode } from "react"
import { BsThreeDots } from "react-icons/bs"
interface TitleProps {
children: ReactNode
titleType?: "small" | "normal" | "large"
showOptions?: boolean
}
const Title = ({
children,
titleType = "normal",
showOptions = false,
className = "",
}: TitleProps) => {
const cName = className + (titleType === "small"
? " text-sm text-muted-foreground"
: titleType === "large"
? " text-lg font-bold"
: " text-base font-bold")
return (
<div className={"flex justify-between mb-4 ".concat(cName)}>
<div className="capitalize">{children}</div>
{showOptions && <BsThreeDots className="cursor-pointer" />}
</div>
)
}
export { Title }

8
src/components/index.tsx Normal file
View File

@ -0,0 +1,8 @@
export { default as Logo } from "./Logo"
export { default as light } from "./theme"
export { MenuContent } from "./MenuContent"
export { BraveModal } from "./BraveModal"
export { IconWeb3 } from "./IconWeb3"
export { Bootnodes } from "./Bootnodes"
export { Title } from "./Title"
export { Switch } from "./Switch"

150
src/components/theme.tsx Normal file
View File

@ -0,0 +1,150 @@
export const substrateGreen = {
100: "#7E8D96",
200: "#5CFFC8",
300: "#18FFB2",
400: "#16DB9A",
500: "#11B37C",
600: "#1A9A6C",
}
const red = {
50: "#ffebee",
100: "#ffcdd2",
200: "#ef9a9a",
300: "#e57373",
400: "#ef5350",
500: "#f44336",
600: "#e53935",
700: "#d32f2f",
800: "#c62828",
900: "#b71c1c",
A100: "#ff8a80",
A200: "#ff5252",
A400: "#ff1744",
A700: "#d50000",
}
const grey = {
50: "#fafafa",
100: "#f5f5f5",
200: "#eeeeee",
300: "#e0e0e0",
400: "#bdbdbd",
500: "#9e9e9e",
600: "#757575",
700: "#616161",
800: "#424242",
900: "#212121",
A100: "#d5d5d5",
A200: "#aaaaaa",
A400: "#303030",
A700: "#616161",
}
const palette = {
type: "light",
common: {
black: "black",
white: "white",
},
background: {
paper: "white",
default: "white",
},
primary: {
light: substrateGreen[100],
main: substrateGreen[400],
dark: substrateGreen[500],
contrastText: "black",
},
secondary: {
light: "#78B1D0",
main: "#78B1D0",
dark: "#78B1D0",
contrastText: "#000000",
},
error: {
light: red[100],
main: "#FF3014",
dark: red[500],
contrastText: "black",
},
text: {
primary: grey[800],
secondary: grey[500],
disabled: grey[300],
hint: grey[700],
},
action: {
active: substrateGreen[300],
},
divider: grey[200],
}
const light = {
typography: {
fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, Segoe UI, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Ubuntu-Regular"',
h1: {
fontWeight: 500,
fontSize: 30,
lineHeight: "120%",
},
h2: {
fontWeight: 500,
fontSize: 20,
lineHeight: "120%",
letterSpacing: -0.02,
textTransform: "capitalize",
},
h3: {
fontWeight: 600,
fontSize: 17,
lineHeight: "120%",
},
h4: {
fontWeight: 500,
fontSize: 15,
lineHeight: "120%",
color: grey[800],
},
body1: {
fontWeight: 400,
fontSize: 14,
lineHeight: "135%",
letterSpacing: 0.15,
},
body2: {
fontWeight: 400,
fontSize: 11,
lineHeight: "135%",
},
button: {
fontWeight: 500,
fontSize: 14,
lineHeight: "120%",
letterSpacing: 0.2,
textTransform: "none",
},
subtitle1: {
fontFamily: "SFMono-Regular, Consolas , Liberation Mono, Menlo, monospace",
fontWeight: 400,
fontSize: 20,
lineHeight: "120%",
},
subtitle2: {
fontFamily: "SFMono-Regular, Consolas , Liberation Mono, Menlo, monospace",
fontWeight: 400,
fontSize: 12,
lineHeight: "135%",
letterSpacing: 0.1,
},
overline: {
fontSize: 11,
lineHeight: "120%",
letterSpacing: 0.7,
},
},
palette: palette,
}
export default light

View File

@ -0,0 +1,56 @@
import * as React from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const Accordion = AccordionPrimitive.Root
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
))
AccordionItem.displayName = "AccordionItem"
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
))
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }

View File

@ -0,0 +1,139 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className,
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className,
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@ -0,0 +1,5 @@
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
const AspectRatio = AspectRatioPrimitive.Root
export { AspectRatio }

View File

@ -0,0 +1,48 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className,
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
},
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,115 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className,
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@ -0,0 +1,55 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-foreground hover:text-background",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary-foreground hover:text-secondary",
ghost: "text-primary hover:bg-accent hover:text-background",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
full: "h-10 w-full",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
},
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,64 @@
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100",
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: () => <ChevronLeft className="h-4 w-4" />,
IconRight: () => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View File

@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"border-t border-primary bg-card shadow-sm",
className,
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("px-6", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,260 @@
import * as React from "react"
import useEmblaCarousel, {
type UseEmblaCarouselType,
} from "embla-carousel-react"
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
type CarouselApi = UseEmblaCarouselType[1]
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
type CarouselOptions = UseCarouselParameters[0]
type CarouselPlugin = UseCarouselParameters[1]
type CarouselProps = {
opts?: CarouselOptions
plugins?: CarouselPlugin
orientation?: "horizontal" | "vertical"
setApi?: (api: CarouselApi) => void
}
type CarouselContextProps = {
carouselRef: ReturnType<typeof useEmblaCarousel>[0]
api: ReturnType<typeof useEmblaCarousel>[1]
scrollPrev: () => void
scrollNext: () => void
canScrollPrev: boolean
canScrollNext: boolean
} & CarouselProps
const CarouselContext = React.createContext<CarouselContextProps | null>(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & CarouselProps
>(
(
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref,
) => {
const [carouselRef, api] = useEmblaCarousel(
{
...opts,
axis: orientation === "horizontal" ? "x" : "y",
},
plugins,
)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api: CarouselApi) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
},
[scrollPrev, scrollNext],
)
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
}
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}
>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}
>
{children}
</div>
</CarouselContext.Provider>
)
},
)
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className,
)}
{...props}
/>
</div>
)
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className,
)}
{...props}
/>
)
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}
>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
)
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof Button>
>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn(
"absolute h-8 w-8 rounded-full",
orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
className,
)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}
>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
)
})
CarouselNext.displayName = "CarouselNext"
export {
type CarouselApi,
Carousel,
CarouselContent,
CarouselItem,
CarouselPrevious,
CarouselNext,
}

View File

@ -0,0 +1,31 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50",
"data-[state=checked]:bg-accent data-[state=checked]:border-secondary data-[state=checked]:text-secondary",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,9 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@ -0,0 +1,153 @@
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<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
className,
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@ -0,0 +1,198 @@
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className,
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View File

@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@ -0,0 +1,116 @@
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@/lib/utils"
const Drawer = ({
shouldScaleBackground = true,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
<DrawerPrimitive.Root
shouldScaleBackground={shouldScaleBackground}
{...props}
/>
)
Drawer.displayName = "Drawer"
const DrawerTrigger = DrawerPrimitive.Trigger
const DrawerPortal = DrawerPrimitive.Portal
const DrawerClose = DrawerPrimitive.Close
const DrawerOverlay = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Overlay
ref={ref}
className={cn("fixed inset-0 z-50 bg-black/80", className)}
{...props}
/>
))
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
const DrawerContent = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DrawerPortal>
<DrawerOverlay />
<DrawerPrimitive.Content
ref={ref}
className={cn(
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
className,
)}
{...props}
>
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
{children}
</DrawerPrimitive.Content>
</DrawerPortal>
))
DrawerContent.displayName = "DrawerContent"
const DrawerHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
{...props}
/>
)
DrawerHeader.displayName = "DrawerHeader"
const DrawerFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
{...props}
/>
)
DrawerFooter.displayName = "DrawerFooter"
const DrawerTitle = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
))
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
const DrawerDescription = React.forwardRef<
React.ElementRef<typeof DrawerPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
>(({ className, ...props }, ref) => (
<DrawerPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}

View File

@ -0,0 +1,198 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

176
src/components/ui/form.tsx Normal file
View File

@ -0,0 +1,176 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { formItemId } = useFormField()
return (
<Label
ref={ref}
className={className}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -0,0 +1,27 @@
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@ -0,0 +1,69 @@
import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { Dot } from "lucide-react"
import { cn } from "@/lib/utils"
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName,
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
))
InputOTP.displayName = "InputOTP"
const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center", className)} {...props} />
))
InputOTPGroup.displayName = "InputOTPGroup"
const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
return (
<div
ref={ref}
className={cn(
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className,
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
)
})
InputOTPSlot.displayName = "InputOTPSlot"
const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
))
InputOTPSeparator.displayName = "InputOTPSeparator"
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }

View File

@ -0,0 +1,28 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background",
"file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
)
},
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,234 @@
import * as React from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const MenubarMenu = MenubarPrimitive.Menu
const MenubarGroup = MenubarPrimitive.Group
const MenubarPortal = MenubarPrimitive.Portal
const MenubarSub = MenubarPrimitive.Sub
const MenubarRadioGroup = MenubarPrimitive.RadioGroup
const Menubar = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
className,
)}
{...props}
/>
))
Menubar.displayName = MenubarPrimitive.Root.displayName
const MenubarTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className,
)}
{...props}
/>
))
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName
const MenubarSubTrigger = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</MenubarPrimitive.SubTrigger>
))
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName
const MenubarSubContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
))
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName
const MenubarContent = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
>(
(
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
ref,
) => (
<MenubarPrimitive.Portal>
<MenubarPrimitive.Content
ref={ref}
align={align}
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</MenubarPrimitive.Portal>
),
)
MenubarContent.displayName = MenubarPrimitive.Content.displayName
const MenubarItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className,
)}
{...props}
/>
))
MenubarItem.displayName = MenubarPrimitive.Item.displayName
const MenubarCheckboxItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.CheckboxItem>
))
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName
const MenubarRadioItem = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<MenubarPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</MenubarPrimitive.ItemIndicator>
</span>
{children}
</MenubarPrimitive.RadioItem>
))
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName
const MenubarLabel = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className,
)}
{...props}
/>
))
MenubarLabel.displayName = MenubarPrimitive.Label.displayName
const MenubarSeparator = React.forwardRef<
React.ElementRef<typeof MenubarPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName
const MenubarShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
/>
)
}
MenubarShortcut.displayname = "MenubarShortcut"
export {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
MenubarSeparator,
MenubarLabel,
MenubarCheckboxItem,
MenubarRadioGroup,
MenubarRadioItem,
MenubarPortal,
MenubarSubContent,
MenubarSubTrigger,
MenubarGroup,
MenubarSub,
MenubarShortcut,
}

View File

@ -0,0 +1,128 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDown } from "lucide-react"
import { cn } from "@/lib/utils"
const NavigationMenu = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Root
ref={ref}
className={cn(
"relative z-10 flex max-w-max flex-1 items-center justify-center",
className,
)}
{...props}
>
{children}
<NavigationMenuViewport />
</NavigationMenuPrimitive.Root>
))
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
const NavigationMenuList = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.List>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center space-x-1",
className,
)}
{...props}
/>
))
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
)
const NavigationMenuTrigger = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<NavigationMenuPrimitive.Trigger
ref={ref}
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDown
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
))
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
const NavigationMenuContent = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className,
)}
{...props}
/>
))
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
const NavigationMenuLink = NavigationMenuPrimitive.Link
const NavigationMenuViewport = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
>(({ className, ...props }, ref) => (
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className,
)}
ref={ref}
{...props}
/>
</div>
))
NavigationMenuViewport.displayName =
NavigationMenuPrimitive.Viewport.displayName
const NavigationMenuIndicator = React.forwardRef<
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
>(({ className, ...props }, ref) => (
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className,
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =
NavigationMenuPrimitive.Indicator.displayName
export {
navigationMenuTriggerStyle,
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
}

View File

@ -0,0 +1,117 @@
import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
Pagination.displayName = "Pagination"
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className,
)}
{...props}
/>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
PaginationNext.displayName = "PaginationNext"
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
PaginationEllipsis.displayName = "PaginationEllipsis"
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
}

View File

@ -0,0 +1,29 @@
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<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View File

@ -0,0 +1,26 @@
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@ -0,0 +1,49 @@
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, label, ...props }, ref) => {
return (
<div className="flex items-center">
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary bg-background text-accent ring-offset-background",
"focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
<label className="ml-2" htmlFor={props.id}>
{label}
</label>
</div>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View File

@ -0,0 +1,43 @@
import { GripVertical } from "lucide-react"
import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils"
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
className,
)}
{...props}
/>
)
const ResizablePanel = ResizablePrimitive.Panel
const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
withHandle?: boolean
}) => (
<ResizablePrimitive.PanelResizeHandle
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className,
)}
{...props}
>
{withHandle && (
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
)
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

View File

@ -0,0 +1,46 @@
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -0,0 +1,168 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input",
"bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground",
"text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border",
"bg-background text-primary shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95",
"data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2",
"data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2",
"text-sm outline-none text-primary focus:bg-accent focus:text-accent-foreground",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,29 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className,
)}
{...props}
/>
),
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

136
src/components/ui/sheet.tsx Normal file
View File

@ -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<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
ref={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=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
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<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className,
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@ -0,0 +1,26 @@
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className,
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

View File

@ -0,0 +1,29 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

117
src/components/ui/table.tsx Normal file
View File

@ -0,0 +1,117 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
className,
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -0,0 +1,53 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,27 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background",
"px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
)
},
)
Textarea.displayName = "Textarea"
export { Textarea }

126
src/components/ui/toast.tsx Normal file
View File

@ -0,0 +1,126 @@
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<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
))
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<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className,
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className,
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -0,0 +1,33 @@
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 (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@ -0,0 +1,59 @@
import * as React from "react"
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
import { VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { toggleVariants } from "@/components/ui/toggle"
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: "default",
variant: "default",
})
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn("flex items-center justify-center gap-1", className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
))
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext)
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className,
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
)
})
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
export { ToggleGroup, ToggleGroupItem }

View File

@ -0,0 +1,42 @@
import * as React from "react"
import * as TogglePrimitive from "@radix-ui/react-toggle"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline: "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",
sm: "h-9 px-2.5",
lg: "h-11 px-5",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
const Toggle = React.forwardRef<
React.ElementRef<typeof TogglePrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, ...props }, ref) => (
<TogglePrimitive.Root
ref={ref}
className={cn(toggleVariants({ variant, size, className }))}
{...props}
/>
))
Toggle.displayName = TogglePrimitive.Root.displayName
export { Toggle, toggleVariants }

View File

@ -0,0 +1,28 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@ -0,0 +1,190 @@
// 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<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
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<ToasterToast, "id">
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<State>(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 }

17
src/constants.ts Normal file
View File

@ -0,0 +1,17 @@
export const wellKnownGenesisHashByChainId: Record<string, string> = {
casper_staging_testnet: "0x07074eb5f47a6f4dd70430674e5174d5414bc055292b90392fb6f0a28c7524d1",
}
export const wellKnownChainIdByGenesisHash: Record<string, string> = {
"0x07074eb5f47a6f4dd70430674e5174d5414bc055292b90392fb6f0a28c7524d1": "casper_staging_testnet",
}
export const wellKnownPrefixByGenesisHash: Record<string, number> = {
"0x07074eb5f47a6f4dd70430674e5174d5414bc055292b90392fb6f0a28c7524d1": 1996,
}
export const wellKnownDecimalsByGenesisHash: Record<string, number> = {
"0x07074eb5f47a6f4dd70430674e5174d5414bc055292b90392fb6f0a28c7524d1": 18,
}
export const CHANNEL_ID = "ghost-extension"

View File

@ -0,0 +1,7 @@
import { Layout2 } from "@/components/Layout2"
export const EmptyPage = () => {
return (
<Layout2 />
)
}

199
src/containers/Options.tsx Normal file
View File

@ -0,0 +1,199 @@
import { FunctionComponent, useEffect, useMemo, useState } from "react"
import { MdOutlineNetworkCell, MdOutlineOnlinePrediction } from "react-icons/md"
import pckg from "../../package.json"
import { FaGithub } from "react-icons/fa"
import * as environment from "../environment"
import { BraveModal, Logo, MenuContent, Bootnodes } from "../components"
import { Link } from "react-router-dom"
import { useActiveChains } from "@/hooks/useActiveChains"
import { NetworkTabProps } from "@/types"
import { NetworkTab } from "./WalletPopup/components"
import { Accordion } from "@/components/ui/accordion"
type MenuItemTypes = "item" | "title" | "icon"
const item = [
"group",
"flex",
"items-center",
"text-base",
"text-primary",
"py-4",
"px-6",
"h-12",
"overflow-hidden",
"text-ellipsis",
"whitespace-nowrap",
"bg-background",
"md:justify-start",
"justify-center",
]
const itemInactive = [
"bg-transparent",
"transition",
"duration-300",
"ease-in-out",
]
const itemActive = [
"cursor-default",
]
const title = ["ml-4 font-inter font-medium text-primary"]
const titleInactive = ["group-hover:text-accent"]
const titleActive = ["text-accent", "cursor-default"]
const icon = [
"w-6",
"h-6",
"md:w-4",
"md:h-4",
"text-primary",
]
const iconInactive = ["group-hover:text-accent"]
const iconActive = ["cursor-default"]
const cName = (type: MenuItemTypes, menu = 0, reqMenu: number) => {
let classes: string[] = []
switch (type) {
case "item":
if (menu === reqMenu) {
classes = [...item, ...itemActive]
} else {
classes = [...item, ...itemInactive]
}
break
case "title":
if (menu === reqMenu) {
classes = [...title, ...titleActive]
} else {
classes = [...title, ...titleInactive]
}
break
case "icon":
if (menu === reqMenu) {
classes = [...icon, ...iconActive]
} else {
classes = [...icon, ...iconInactive]
}
break
}
return classes.join(" ")
}
const Networks: React.FC = () => {
const chains = useActiveChains()
const networks: NetworkTabProps[] = useMemo(
() =>
chains.map(({ chainName, isWellKnown, details }) => {
return {
isWellKnown,
name: chainName,
health: {
isSyncing: details[0].isSyncing,
peers: details[0].peers,
status: "connected",
bestBlockHeight: details[0].bestBlockHeight,
},
apps: details.map(({ url }) => ({
name: url ?? "",
url: url,
})),
}
}),
[chains],
)
return (
<section className="container">
<h2 className="pb-2 space-y-2 text-3xl font-semibold md:pb-4">
Networks
</h2>
{networks.length ? (
<Accordion type="multiple" className="w-full flex flex-col gap-4">
{networks.map(({ name, health, apps, isWellKnown }, i) => (
<NetworkTab
key={i}
name={name}
health={health}
isWellKnown={isWellKnown}
apps={apps}
/>
))}
</Accordion>
) : (
<div>The extension isn't connected to any network.</div>
)}
</section>
)
}
export const Options: FunctionComponent = () => {
const [menu, setMenu] = useState<0 | 1>(0)
const [showModal, setShowModal] = useState<boolean>(false)
useEffect(() => {
window.navigator?.brave?.isBrave().then(async (isBrave: any) => {
const braveSetting = await environment.get({ type: "braveSetting" })
setShowModal(isBrave && !braveSetting)
})
}, [])
return (
<>
<BraveModal show={showModal} isOptions={true} />
<div className="h-full flex">
<div className="h-[full] bg-muted flex flex-col md:w-60 w-[100px]">
<div className="md:px-6 md:pt-5 md:pb-2 p-2 pt-6">
<div className="flex items-center">
<div className="grow">
<div className="flex justify-center items-center">
<Logo cName="md:w-[80%] cursor-pointer fill-primary hover:fill-accent w-full" />
</div>
</div>
</div>
</div>
<ul className="relative pt-6">
<li className="relative">
<Link to="" className={cName("item", menu, 0)} onClick={() => setMenu(0)}>
<MdOutlineNetworkCell className={cName("icon", menu, 0)} />
<span className={`md:block hidden ${cName("title", menu, 0)}`} >Networks</span>
</Link>
</li>
<li className="relative">
<Link to="" className={cName("item", menu, 1)} onClick={() => setMenu(1)}>
<MdOutlineOnlinePrediction className={cName("icon", menu, 1)} />
<span className={`md:block hidden ${cName("title", menu, 1)}`} >Bootnodes</span>
</Link>
</li>
</ul>
<div className="w-full text-center flex-grow flex flex-col justify-end">
<hr className="m-0" />
<div className="block float-left px-6 py-4 cursor-pointer">
<Link
rel="noreferrer"
to="https://git.ghostchain.io/ghostchain/ghost-extension-wallet"
target="_blank"
className="flex md:flex-row flex-col gap-4 items-center"
>
<FaGithub className="w-8 h-8" />
<div className="block float-left text-xs text-left">
<div className="md:block hidden text-primary">Ghost Wallet Git</div>
<div className="text-accent">v {pckg.version}</div>
</div>
</Link>
</div>
</div>
</div>
<div className="w-[calc(100%-15rem)] h-[100vh] overflow-auto grow flex-grow">
<MenuContent>
{menu === 0 ? (
<Networks />
) : menu === 1 ? (
<Bootnodes />
) : null}
</MenuContent>
</div>
</div>
</>
)
}

View File

@ -0,0 +1,54 @@
import { HashRouter, Routes, Route } from "react-router-dom"
import { lazy, Suspense } from "react"
import { ProtectedRoute } from "./WalletPopup/components"
import { KeyringProvider } from "./WalletPopup/hooks"
import { EmptyPage } from "./EmptyPage"
// Dynamic Imports for code splitting
const UnlockKeyring = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.UnlockKeyring })))
const SignRequest = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.SignRequest })))
const Settings = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.Settings })))
const ChangePassword = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.ChangePassword })))
const Welcome = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.Welcome })))
const AddAccount = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.AddAccount })))
const SwitchAccount = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.SwitchAccount })))
const ImportAccounts = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.ImportAccounts })))
const AccountDetails = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.AccountDetails })))
const AddChainByUser = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.AddChainByUser })))
const Accounts = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.Accounts })))
const DeleteWallet = lazy(() => import("./WalletPopup/pages").then(module => ({ default: module.DeleteWallet })))
const Options = lazy(() => import("./Options").then(module => ({ default: module.Options })))
const CreatePassword = lazy(() => import("./WalletPopup/pages/CreatePassword").then(module => ({ default: module.CreatePassword })))
const Networks = lazy(() => import("./WalletPopup/pages/Networks").then(module => ({ default: module.Networks })))
export const WalletPopup = () => (
<HashRouter>
<KeyringProvider>
<Suspense fallback={<EmptyPage />}>
<Routes>
<Route element={<ProtectedRoute />}>
<Route path="/" element={<Accounts />} />
<Route path="/settings" element={<Settings />} />
<Route path="/change-password" element={<ChangePassword />} />
<Route path="/delete-wallet" element={<DeleteWallet />} />
<Route path="/accounts/:accountId" element={<AccountDetails />} />
<Route path="/accounts" element={<Accounts />} />
<Route path="/accounts/add" element={<AddAccount />} />
<Route path="/accounts/switch" element={<SwitchAccount />} />
<Route path="/accounts/import" element={<ImportAccounts />} />
<Route path="/sign-request/:signRequestId" element={<SignRequest />} />
<Route path="/add-chain-by-user" element={<AddChainByUser />} />
</Route>
<Route path="/options" element={<Options />} />
<Route path="/welcome" element={<Welcome />} />
<Route path="/networks" element={<Networks />} />
<Route path="/create-password" element={<CreatePassword />} />
<Route path="/unlock-keyring" element={<UnlockKeyring />} />
</Routes>
</Suspense>
</KeyringProvider>
</HashRouter>
)

Some files were not shown because too many files have changed in this diff Show More