This commit is contained in:
longdayi 2025-05-26 19:56:34 +08:00
parent ee771ba636
commit 32040f4457
255 changed files with 12183 additions and 889 deletions

View File

@ -2,7 +2,7 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
ignorePatterns: ["apps/**", "packages/**"],
extends: ["@workspace/eslint-config/library.js"],
extends: ["@repo/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,

3
.gitignore vendored
View File

@ -34,3 +34,6 @@ npm-debug.log*
# Misc
.DS_Store
*.pem
packages/db/generated

View File

@ -38,7 +38,7 @@ Your `globals.css` are already set up to use the components from the `ui` packag
To use the components in your app, import them from the `ui` package.
```tsx
import { Button } from '@workspace/ui/components/ui/button';
import { Button } from '@repo/ui/components/ui/button';
```
## More Resources

11
apps/backend/README.md Normal file
View File

@ -0,0 +1,11 @@
To install dependencies:
```sh
bun install
```
To run:
```sh
bun run dev
```
open http://localhost:3000

24
apps/backend/package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "backend",
"scripts": {
"dev": "bun run --hot src/index.ts"
},
"dependencies": {
"@elastic/elasticsearch": "^9.0.2",
"@hono/trpc-server": "^0.3.4",
"@hono/zod-validator": "^0.5.0",
"@repo/db": "workspace:*",
"@trpc/server": "11.1.2",
"@types/oidc-provider": "^9.1.0",
"hono": "^4.7.10",
"ioredis": "5.4.1",
"minio": "7.1.3",
"node-cron": "^4.0.7",
"oidc-provider": "^9.1.1",
"zod": "^3.25.23"
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^22.15.21"
}
}

View File

@ -0,0 +1,10 @@
import { Client } from '@elastic/elasticsearch';
export const esClient = new Client({
node: process.env.ELASTICSEARCH_NODE || 'http://localhost:9200',
auth: {
username: process.env.ELASTICSEARCH_USER || 'elastic',
password: process.env.ELASTICSEARCH_PASSWORD || 'changeme',
},
});

50
apps/backend/src/index.ts Normal file
View File

@ -0,0 +1,50 @@
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { contextStorage, getContext } from 'hono/context-storage'
import { prettyJSON } from 'hono/pretty-json'
import { trpcServer } from '@hono/trpc-server'
import { appRouter } from './trpc'
import Redis from 'ioredis'
import redis from './redis'
import minioClient from './minio'
import { Client } from 'minio'
import oidc from './oidc/provider'
type Env = {
Variables: {
redis: Redis
minio: Client
}
}
const app = new Hono<Env>()
app.use('*', async (c, next) => {
c.set('redis', redis)
c.set('minio', minioClient)
await next()
})
app.use('*', async (c, next) => {
c.set('redis', redis);
await next();
});
app.use(contextStorage())
app.use(prettyJSON()) // With options: prettyJSON({ space: 4 })
app.use(logger())
app.use(
'/trpc/*',
trpcServer({
router: appRouter,
})
)
app.get('/', (c) => {
return c.text('Hello Hono!')
})
app.all('/oidc/*', async (c) => {
// 让 oidc-provider 处理请求
return await oidc.callback(c.req.raw, c.res.raw);
});
export default app

13
apps/backend/src/minio.ts Normal file
View File

@ -0,0 +1,13 @@
// apps/backend/src/minio.ts
import { Client } from 'minio'
const minioClient = new Client({
endPoint: process.env.MINIO_ENDPOINT || 'localhost',
port: Number(process.env.MINIO_PORT) || 9000,
useSSL: process.env.MINIO_USE_SSL === 'true',
accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
})
export default minioClient

View File

@ -0,0 +1,15 @@
import { Configuration } from 'oidc-provider';
const config: Configuration = {
clients: [
{
client_id: 'example-client',
client_secret: 'example-secret',
grant_types: ['authorization_code'],
redirect_uris: ['http://localhost:3000/cb'],
},
],
// 其他配置项...
};
export default config;

View File

@ -0,0 +1,6 @@
import { Provider } from 'oidc-provider';
import config from './config';
const oidc = new Provider('http://localhost:4000', config);
export default oidc;

10
apps/backend/src/redis.ts Normal file
View File

@ -0,0 +1,10 @@
// apps/backend/src/redis.ts
import Redis from 'ioredis';
const redis = new Redis({
host: 'localhost', // 根据实际情况配置
port: 6379,
// password: 'yourpassword', // 如有需要
});
export default redis;

15
apps/backend/src/trpc.ts Normal file
View File

@ -0,0 +1,15 @@
import { z } from 'zod'
import { initTRPC } from '@trpc/server'
const t = initTRPC.create()
export const publicProcedure = t.procedure
export const router = t.router
export const appRouter = router({
hello: publicProcedure.input(z.string().nullish()).query(({ input }) => {
return `Hello ${input ?? 'World'}!`
}),
})
export type AppRouter = typeof appRouter

View File

@ -0,0 +1,17 @@
import { Hono } from "hono";
import { createUser, searchUser } from "./userindex";
const userRoute = new Hono();
userRoute.post('/', async (c) => {
const user = await c.req.json();
const result = await createUser(user);
return c.json(result);
});
userRoute.get('/search', async (c) => {
const q = c.req.query('q') || '';
const result = await searchUser(q);
return c.json(result.hits.hits);
});
export default userRoute;

View File

@ -0,0 +1,7 @@
import { publicProcedure, router } from "../trpc";
import { prisma } from "@repo/db";
export const userRouter = router({
getUser: publicProcedure.query(async ({ ctx }) => {
return prisma.user.findMany()
})
})

View File

@ -0,0 +1,21 @@
import { esClient } from "../elasticsearch";
const USER_INDEX = 'users';
export async function createUser(user: any): Promise<ReturnType<typeof esClient.index>> {
return esClient.index({
index: USER_INDEX,
document: user,
});
}
export async function searchUser(query: string): Promise<ReturnType<typeof esClient.search>> {
return esClient.search({
index: USER_INDEX,
query: {
multi_match: {
query,
fields: ['name', 'email'],
},
},
});
}

View File

@ -0,0 +1,11 @@
{
"extends": "@repo/typescript-config/hono.json",
"compilerOptions": {
"moduleResolution": "bundler",
"paths": {
"@/*": ["./*"],
"@repo/db/*": ["../../packages/db/src/*"],
},
}
}

View File

@ -1,6 +1,6 @@
import { Geist, Geist_Mono } from 'next/font/google';
import '@workspace/ui/globals.css';
import '@repo/ui/globals.css';
import { Providers } from '@/components/providers';
import type { Metadata } from 'next';

View File

@ -1,115 +1,3 @@
import { ModeToggle } from '@/components/mode-toggle';
import { Button, buttonVariants } from '@workspace/ui/components/button';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '@workspace/ui/components/dropdown-menu';
import { cn } from '@workspace/ui/lib/utils';
import { ChevronDownIcon } from 'lucide-react';
import Image from 'next/image';
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image className="dark:invert" src="/next.svg" alt="Next.js logo" width={180} height={38} priority />
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{' '}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">app/page.tsx</code>.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<p>
All the buttons are from the <kbd>ui</kbd> package. The auto complete works as well.
</p>
<pre className="border rounded-sm p-1.5 bg-foreground/10">
<code>{`import { Button, buttonVariants } from '@workspace/ui/components/button';
import { cn } from '@workspace/ui/lib/utils';`}</code>
</pre>
<ModeToggle />
<Button size={'sm'}>Click me</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size={'sm'}>
Dropdown <ChevronDownIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>Item 1</DropdownMenuItem>
<DropdownMenuItem>Item 2</DropdownMenuItem>
<DropdownMenuCheckboxItem checked>Item 3</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Item 3</DropdownMenuCheckboxItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Item 3</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem>Item 3.1</DropdownMenuItem>
<DropdownMenuItem>Item 3.2</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className={cn(buttonVariants({ size: 'lg' }), 'rounded-full')}
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image className="dark:invert" src="/vercel.svg" alt="Vercel logomark" width={20} height={20} />
Deploy now
</a>
<a
className={cn(buttonVariants({ size: 'lg', variant: 'outline' }), 'rounded-full')}
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image aria-hidden src="/file.svg" alt="File icon" width={16} height={16} />
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image aria-hidden src="/window.svg" alt="Window icon" width={16} height={16} />
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image aria-hidden src="/globe.svg" alt="Globe icon" width={16} height={16} />
Go to nextjs.org
</a>
</footer>
</div>
);
export default async function Home() {
return <div></div>;
}

View File

@ -14,7 +14,7 @@
"components": "@/components",
"hooks": "@/hooks",
"lib": "@/lib",
"utils": "@workspace/ui/lib/utils",
"ui": "@workspace/ui/components"
"utils": "@repo/ui/lib/utils",
"ui": "@repo/ui/components"
}
}

View File

@ -4,13 +4,13 @@ import * as React from 'react';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@workspace/ui/components/button';
import { Button } from '@repo/ui/components/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@workspace/ui/components/dropdown-menu';
} from '@repo/ui/components/dropdown-menu';
export function ModeToggle() {
const { setTheme } = useTheme();

View File

@ -1,4 +1,4 @@
import { nextJsConfig } from "@workspace/eslint-config/next-js"
import { nextJsConfig } from "@repo/eslint-config/next-js"
/** @type {import("eslint").Linter.Config} */
export default nextJsConfig

View File

@ -1,6 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ["@workspace/ui"],
transpilePackages: ["@repo/ui"],
}
export default nextConfig

View File

@ -10,20 +10,30 @@
"lint": "next lint"
},
"dependencies": {
"@workspace/ui": "workspace:*",
"@repo/client": "workspace:*",
"@repo/db": "workspace:*",
"@repo/ui": "workspace:*",
"@tanstack/react-query": "^5.51.21",
"@trpc/client": "11.1.2",
"@trpc/react-query": "11.1.2",
"@trpc/server": "11.1.2",
"@trpc/tanstack-react-query": "11.1.2",
"axios": "^1.7.2",
"dayjs": "^1.11.12",
"lucide-react": "0.511.0",
"next-themes": "^0.4.6",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0"
"next-themes": "^0.4.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"superjson": "^2.2.2"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5",
"@workspace/eslint-config": "workspace:*",
"@workspace/typescript-config": "workspace:*",
"tailwindcss": "^4",
"typescript": "^5"
}

View File

@ -1 +1 @@
export { default } from '@workspace/ui/postcss.config';
export { default } from '@repo/ui/postcss.config';

View File

@ -0,0 +1,57 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { loggerLink, httpBatchLink, createTRPCClient } from '@trpc/client';
import { TRPCProvider } from '@repo/client';
import { useMemo, useState } from 'react';
import superjson from 'superjson';
import { AppRouter } from '@repo/backend/trpc';
export default function QueryProvider({ children }) {
// 将accessToken设置为空字符串
const accessToken = '';
// 使用Next.js环境变量
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
// Set the default query options including staleTime.
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
},
},
}),
);
const trpcClient = useMemo(() => {
const headers = async () => ({
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
});
const links = [
httpBatchLink({
url: `${apiUrl}/trpc`,
headers,
transformer: superjson,
}),
loggerLink({
enabled: (opts) =>
(process.env.NODE_ENV === 'development' && typeof window !== 'undefined') ||
(opts.direction === 'down' && opts.result instanceof Error),
}),
];
return createTRPCClient<AppRouter>({
links,
});
}, [accessToken, apiUrl]);
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{children}
</TRPCProvider>
</QueryClientProvider>
);
}

View File

@ -1,10 +1,10 @@
{
"extends": "@workspace/typescript-config/nextjs.json",
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@workspace/ui/*": ["../../packages/ui/src/*"]
"@repo/backend/*": ["../backend/src/*"],
},
"plugins": [
{

View File

@ -9,11 +9,12 @@
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
"@workspace/eslint-config": "workspace:*",
"@workspace/typescript-config": "workspace:*",
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"prettier": "^3.5.3",
"turbo": "^2.5.3",
"typescript": "5.8.3"
"typescript": "5.8.3",
"@types/node": "^20"
},
"packageManager": "pnpm@9.12.3",
"engines": {

41
packages/client/package.json Executable file
View File

@ -0,0 +1,41 @@
{
"name": "@repo/client",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"sideEffects": false,
"files": [
"dist",
"src"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"dev-static": "tsup --no-watch",
"clean": "rimraf dist",
"typecheck": "tsc --noEmit"
},
"dependencies": {
},
"peerDependencies": {
"@tanstack/query-async-storage-persister": "^5.51.9",
"@tanstack/react-query": "^5.51.21",
"@tanstack/react-query-persist-client": "^5.51.9",
"@trpc/client": "11.1.2",
"@trpc/react-query": "11.1.2",
"@trpc/server": "11.1.2",
"@trpc/tanstack-react-query": "11.1.2",
"axios": "^1.7.2",
"dayjs": "^1.11.12",
"react": "^19.1.0"
},
"devDependencies": {
"rimraf": "^6.0.1",
"tsup": "^8.3.5",
"@types/react": "^19.1.4",
"@types/react-dom": "^19.1.5"
}
}

View File

@ -0,0 +1,2 @@
export * from "./useEntity"

View File

@ -0,0 +1,115 @@
import { useTRPC, type RouterInputs, type RouterOutputs } from "../trpc";
import { useQueryClient } from "@tanstack/react-query";
import { useMutation, type UseMutationResult } from "@tanstack/react-query";
/**
* MutationType mutation
* @template T - 'post', 'user'
* @description RouterInputs[T] mutation 'create', 'update'
*/
type MutationType<T extends keyof RouterInputs> = keyof {
[K in keyof RouterInputs[T]]: K extends
| "create"
| "update"
| "deleteMany"
| "softDeleteByIds"
| "restoreByIds"
| "updateOrder"
? RouterInputs[T][K]
: never;
};
/**
* MutationOptions mutation
* @template T -
* @template K - mutation
* @description onSuccess mutation
*/
type MutationOptions<
T extends keyof RouterInputs,
K extends MutationType<T>,
> = {
onSuccess?: (
data: RouterOutputs[T][K], // mutation 成功后的返回数据
variables: RouterInputs[T][K], // mutation 的输入参数
context?: unknown // 可选的上下文信息
) => void;
};
/**
* EntityOptions mutation
* @template T -
* @description mutation MutationOptions
*/
type EntityOptions<T extends keyof RouterInputs> = {
[K in MutationType<T>]?: MutationOptions<T, K>;
};
/**
* TanStack Query UseMutationResult
* @template T -
* @template K - mutation
* @description mutation tRPC
*/
export type MutationResult<
T extends keyof RouterInputs,
K extends MutationType<T>,
> = UseMutationResult<
RouterOutputs[T][K], // mutation 成功后的返回数据
Error, // mutation 的错误类型
RouterInputs[T][K], // mutation 的输入参数
unknown // mutation 的上下文类型
>;
/**
* Hook mutation
* @template T - 'post', 'user'
* @param {T} key -
* @param {EntityOptions<T>} [options] - mutation
* @returns mutation
* @description Hook mutation create, update, deleteMany mutation
*/
export function useEntity<T extends keyof RouterInputs>(
key: T,
options?: EntityOptions<T>
) {
const trpc = useTRPC();
const queryClient = useQueryClient();
/**
* mutation
* @template K - mutation
* @param {K} mutation - mutation
* @returns mutation
* @description mutation mutation onSuccess
*/
const createMutationHandler = <K extends MutationType<T>>(mutation: K) => {
// 获取对应的 tRPC mutation 配置
const mutationOptions = trpc[key as keyof typeof trpc][mutation as any].mutationOptions();
// 使用 TanStack Query 的 useMutation 创建 mutation
return useMutation({
...mutationOptions,
onSuccess: (data, variables, context) => {
// 调用原始配置的 onSuccess 回调(如果有)
mutationOptions.onSuccess?.(data as any, variables as any, context);
// 失效指定实体的缓存
queryClient.invalidateQueries({ queryKey: [key] });
// 调用用户自定义的 onSuccess 回调
options?.[mutation]?.onSuccess?.(data as any, variables as any, context);
},
}) as MutationResult<T, K>;
};
// 返回包含多个 mutation 函数的对象
return {
create: createMutationHandler("create"),
createCourse: createMutationHandler("createCourse"),
update: createMutationHandler("update"),
deleteMany: createMutationHandler("deleteMany"),
softDeleteByIds: createMutationHandler("softDeleteByIds"),
restoreByIds: createMutationHandler("restoreByIds"),
updateOrder: createMutationHandler("updateOrder"),
updateOrderByIds: createMutationHandler("updateOrderByIds"),
};
}

View File

@ -0,0 +1,3 @@
export * from "./utils"
export * from "./hooks"
export * from "./trpc"

View File

@ -0,0 +1,8 @@
import { AppRouter } from "@repo/backend/trpc"
import { inferReactQueryProcedureOptions } from "@trpc/react-query";
import { inferRouterInputs, inferRouterOutputs } from "@trpc/server";
import { createTRPCContext } from '@trpc/tanstack-react-query';
export type ReactQueryOptions = inferReactQueryProcedureOptions<AppRouter>;
export type RouterInputs = inferRouterInputs<AppRouter>;
export type RouterOutputs = inferRouterOutputs<AppRouter>;
export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext<AppRouter>();

View File

@ -0,0 +1,62 @@
import { QueryClient } from "@tanstack/react-query";
import { getQueryKey } from "@trpc/react-query";
/**
*
*
* @template T -
* @param {QueryClient} client -
* @param {any} trpcQueryKey -
* @param {string} uniqueField - 'id'
* @returns {T[]} -
*/
export function getCacheDataFromQuery<T extends Record<string, any>>(client: QueryClient, trpcQueryKey: any, uniqueField: string = 'id'): T[] {
// 获取查询缓存数据
const cacheData = client.getQueriesData({ queryKey: getQueryKey(trpcQueryKey) });
// 提取并整理缓存数据
const data = cacheData
.flatMap(cache => cache.slice(1))
.flat()
.filter(item => item !== undefined) as T[];
// console.log('cacheData', cacheData)
// console.log('data', data)
// 使用 Map 进行去重
const uniqueDataMap = new Map<string | number, T>();
data.forEach((item: T) => {
if (item && item[uniqueField] !== undefined) {
uniqueDataMap.set(item[uniqueField], item);
}
});
// 转换为数组返回唯一的数据列表
return Array.from(uniqueDataMap.values());
}
/**
*
*
* @template T -
* @param {T[]} uniqueData -
* @param {string} key -
* @param {string} uniqueField - 'id'
* @returns {T | undefined} - undefined
*/
export function findDataByKey<T extends Record<string, any>>(uniqueData: T[], key: string | number, uniqueField: string = 'id'): T | undefined {
return uniqueData.find(item => item[uniqueField] === key);
}
/**
* 使
*
* @template T -
* @param {QueryClient} client -
* @param {any} trpcQueryKey -
* @param {string} key -
* @param {string} uniqueField - 'id'
* @returns {T | undefined} - undefined
*/
export function findQueryData<T extends Record<string, any>>(client: QueryClient, trpcQueryKey: any, key: string | number, uniqueField: string = 'id'): T | undefined {
const uniqueData = getCacheDataFromQuery<T>(client, trpcQueryKey, uniqueField);
return findDataByKey<T>(uniqueData, key, uniqueField);
}

1
packages/client/src/index.ts Executable file
View File

@ -0,0 +1 @@
export * from "./api"

View File

@ -0,0 +1,25 @@
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"target": "es2022",
"module": "es2022",
"allowJs": true,
"esModuleInterop": true,
"lib": ["dom", "es2022"],
"jsx": "react-jsx",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"moduleResolution": "node",
"incremental": true,
"strict": true,
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
"useDefineForClassFields": false,
"paths": {
"@repo/backend/*": ["../../apps/backend/src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

1
packages/db/.env.example Executable file
View File

@ -0,0 +1 @@
DATABASE_URL="postgresql://root:Letusdoit000@localhost:5432/app?schema=public"

33
packages/db/package.json Executable file
View File

@ -0,0 +1,33 @@
{
"name": "@repo/db",
"version": "1.0.0",
"exports": {
".": "./src/index.ts"
},
"private": true,
"scripts": {
"db:migrate": "prisma migrate dev --skip-generate",
"db:deploy": "prisma migrate deploy",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:seed": "tsx src/seed.ts",
"format": "prisma format",
"studio": "prisma studio"
},
"dependencies": {
"@prisma/client": "^6.6.0"
},
"peerDependencies": {
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.3.1",
"concurrently": "^8.0.0",
"prisma": "^6.6.0",
"rimraf": "^6.0.1",
"ts-node": "^10.9.1",
"tsup": "^8.3.5",
"tsx": "^4.19.4",
"typescript": "^5.5.4"
}
}

View File

@ -0,0 +1,244 @@
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"password" TEXT,
"salt" TEXT,
"phone" TEXT,
"email" TEXT NOT NULL,
"avatar" TEXT,
"is_system" BOOLEAN,
"is_admin" BOOLEAN,
"last_sign_time" TIMESTAMP(3),
"deactivated_time" TIMESTAMP(3),
"created_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_time" TIMESTAMP(3),
"last_modified_time" TIMESTAMP(3),
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "attachments" (
"id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"hash" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"mimetype" TEXT NOT NULL,
"path" TEXT NOT NULL,
"width" INTEGER,
"height" INTEGER,
"deleted_time" TIMESTAMP(3),
"created_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"created_by" TEXT NOT NULL,
"last_modified_by" TEXT,
"thumbnail_path" TEXT,
CONSTRAINT "attachments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notification" (
"id" TEXT NOT NULL,
"from_user_id" TEXT NOT NULL,
"to_user_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"message" TEXT NOT NULL,
"url_path" TEXT,
"is_read" BOOLEAN NOT NULL DEFAULT false,
"created_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"created_by" TEXT NOT NULL,
CONSTRAINT "notification_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "setting" (
"instance_id" TEXT NOT NULL,
"disallow_sign_up" BOOLEAN,
"disallow_space_creation" BOOLEAN,
"disallow_space_invitation" BOOLEAN,
"enable_email_verification" BOOLEAN,
"ai_config" TEXT,
"brand_name" TEXT,
"brand_logo" TEXT,
CONSTRAINT "setting_pkey" PRIMARY KEY ("instance_id")
);
-- CreateTable
CREATE TABLE "trash" (
"id" TEXT NOT NULL,
"resource_type" TEXT NOT NULL,
"resource_id" TEXT NOT NULL,
"parent_id" TEXT,
"deleted_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_by" TEXT NOT NULL,
CONSTRAINT "trash_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_last_visit" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"resource_type" TEXT NOT NULL,
"resource_id" TEXT NOT NULL,
"parent_resource_id" TEXT NOT NULL,
"last_visit_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "user_last_visit_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "oidc_clients" (
"id" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"client_secret" TEXT,
"client_name" TEXT NOT NULL,
"client_uri" TEXT,
"logo_uri" TEXT,
"contacts" TEXT[],
"redirect_uris" TEXT[],
"post_logout_redirect_uris" TEXT[],
"token_endpoint_auth_method" TEXT NOT NULL,
"grant_types" TEXT[],
"response_types" TEXT[],
"scope" TEXT NOT NULL,
"jwks_uri" TEXT,
"jwks" TEXT,
"policy_uri" TEXT,
"tos_uri" TEXT,
"require_pkce" BOOLEAN NOT NULL DEFAULT false,
"active" BOOLEAN NOT NULL DEFAULT true,
"created_by" TEXT,
"created_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"last_modified_time" TIMESTAMP(3),
CONSTRAINT "oidc_clients_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "oidc_consents" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"created_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3),
CONSTRAINT "oidc_consents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "oidc_authorization_codes" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"redirect_uri" TEXT NOT NULL,
"code_challenge" TEXT,
"code_challenge_method" TEXT,
"nonce" TEXT,
"auth_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3) NOT NULL,
"used" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "oidc_authorization_codes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "oidc_tokens" (
"id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"client_id" TEXT NOT NULL,
"token_type" TEXT NOT NULL,
"scope" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"created_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"is_revoked" BOOLEAN NOT NULL DEFAULT false,
"parent_id" TEXT,
CONSTRAINT "oidc_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "oidc_sessions" (
"id" TEXT NOT NULL,
"session_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"last_active" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"device_info" TEXT,
"created_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"last_modified_time" TIMESTAMP(3),
CONSTRAINT "oidc_sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "oidc_key_pairs" (
"id" TEXT NOT NULL,
"kid" TEXT NOT NULL,
"private_key" TEXT NOT NULL,
"public_key" TEXT NOT NULL,
"algorithm" TEXT NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"created_time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expires_at" TIMESTAMP(3),
CONSTRAINT "oidc_key_pairs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_phone_key" ON "users"("phone");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "attachments_token_key" ON "attachments"("token");
-- CreateIndex
CREATE INDEX "notification_to_user_id_is_read_created_time_idx" ON "notification"("to_user_id", "is_read", "created_time");
-- CreateIndex
CREATE UNIQUE INDEX "trash_resource_type_resource_id_key" ON "trash"("resource_type", "resource_id");
-- CreateIndex
CREATE INDEX "user_last_visit_user_id_resource_type_idx" ON "user_last_visit"("user_id", "resource_type");
-- CreateIndex
CREATE UNIQUE INDEX "user_last_visit_user_id_resource_type_parent_resource_id_key" ON "user_last_visit"("user_id", "resource_type", "parent_resource_id");
-- CreateIndex
CREATE UNIQUE INDEX "oidc_clients_client_id_key" ON "oidc_clients"("client_id");
-- CreateIndex
CREATE UNIQUE INDEX "oidc_consents_user_id_client_id_key" ON "oidc_consents"("user_id", "client_id");
-- CreateIndex
CREATE UNIQUE INDEX "oidc_authorization_codes_code_key" ON "oidc_authorization_codes"("code");
-- CreateIndex
CREATE UNIQUE INDEX "oidc_tokens_token_key" ON "oidc_tokens"("token");
-- CreateIndex
CREATE INDEX "oidc_tokens_user_id_token_type_is_revoked_idx" ON "oidc_tokens"("user_id", "token_type", "is_revoked");
-- CreateIndex
CREATE UNIQUE INDEX "oidc_sessions_session_id_key" ON "oidc_sessions"("session_id");
-- CreateIndex
CREATE UNIQUE INDEX "oidc_key_pairs_kid_key" ON "oidc_key_pairs"("kid");
-- AddForeignKey
ALTER TABLE "oidc_consents" ADD CONSTRAINT "oidc_consents_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "oidc_clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "oidc_authorization_codes" ADD CONSTRAINT "oidc_authorization_codes_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "oidc_clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "oidc_tokens" ADD CONSTRAINT "oidc_tokens_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "oidc_clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

218
packages/db/prisma/schema.prisma Executable file
View File

@ -0,0 +1,218 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-1.1.x"]
output = "../generated/prisma"
}
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
name String
password String?
salt String?
phone String? @unique
email String @unique
avatar String?
isSystem Boolean? @map("is_system")
isAdmin Boolean? @map("is_admin")
lastSignTime DateTime? @map("last_sign_time")
deactivatedTime DateTime? @map("deactivated_time")
createdTime DateTime @default(now()) @map("created_time")
deletedTime DateTime? @map("deleted_time")
lastModifiedTime DateTime? @updatedAt @map("last_modified_time")
@@map("users")
}
model Attachments {
id String @id @default(cuid())
token String @unique
hash String
size Int
mimetype String
path String
width Int?
height Int?
deletedTime DateTime? @map("deleted_time")
createdTime DateTime @default(now()) @map("created_time")
createdBy String @map("created_by")
lastModifiedBy String? @map("last_modified_by")
thumbnailPath String? @map("thumbnail_path")
@@map("attachments")
}
model Notification {
id String @id @default(cuid())
fromUserId String @map("from_user_id")
toUserId String @map("to_user_id")
type String @map("type")
message String @map("message")
urlPath String? @map("url_path")
isRead Boolean @default(false) @map("is_read")
createdTime DateTime @default(now()) @map("created_time")
createdBy String @map("created_by")
@@index([toUserId, isRead, createdTime])
@@map("notification")
}
model Setting {
instanceId String @id @default(cuid()) @map("instance_id")
disallowSignUp Boolean? @map("disallow_sign_up")
disallowSpaceCreation Boolean? @map("disallow_space_creation")
disallowSpaceInvitation Boolean? @map("disallow_space_invitation")
enableEmailVerification Boolean? @map("enable_email_verification")
aiConfig String? @map("ai_config")
brandName String? @map("brand_name")
brandLogo String? @map("brand_logo")
@@map("setting")
}
model Trash {
id String @id @default(cuid())
resourceType String @map("resource_type")
resourceId String @map("resource_id")
parentId String? @map("parent_id")
deletedTime DateTime @default(now()) @map("deleted_time")
deletedBy String @map("deleted_by")
@@unique([resourceType, resourceId])
@@map("trash")
}
model UserLastVisit {
id String @id @default(cuid())
userId String @map("user_id")
resourceType String @map("resource_type")
resourceId String @map("resource_id")
parentResourceId String @map("parent_resource_id")
lastVisitTime DateTime @default(now()) @map("last_visit_time")
@@unique([userId, resourceType, parentResourceId])
@@index([userId, resourceType])
@@map("user_last_visit")
}
// OIDC 客户端相关模型
model OidcClient {
id String @id @default(cuid())
clientId String @unique @map("client_id")
clientSecret String? @map("client_secret")
clientName String @map("client_name")
clientUri String? @map("client_uri")
logoUri String? @map("logo_uri")
contacts String[]
redirectUris String[] @map("redirect_uris")
postLogoutRedirectUris String[] @map("post_logout_redirect_uris")
tokenEndpointAuthMethod String @map("token_endpoint_auth_method")
grantTypes String[] @map("grant_types")
responseTypes String[] @map("response_types")
scope String
jwksUri String? @map("jwks_uri")
jwks String?
policyUri String? @map("policy_uri")
tosUri String? @map("tos_uri")
requirePkce Boolean @default(false) @map("require_pkce")
active Boolean @default(true)
createdBy String? @map("created_by")
createdTime DateTime @default(now()) @map("created_time")
lastModifiedTime DateTime? @updatedAt @map("last_modified_time")
// 关联模型
consents OidcConsent[]
authorizationCodes OidcCode[]
tokens OidcToken[]
@@map("oidc_clients")
}
// 用户同意记录
model OidcConsent {
id String @id @default(cuid())
userId String @map("user_id")
clientId String @map("client_id")
scope String
createdTime DateTime @default(now()) @map("created_time")
expiresAt DateTime? @map("expires_at")
// 关联
client OidcClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@unique([userId, clientId])
@@map("oidc_consents")
}
// 授权码
model OidcCode {
id String @id @default(cuid())
code String @unique
userId String @map("user_id")
clientId String @map("client_id")
scope String
redirectUri String @map("redirect_uri")
codeChallenge String? @map("code_challenge")
codeChallengeMethod String? @map("code_challenge_method")
nonce String?
authTime DateTime @default(now()) @map("auth_time")
expiresAt DateTime @map("expires_at")
used Boolean @default(false)
// 关联
client OidcClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@map("oidc_authorization_codes")
}
// 统一令牌表合并access和refresh token
model OidcToken {
id String @id @default(cuid())
token String @unique
userId String @map("user_id")
clientId String @map("client_id")
tokenType String @map("token_type") // "access" 或 "refresh"
scope String
expiresAt DateTime @map("expires_at")
createdTime DateTime @default(now()) @map("created_time")
isRevoked Boolean @default(false) @map("is_revoked")
parentId String? @map("parent_id") // 用于关联refresh token和对应的access token
// 关联
client OidcClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
@@index([userId, tokenType, isRevoked])
@@map("oidc_tokens")
}
// Session管理
model OidcSession {
id String @id @default(cuid())
sessionId String @unique @map("session_id")
userId String @map("user_id")
expiresAt DateTime @map("expires_at")
lastActive DateTime @default(now()) @map("last_active")
deviceInfo String? @map("device_info")
createdTime DateTime @default(now()) @map("created_time")
lastModifiedTime DateTime? @updatedAt @map("last_modified_time")
@@map("oidc_sessions")
}
// 供应商的密钥对
model OidcKeyPair {
id String @id @default(cuid())
kid String @unique
privateKey String @map("private_key")
publicKey String @map("public_key")
algorithm String
active Boolean @default(true)
createdTime DateTime @default(now()) @map("created_time")
expiresAt DateTime? @map("expires_at")
@@map("oidc_key_pairs")
}

8
packages/db/src/client.ts Executable file
View File

@ -0,0 +1,8 @@
import { PrismaClient } from "../generated/prisma";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

2
packages/db/src/index.ts Executable file
View File

@ -0,0 +1,2 @@
export * from "./client"
export * from "../generated/prisma" // exports generated types from prisma

40
packages/db/tsconfig.json Executable file
View File

@ -0,0 +1,40 @@
{
"compilerOptions": {
"target": "es2022",
"module": "esnext",
"lib": [
"DOM",
"es2022"
],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"moduleResolution": "node",
"removeComments": true,
"skipLibCheck": true,
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitReturns": false,
"noFallthroughCasesInSwitch": false,
"noUncheckedIndexedAccess": false,
"noImplicitOverride": false,
"noPropertyAccessFromIndexSignature": false,
"emitDeclarationOnly": true,
"outDir": "dist",
"incremental": true,
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
},
"include": [
"src"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/__tests__"
]
}

View File

@ -1,3 +1,3 @@
# `@workspace/eslint-config`
# `@repo/eslint-config`
Shared eslint configuration for the workspace.

View File

@ -1,5 +1,5 @@
{
"name": "@workspace/eslint-config",
"name": "@repo/eslint-config",
"version": "0.0.0",
"type": "module",
"private": true,

18
packages/icons/.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# build
/dist
/build
/storybook-static
# dependencies
node_modules
# testing
/coverage
# misc
.DS_Store
*.pem
.env

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/icons.iml" filepath="$PROJECT_DIR$/.idea/icons.iml" />
</modules>
</component>
</project>

21
packages/icons/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023-2025 Teable, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,58 @@
{
"name": "@repo/icons",
"version": "1.7.0",
"license": "MIT",
"homepage": "https://github.com/teableio/teable",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/teableio/teable",
"directory": "packages/icons"
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"clean": "rimraf ./dist ./build ./tsconfig.tsbuildinfo ./node_modules/.cache",
"dev": "rm -rf dist && tsc --watch",
"test": "echo \"Error: no test specified\"",
"lint": "eslint . --ext .ts,.tsx,.js,.jsx,.cjs,.mjs,.mdx --cache --cache-location ../../.cache/eslint/icons.eslintcache",
"typecheck": "tsc --project ./tsconfig.json --noEmit",
"generate": "rm -rf src/components && node ./scripts/generate.mjs"
},
"peerDependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@svgr/core": "8.1.0",
"@svgr/plugin-jsx": "8.1.0",
"@svgr/plugin-prettier": "8.1.0",
"@svgr/plugin-svgo": "8.1.0",
"@types/fs-extra": "11.0.4",
"@types/node": "20.9.0",
"@types/react": "18.2.45",
"axios": "1.7.7",
"chalk": "5.3.0",
"dotenv": "16.4.5",
"eslint": "8.57.0",
"figma-js": "1.16.0",
"fs-extra": "11.2.0",
"lodash": "4.17.21",
"rimraf": "5.0.5",
"typescript": "5.4.3"
}
}

View File

@ -0,0 +1,155 @@
import path from 'path';
import { transform } from '@svgr/core';
import axios from 'axios';
import chalk from 'chalk';
import dotenv from 'dotenv';
import fs from 'fs-extra';
import _ from 'lodash';
import * as Figma from 'figma-js';
dotenv.config();
const componentsDir = 'src/components';
// Add .env file
const FIGMA_API_TOKEN = process.env.FIGMA_API_TOKEN;
const FIGMA_FILE_ID = process.env.FIGMA_FILE_ID;
const FIGMA_CANVAS = process.env.FIGMA_CANVAS;
if (!FIGMA_API_TOKEN) {
throw new Error('Missing environment variable FIGMA_API_TOKEN');
}
if (!FIGMA_FILE_ID) {
throw new Error('Missing environment variable FIGMA_FILE_ID');
}
if (!FIGMA_CANVAS) {
throw new Error('Missing environment variable FIGMA_CANVAS');
}
const figmaApi = Figma.Client({ personalAccessToken: FIGMA_API_TOKEN });
const getSvgs = async ({ fileId, canvas, group }) => {
const file = await figmaApi.file(fileId);
const { document } = file.data;
const iconsNode = document.children.find(({ name }) => name === canvas);
if (!iconsNode) {
throw new Error(`Couldn't find page with name ${canvas}`);
}
const usingIconNodes = iconsNode.children.find(({ name }) => name === group)?.children || [];
const usingNodeId = usingIconNodes.map(({ id }) => id);
const svgs = await figmaApi.fileImages(fileId, {
format: 'svg',
ids: usingNodeId,
});
return usingIconNodes.map(({ id, name }) => ({ id, name, url: svgs.data.images[id] }));
};
const downloadSVGsData = async (data, batchSize = 20, delayBetweenBatches = 500) => {
const results = [];
const batchCount = Math.ceil(data.length / batchSize);
for (let i = 0; i < batchCount; i++) {
const batchData = data.slice(i * batchSize, (i + 1) * batchSize);
console.log(`Processing batch ${i + 1}/${batchCount}, containing ${batchData.length} requests`);
const batchResults = await Promise.all(
batchData.map(async (dataItem) => {
try {
const downloadedSvg = await axios.get(dataItem.url);
return {
...dataItem,
data: downloadedSvg.data,
success: true,
};
} catch (error) {
console.error(`Failed to download ${dataItem.url}:`, error.message);
return {
...dataItem,
success: false,
};
}
})
);
results.push(...batchResults);
if (i < batchCount - 1) {
await new Promise((resolve) => setTimeout(resolve, delayBetweenBatches));
}
}
return results;
};
const transformReactComponent = (svgList) => {
if (!fs.existsSync(componentsDir)) {
fs.mkdirSync(componentsDir);
}
svgList.forEach((svg) => {
if (!svg.success) return;
const svgCode = svg.data;
const svgName = svg.name.split('/').pop();
const camelCaseInput = _.camelCase(svgName);
const componentName = camelCaseInput.charAt(0).toUpperCase() + camelCaseInput.slice(1);
const componentFileName = `${componentName}.tsx`;
// Converts SVG code into React code using SVGR library
const componentCode = transform.sync(
svgCode,
{
typescript: true,
icon: true,
replaceAttrValues: {
'#000': 'currentColor',
},
plugins: [
// Clean SVG files using SVGO
'@svgr/plugin-svgo',
// Generate JSX
'@svgr/plugin-jsx',
// Format the result using Prettier
'@svgr/plugin-prettier',
],
},
{ componentName }
);
// 6. Write generated component to file system
fs.outputFileSync(path.resolve(componentsDir, componentFileName), componentCode);
// fs.outputFileSync(path.resolve('src/icons', `${svgName}.svg`), svg.data);
});
};
const genIndexContent = () => {
let indexContent = '';
const indexPath = path.resolve('src/index.ts');
fs.readdirSync(componentsDir).forEach((componentFileName) => {
// Convert name to pascal case
const componentName = componentFileName.split('.')[0];
// Export statement
const componentExport = `export { default as ${componentName} } from './components/${componentName}';\n`;
indexContent += componentExport;
});
// Write the content to file system
fs.writeFileSync(indexPath, indexContent);
};
const generate = async () => {
console.log(chalk.magentaBright('-> Fetching icons metadata'));
const svgs = await getSvgs({ fileId: FIGMA_FILE_ID, canvas: FIGMA_CANVAS, group: 'using' });
console.log(chalk.blueBright('-> Downloading SVG code'));
const svgsData = await downloadSVGsData(svgs);
console.log(chalk.cyanBright('-> Converting to React components'));
transformReactComponent(svgsData);
console.log(chalk.yellowBright('-> Writing exports components'));
genIndexContent();
console.log(chalk.greenBright('-> All done! ✅'));
};
generate();

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const A = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21 12 3 3 21m3-7h12"
/>
</svg>
);
export default A;

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Admin = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M22.429 18.357c-.077.41-.342.676-.673.676H21.7a1.18 1.18 0 0 0-1.176 1.176c.009.154.044.306.103.448a.85.85 0 0 1-.29 1.026l-.032.022-1.364.751a.9.9 0 0 1-.374.086.91.91 0 0 1-.67-.287c-.176-.19-.642-.584-.97-.584-.32 0-.793.394-.959.574a.92.92 0 0 1-1.012.21l-1.35-.75c-.355-.247-.479-.69-.317-1.05.028-.066.103-.277.103-.444a1.18 1.18 0 0 0-1.178-1.176h-.046c-.34 0-.603-.267-.682-.678a7 7 0 0 1-.116-1.09c0-.46.104-1.031.116-1.092.08-.41.343-.678.674-.678h.055c.649 0 1.176-.525 1.177-1.173a1.3 1.3 0 0 0-.104-.45.85.85 0 0 1 .29-1.027l.033-.02 1.4-.769.026-.01a.93.93 0 0 1 1.003.21c.17.178.629.55.946.55.312 0 .768-.364.938-.542a.93.93 0 0 1 1.006-.2l1.374.76c.357.246.482.69.32 1.05a1.4 1.4 0 0 0-.102.446 1.177 1.177 0 0 0 1.176 1.174h.047c.338 0 .603.266.681.678.012.062.116.632.116 1.092 0 .483-.116 1.086-.114 1.09m-1.024-1.812a2.244 2.244 0 0 1-1.953-2.224q.007-.349.12-.68l-.972-.54q-.19.175-.401.325-.638.449-1.212.45-.582 0-1.224-.458a3.5 3.5 0 0 1-.402-.334l-1.017.56c.057.168.12.417.12.678a2.25 2.25 0 0 1-1.953 2.225 5 5 0 0 0-.067.719q.009.36.067.717a2.24 2.24 0 0 1 1.954 2.225c0 .26-.067.51-.122.68l.94.525q.192-.182.403-.34c.431-.314.85-.476 1.242-.476.396 0 .819.165 1.254.487q.214.16.405.346l.985-.542a2.2 2.2 0 0 1-.12-.679 2.24 2.24 0 0 1 1.953-2.225 5.5 5.5 0 0 0 .067-.72c0-.234-.04-.532-.067-.72m-4.47 2.788a2.08 2.08 0 0 1-2.078-2.076 2.078 2.078 0 0 1 4.154 0 2.08 2.08 0 0 1-2.077 2.076m0-3.041c-.54 0-.985.426-1.008.966a1.007 1.007 0 0 0 2.014 0 1.01 1.01 0 0 0-1.007-.966m-3.014-5.288-.339.263c-.688.612-2.002.817-2.87.884l-.112-.003q-.366.002-.731.035h-.01v.002c-3.929.37-7.014 3.663-7.014 7.662v1.176h8.61c.227.488.508.95.84 1.373H2.151a.69.69 0 0 1-.694-.686v-1.863c0-3.729 2.26-7.035 5.762-8.424l.397-.156-.337-.263a5.3 5.3 0 0 1-2.068-4.203c0-2.947 2.417-5.344 5.388-5.344s5.388 2.397 5.39 5.344a5.31 5.31 0 0 1-2.068 4.203m-3.322-8.173c-2.205 0-3.999 1.782-3.999 3.97 0 2.19 1.794 3.972 4 3.972s4-1.781 4-3.972-1.795-3.97-4-3.97"
/>
</svg>
);
export default Admin;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const AlertCircle = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10M12 8v4M12 16h.01"
/>
</svg>
);
export default AlertCircle;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const AlertTriangle = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3M12 9v4M12 17h.01"
/>
</svg>
);
export default AlertTriangle;

View File

@ -0,0 +1,26 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Anthropic = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<g clipPath="url(#prefix__a)">
<path fill="#CA9F7B" d="M6 0h12q6 0 6 6v12q0 6-6 6H6q-6 0-6-6V6q0-6 6-6" />
<path
fill="#191918"
d="M15.384 6.435H12.97l4.405 11.13h2.416zm-6.979 0L4 17.565h2.463l.901-2.337h4.609l.9 2.337h2.464l-4.405-11.13zm-.244 6.726 1.508-3.912 1.507 3.912z"
/>
</g>
<defs>
<clipPath id="prefix__a">
<path fill="#fff" d="M0 0h24v24H0z" />
</clipPath>
</defs>
</svg>
);
export default Anthropic;

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Apple = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 20.94c1.5 0 2.75 1.06 4 1.06 3 0 6-8 6-12.22A4.91 4.91 0 0 0 17 5c-2.22 0-4 1.44-5 2-1-.56-2.78-2-5-2a4.9 4.9 0 0 0-5 4.78C2 14 5 22 8 22c1.25 0 2.5-1.06 4-1.06"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M10 2c1 .5 2 2 2 5"
/>
</svg>
);
export default Apple;

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Array = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M8.949 22.348v-1.704H6.765V3.41h2.184V1.707H4.82v20.64zm10.008 0V1.707h-4.128V3.41h2.184v17.232h-2.184v1.704z"
/>
</svg>
);
export default Array;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ArrowDown = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 5v14M19 12l-7 7-7-7"
/>
</svg>
);
export default ArrowDown;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ArrowLeft = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 12H5M12 19l-7-7 7-7"
/>
</svg>
);
export default ArrowLeft;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ArrowRight = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 12h14M12 5l7 7-7 7"
/>
</svg>
);
export default ArrowRight;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ArrowUp = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 19V5M5 12l7-7 7 7"
/>
</svg>
);
export default ArrowUp;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ArrowUpDown = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m11 17-4 4-4-4M7 21V9M21 7l-4-4-4 4M17 15V3"
/>
</svg>
);
export default ArrowUpDown;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ArrowUpRight = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 17 17 7M7 7h10v10"
/>
</svg>
);
export default ArrowUpRight;

View File

@ -0,0 +1,22 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Audio = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M19.734 10.64a.19.19 0 0 0-.187-.187H18.14a.19.19 0 0 0-.188.188 5.953 5.953 0 1 1-11.906 0 .19.19 0 0 0-.188-.188H4.453a.19.19 0 0 0-.187.188 7.735 7.735 0 0 0 6.797 7.678v2.4H7.656c-.321 0-.579.335-.579.75v.843c0 .104.066.188.145.188h9.554c.08 0 .145-.084.145-.187v-.844c0-.415-.258-.75-.579-.75h-3.5V18.33a7.74 7.74 0 0 0 6.891-7.69"
/>
<path
fill="currentColor"
d="M12 14.625c2.2 0 3.984-1.762 3.984-3.937v-5.25C15.984 3.263 14.201 1.5 12 1.5S8.016 3.263 8.016 5.438v5.25c0 2.175 1.783 3.937 3.984 3.937M9.797 5.438c0-1.186.982-2.157 2.203-2.157s2.203.97 2.203 2.157v5.25c0 1.185-.982 2.156-2.203 2.156s-2.203-.97-2.203-2.156z"
/>
</svg>
);
export default Audio;

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Azure = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="#1E88E5"
d="M13.26 2.69 5.472 19.262 0 19.2 6.108 8.688l7.152-6.002m.84 1.308L24 21.31H5.69l11.16-1.988-5.844-6.952z"
/>
</svg>
);
export default Azure;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const BarChart2 = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M18 20V10M12 20V4M6 20v-6"
/>
</svg>
);
export default BarChart2;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Bell = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M18 8A6 6 0 1 0 6 8c0 7-3 9-3 9h18s-3-2-3-9M13.73 21a2 2 0 0 1-3.46 0"
/>
</svg>
);
export default Bell;

View File

@ -0,0 +1,25 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Boolean = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<g clipPath="url(#prefix__a)">
<path
fill="currentColor"
d="M16 4a8 8 0 0 1 0 16H8A8 8 0 0 1 8 4zm0 2H8a6 6 0 0 0-.225 11.996L8 18h8a6 6 0 0 0 .225-11.996zm0 1a5 5 0 1 1 0 10 5 5 0 0 1 0-10"
/>
</g>
<defs>
<clipPath id="prefix__a">
<path fill="#fff" d="M0 0h24v24H0z" />
</clipPath>
</defs>
</svg>
);
export default Boolean;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Building2 = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 22V4c0-.27 0-.55.07-.82a1.48 1.48 0 0 1 1.1-1.11C7.46 2 8.73 2 9 2h7c.27 0 .55 0 .82.07a1.48 1.48 0 0 1 1.11 1.1c.07.28.07.56.07.83v18zM2 14v6c0 1.1.9 2 2 2h2V12H4c-.27 0-.55 0-.82.07s-.52.2-.72.4c-.19.19-.32.44-.39.71A3.4 3.4 0 0 0 2 14M20.82 9.07A3.4 3.4 0 0 0 20 9h-2v13h2a2 2 0 0 0 2-2v-9c0-.28 0-.55-.07-.82s-.2-.52-.4-.72c-.19-.19-.44-.32-.71-.39M10 6h4M10 10h4M10 14h4M10 18h4"
/>
</svg>
);
export default Building2;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Calendar = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 2v4M8 2v4m-5 4h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"
/>
</svg>
);
export default Calendar;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Check = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 6 9 17l-5-5"
/>
</svg>
);
export default Check;

View File

@ -0,0 +1,29 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const CheckCircle2 = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10"
clipRule="evenodd"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m9 12 2 2 4-4"
/>
</svg>
);
export default CheckCircle2;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const CheckSquare = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m9 11 3 3L22 4m-1 8v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"
/>
</svg>
);
export default CheckSquare;

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Checked = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 16 16"
{...props}
>
<path
fill="currentColor"
d="M12.667 2H3.333C2.593 2 2 2.6 2 3.333v9.334C2 13.4 2.593 14 3.333 14h9.334c.74 0 1.333-.6 1.333-1.333V3.333C14 2.6 13.407 2 12.667 2m-6 9.333L3.333 8l.94-.94 2.394 2.387 5.06-5.06.94.946z"
/>
</svg>
);
export default Checked;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ChevronDown = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m6 9 6 6 6-6"
/>
</svg>
);
export default ChevronDown;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ChevronLeft = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m15 18-6-6 6-6"
/>
</svg>
);
export default ChevronLeft;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ChevronRight = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m9 18 6-6-6-6"
/>
</svg>
);
export default ChevronRight;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ChevronUp = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m18 15-6-6-6 6"
/>
</svg>
);
export default ChevronUp;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ChevronsLeft = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m11 17-5-5 5-5M18 17l-5-5 5-5"
/>
</svg>
);
export default ChevronsLeft;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ChevronsRight = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m13 17 5-5-5-5M6 17l5-5-5-5"
/>
</svg>
);
export default ChevronsRight;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ChevronsUpDown = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m7 15 5 5 5-5M7 9l5-5 5 5"
/>
</svg>
);
export default ChevronsUpDown;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Circle = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10"
/>
</svg>
);
export default Circle;

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const ClipboardList = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 2H9a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2M12 11h4M12 16h4M8 11h.01M8 16h.01"
/>
</svg>
);
export default ClipboardList;

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Clock4 = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6v6l4 2"
/>
</svg>
);
export default Clock4;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Code = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m16 18 6-6-6-6M8 6l-6 6 6 6"
/>
</svg>
);
export default Code;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Code2 = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="m18 16 4-4-4-4M6 8l-4 4 4 4M14.5 4l-5 16"
/>
</svg>
);
export default Code2;

View File

@ -0,0 +1,33 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Cohere = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<g clipPath="url(#prefix__a)">
<path
fill="#39594D"
d="M7.775 14.288c.645 0 1.931-.036 3.705-.767 2.07-.852 6.188-2.4 9.158-3.988 2.078-1.11 2.987-2.581 2.987-4.56A4.97 4.97 0 0 0 18.653 0H7.144A7.144 7.144 0 0 0 0 7.144c0 3.944 2.994 7.144 7.775 7.144"
/>
<path
fill="#D18EE2"
d="M9.72 19.207a4.785 4.785 0 0 1 2.95-4.42l3.626-1.504c3.666-1.52 7.702 1.173 7.702 5.143a5.566 5.566 0 0 1-5.57 5.567h-3.924a4.784 4.784 0 0 1-4.784-4.786"
/>
<path
fill="#FF7759"
d="M4.118 15.23A4.117 4.117 0 0 0 0 19.348v.533a4.118 4.118 0 0 0 8.236 0v-.533a4.12 4.12 0 0 0-4.118-4.118z"
/>
</g>
<defs>
<clipPath id="prefix__a">
<path fill="#fff" d="M0 0h24v24H0z" />
</clipPath>
</defs>
</svg>
);
export default Cohere;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Component = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.33}
d="M5.5 8.5 9 12l-3.5 3.5L2 12zM12 2l3.5 3.5L12 9 8.5 5.5zM18.5 8.5 22 12l-3.5 3.5L15 12zM12 15l3.5 3.5L12 22l-3.5-3.5z"
/>
</svg>
);
export default Component;

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Condition = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="m20.589 6.429-.001 10.285h2.555l-3.429 3.429-3.428-3.429h2.554V8.143H16.2a4.3 4.3 0 0 0 0-1.715zm-12.875.857q.001.441.086.857h-.943V8.14l-1.698.001v8.572h2.555l-3.428 3.429-3.429-3.429h2.554V6.43L7.8 6.428a4.4 4.4 0 0 0-.086.858M12 3.857a3.429 3.429 0 1 1 0 6.858 3.429 3.429 0 0 1 0-6.858m0 1.714A1.714 1.714 0 1 0 12 9a1.714 1.714 0 0 0 0-3.429"
/>
</svg>
);
export default Condition;

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Copy = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 9h-9a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
/>
</svg>
);
export default Copy;

View File

@ -0,0 +1,26 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const CreateRecord = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<g clipPath="url(#prefix__a)">
<rect width={24} height={24} fill="#C4B5FD" rx={3} />
<path
fill="#A855F7"
d="M17.01 11.01h-4.02V6.99a.99.99 0 1 0-1.98 0v4.02H6.99a.99.99 0 1 0 0 1.98h4.02v4.02a.99.99 0 1 0 1.98 0v-4.02h4.02a.99.99 0 1 0 0-1.98"
/>
</g>
<defs>
<clipPath id="prefix__a">
<rect width={24} height={24} fill="#fff" rx={3} />
</clipPath>
</defs>
</svg>
);
export default CreateRecord;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const CreditCard = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 5H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2M2 10h20"
/>
</svg>
);
export default CreditCard;

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Database = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8c4.97 0 9-1.343 9-3s-4.03-3-9-3-9 1.343-9 3 4.03 3 9 3M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"
/>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"
/>
</svg>
);
export default Database;

View File

@ -0,0 +1,22 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const DeepThinking = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M2.042 21.764c-1.35-1.349-1.527-3.655-.414-6.546.43-1.097.968-2.151 1.601-3.146L3.273 12l-.044-.072a18.6 18.6 0 0 1-1.604-3.142c-1.11-2.895-.933-5.201.417-6.552.744-.744 1.813-1.143 3.107-1.143 1.894 0 4.21.844 6.59 2.334l.071.044.07-.044c2.376-1.488 4.693-2.334 6.587-2.334 1.294 0 2.363.4 3.105 1.143 1.348 1.35 1.527 3.66.414 6.55a18.5 18.5 0 0 1-1.601 3.142l-.046.074.044.07c.665 1.062 1.21 2.123 1.603 3.144 1.113 2.89.936 5.2-.414 6.55-.742.744-1.811 1.143-3.103 1.143-1.896 0-4.21-.847-6.59-2.335l-.073-.043-.07.043c-2.378 1.49-4.695 2.337-6.59 2.337-1.293 0-2.362-.4-3.104-1.143zm18.122-5.848a15.5 15.5 0 0 0-.942-1.985l-.1-.175-.123.16a29.4 29.4 0 0 1-5.275 5.275l-.16.125.175.098c1.739.975 3.403 1.545 4.728 1.545.748 0 1.337-.184 1.724-.572.4-.4.582-1.03.571-1.79-.01-.763-.212-1.68-.598-2.683zM11.73 18.11l.077.057.078-.057a26.4 26.4 0 0 0 6.05-6.03l.057-.079-.056-.076a26.2 26.2 0 0 0-6.05-6.033l-.08-.055-.076.057a26.4 26.4 0 0 0-6.052 6.03L5.62 12l.057.079a26.3 26.3 0 0 0 6.052 6.032zm8.464-14.494c-.39-.388-.98-.571-1.726-.571-1.327 0-2.99.567-4.728 1.544l-.175.099.16.122a29.2 29.2 0 0 1 5.275 5.275l.122.16.099-.175c.375-.67.698-1.335.944-1.985.386-1 .59-1.916.598-2.68.011-.761-.17-1.39-.57-1.789M3.447 8.085q.404 1.027.945 1.986l.1.173.122-.16A29.2 29.2 0 0 1 9.888 4.81l.161-.122-.174-.099c-1.741-.977-3.404-1.544-4.73-1.544-.749 0-1.338.183-1.724.571-.4.4-.58 1.028-.57 1.79.01.763.212 1.68.596 2.68m0 7.831c-.386 1.002-.587 1.918-.595 2.682-.011.76.168 1.39.567 1.789.39.388.98.572 1.726.572 1.326 0 2.989-.568 4.728-1.545l.174-.098-.16-.125a29.2 29.2 0 0 1-5.273-5.275l-.124-.16-.098.175q-.542.961-.945 1.985"
/>
<path
fill="currentColor"
d="M12.744 14.267a2.45 2.45 0 0 1-3.04-3.527 2.452 2.452 0 1 1 3.038 3.53z"
/>
</svg>
);
export default DeepThinking;

View File

@ -0,0 +1,18 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Deepseek = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
fill="#4D6BFE"
d="M23.44 4.823c-.245-.123-.351.111-.495.23-.049.039-.09.089-.132.135-.36.392-.778.649-1.327.618-.802-.046-1.486.211-2.091.837-.129-.772-.556-1.232-1.206-1.528-.341-.154-.685-.307-.923-.641-.166-.238-.211-.503-.295-.765-.053-.157-.106-.318-.283-.345-.194-.03-.269.134-.345.272-.302.565-.42 1.187-.408 1.817.026 1.417.613 2.546 1.777 3.348.133.092.167.185.125.32-.08.275-.174.544-.257.82-.053.177-.132.216-.318.139a5.4 5.4 0 0 1-1.68-1.163c-.827-.818-1.576-1.72-2.51-2.427a11 11 0 0 0-.665-.465c-.953-.945.124-1.72.374-1.813.26-.096.09-.426-.753-.422s-1.614.292-2.597.676c-.144.058-.296.1-.45.134a9.1 9.1 0 0 0-2.788-.1c-1.822.208-3.278 1.087-4.348 2.589C.559 8.893.256 10.944.627 13.083c.39 2.254 1.517 4.12 3.249 5.58 1.796 1.512 3.864 2.253 6.224 2.111 1.433-.084 3.029-.28 4.829-1.835.454.23.93.323 1.72.392.61.057 1.195-.031 1.65-.127.71-.154.66-.826.404-.949-2.084-.99-1.626-.587-2.042-.913 1.059-1.28 2.654-2.608 3.278-6.912.05-.342.008-.557 0-.834-.004-.169.034-.234.224-.253a4 4 0 0 0 1.493-.469c1.35-.752 1.895-1.989 2.023-3.471.02-.227-.003-.461-.238-.58m-11.763 13.34c-2.02-1.62-3-2.154-3.404-2.131-.378.023-.31.465-.227.753.087.284.201.48.36.73.11.164.185.41-.11.594-.65.411-1.78-.138-1.834-.165-1.316-.79-2.417-1.835-3.192-3.264a10.1 10.1 0 0 1-1.255-4.423c-.02-.38.09-.515.46-.584a4.4 4.4 0 0 1 1.48-.039c2.06.308 3.816 1.249 5.286 2.738.84.849 1.475 1.863 2.13 2.854.695 1.052 1.444 2.054 2.397 2.876.336.288.605.506.862.668-.775.088-2.069.107-2.953-.607m.968-6.355a.3.3 0 0 1 .4-.284.3.3 0 0 1 .194.284.3.3 0 0 1-.184.28.3.3 0 0 1-.23 0 .3.3 0 0 1-.18-.28m3.006 1.574c-.192.081-.386.15-.571.158-.287.015-.601-.104-.771-.25-.265-.226-.454-.353-.533-.749a1.7 1.7 0 0 1 .014-.58c.069-.322-.007-.53-.23-.718-.181-.153-.412-.195-.666-.195a.53.53 0 0 1-.327-.14.25.25 0 0 1-.06-.193.3.3 0 0 1 .032-.097c.027-.054.155-.185.185-.208.345-.2.742-.134 1.108.016.34.142.597.403.968.771.379.446.447.569.662.903.17.26.325.53.43.837.066.192-.018.35-.241.445"
/>
</svg>
);
export default Deepseek;

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const DivideCircle = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<g
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
clipPath="url(#prefix__a)"
>
<path d="M8 12h8M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10" />
</g>
<defs>
<clipPath id="prefix__a">
<path fill="#fff" d="M0 0h24v24H0z" />
</clipPath>
</defs>
</svg>
);
export default DivideCircle;

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const DivideSquare = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<g
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
clipPath="url(#prefix__a)"
>
<path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2M8 12h8" />
</g>
<defs>
<clipPath id="prefix__a">
<path fill="#fff" d="M0 0h24v24H0z" />
</clipPath>
</defs>
</svg>
);
export default DivideSquare;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const DollarSign = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 2v20M17 5H9.5a3.5 3.5 0 1 0 0 7h5a3.5 3.5 0 1 1 0 7H6"
/>
</svg>
);
export default DollarSign;

View File

@ -0,0 +1,21 @@
import * as React from 'react';
import type { SVGProps } from 'react';
const Download = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"
/>
</svg>
);
export default Download;

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