Compare commits

..

25 Commits

Author SHA1 Message Date
737014b90f update codemirror 2025-06-03 19:14:50 +08:00
43980ae43c feat: update 2025-05-24 01:49:20 +08:00
3011a92c73 update 2025-05-23 11:43:01 +08:00
784aa6380e save registry 2025-05-23 11:38:33 +08:00
8b2313f817 update 2025-05-18 17:32:18 +08:00
10fe490879 update copy form talkshow admin 2025-05-18 16:37:36 +08:00
9a0e22ff8a temp 2025-05-18 02:36:18 +08:00
54a486427b feat: add codemirror perf 2025-05-12 15:17:02 +08:00
55b1c5fdca update 2025-05-12 14:33:04 +08:00
64bc50f1e8 update codemirror 2025-05-12 14:24:45 +08:00
e7bfee884d temp 2025-05-11 00:27:03 +08:00
bb7ee2d2a5 temp 2025-05-10 14:29:36 +08:00
b4c367b799 temp 2025-05-10 03:49:47 +08:00
1d10cf5888 Merge branch 'main' of git.xiongxiao.me:kevisual/theme 2025-05-10 00:19:26 +08:00
7b7a647612 update 2025-05-10 00:18:43 +08:00
6c8effeaf3 fix: tailwind 2024-11-28 02:14:26 +08:00
e8b0e353ef fix 2024-11-28 01:05:49 +08:00
99cfa7f34d feat: add loading 2024-11-28 01:02:56 +08:00
c5a509e4e8 add build vite plugins 2024-11-04 11:54:37 +08:00
5e67c93b63 update:抽离和合并内容 2024-11-03 03:28:20 +08:00
1327438f89 temp: 修改theme 2024-10-30 13:18:57 +08:00
4784ac623b fix: 添加打包 2024-10-19 19:11:45 +08:00
c4e0ec2a53 fix 2024-10-12 19:19:57 +08:00
bff92667f4 fix 2024-10-08 17:11:30 +08:00
66aae218f3 modal change 2024-09-29 01:40:16 +08:00
135 changed files with 10363 additions and 2935 deletions

67
.gitignore vendored
View File

@@ -1,9 +1,64 @@
node_modules
.pnpm-debug.log
.vscode
dist
build
.env
# mac
.DS_Store
.env*
!.env*example
dist
# build
/build
/logs
.turbo
pack-dist
# astro
.astro
# next
.next
# nuxt
.nuxt
# vercel
.vercel
# vuepress
.vuepress/dist
# coverage
coverage/
# typescript
*.tsbuildinfo
# debug logs
*.log
*.tmp
# vscode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# idea
.idea
# system
Thumbs.db
ehthumbs.db
Desktop.ini
# temp files
*.tmp
*.temp
# local development
*.local

3
.npmrc
View File

@@ -1 +1,2 @@
@abearxiong:registry=https://npm.pkg.github.com
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"tailwindCSS.classFunctions": ["cva", "cx"]
}

3
apps/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"tailwindCSS.classFunctions": ["cva", "cx"]
}

66
apps/demo/.gitignore vendored Normal file
View File

@@ -0,0 +1,66 @@
node_modules
# mac
.DS_Store
.env*
!.env*example
/dist
# build
/build
/logs
.turbo
/pack-dist
# astro
.astro
# next
.next
# nuxt
.nuxt
# vercel
.vercel
# vuepress
.vuepress/dist
# coverage
coverage/
# typescript
*.tsbuildinfo
# debug logs
*.log
*.tmp
# vscode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# idea
.idea
# system
Thumbs.db
ehthumbs.db
Desktop.ini
# temp files
*.tmp
*.temp
# local development
*.local
public/r

3
apps/demo/.npmrc Normal file
View File

@@ -0,0 +1,3 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
ignore-workspace-root-check=true

View File

@@ -0,0 +1,28 @@
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import react from '@astrojs/react';
// import sitemap from '@astrojs/sitemap';
import pkgs from './package.json';
import tailwindcss from '@tailwindcss/vite';
const isDev = process.env.NODE_ENV === 'development';
export default defineConfig({
// ...
// site: 'https://kevisual.xiongxiao.me/root/astro/',
base: isDev ? undefined : pkgs.basename,
integrations: [
mdx(),
react(), //
// sitemap(), // sitemap must be site has a domain
],
vite: {
plugins: [tailwindcss()],
define: {
BASE_NAME: JSON.stringify(pkgs.basename),
DEV_SERVER: JSON.stringify(isDev),
},
},
});

21
apps/demo/components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

35
apps/demo/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "demo",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"build:registry": "pnpm dlx shadcn@latest build"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.10.0",
"type": "module",
"devDependencies": {
"@astrojs/mdx": "^4.2.6",
"@astrojs/react": "^4.2.7",
"@kevisual/types": "^0.0.10",
"@tailwindcss/vite": "^4.1.6",
"@types/react": "^19.1.3",
"astro": "^5.7.12",
"tailwindcss": "^4.1.6",
"tailwind-merge": "^3.2.0",
"tw-animate-css": "^1.2.9"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.509.0",
"react": "^19.1.0",
"react-dom": "^19.1.0"
}
}

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1 @@
export const basename = DEV_SERVER ? '' : '/root/center';

View File

@@ -0,0 +1,4 @@
---
import '../styles/global.css'
---
index

View File

@@ -0,0 +1,120 @@
@import 'tailwindcss';
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

22
apps/demo/tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"extends": "@kevisual/types/json/frontend.json",
"compilerOptions": {
"baseUrl": ".",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@kevisual"
],
"paths": {
"@/*": [
"src/*"
],
"@/registry/*": [
"registry/*"
]
},
},
"include": [
"src/**/*",
"registry/**/*"
],
}

66
libs/registry/.gitignore vendored Normal file
View File

@@ -0,0 +1,66 @@
node_modules
# mac
.DS_Store
.env*
!.env*example
/dist
# build
/build
/logs
.turbo
/pack-dist
# astro
.astro
# next
.next
# nuxt
.nuxt
# vercel
.vercel
# vuepress
.vuepress/dist
# coverage
coverage/
# typescript
*.tsbuildinfo
# debug logs
*.log
*.tmp
# vscode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# idea
.idea
# system
Thumbs.db
ehthumbs.db
Desktop.ini
# temp files
*.tmp
*.temp
# local development
*.local
public/r

3
libs/registry/.npmrc Normal file
View File

@@ -0,0 +1,3 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
ignore-workspace-root-check=true

10
libs/registry/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"tailwindCSS.classFunctions": [
"cva",
"cx"
],
"workbench.editorAssociations": {
// "*.md": "vscode.markdown.preview.editor" // 预览打开
"*.md": "default" // 默认打开
}
}

View File

@@ -0,0 +1,43 @@
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import react from '@astrojs/react';
// import sitemap from '@astrojs/sitemap';
import pkgs from './package.json';
import tailwindcss from '@tailwindcss/vite';
const isDev = process.env.NODE_ENV === 'development';
export default defineConfig({
// ...
// site: 'https://kevisual.xiongxiao.me/root/astro/',
base: pkgs.basename,
markdown: {
// 适用于 MDX 和普通 Markdown 的配置
syntaxHighlight: 'shiki',
shikiConfig: {
theme: 'nord',
},
remarkPlugins: [
'remark-gfm', // GitHub Flavored Markdown
['remark-toc', { headings: ['h2', 'h3'] }], // 目录生成
],
rehypePlugins: [
'rehype-slug', // 为标题添加 ID
'rehype-autolink-headings', // 为标题添加链接
],
},
integrations: [
mdx(),
react(), //
// sitemap(), // sitemap must be site has a domain
],
vite: {
plugins: [tailwindcss()],
define: {
BASE_NAME: JSON.stringify(pkgs.basename),
DEV_SERVER: JSON.stringify(isDev),
},
},
});

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/global.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,14 @@
{
"metadata": {
"share": "public"
},
"syncDirectory": [
{
"files": [
"registry/**/*"
],
"registry": "https://kevisual.xiongxiao.me/root/ai/code",
"replace": {}
}
]
}

View File

@@ -0,0 +1,63 @@
{
"name": "@kevisual/registry",
"version": "0.0.1",
"description": "",
"main": "index.js",
"basename": "/root/registry",
"scripts": {
"dev": "astro dev",
"build": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide && astro build",
"preview": "astro preview",
"build:registry": "pnpm dlx shadcn@latest build",
"machine-translate": "inlang machine translate --project project.inlang",
"pub": ""
},
"files": [
"registry",
"dist"
],
"publishConfig": {
"access": "public"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.10.0",
"type": "module",
"devDependencies": {
"@astrojs/mdx": "^4.2.6",
"@astrojs/react": "^4.2.7",
"@kevisual/types": "^0.0.10",
"@tailwindcss/vite": "^4.1.6",
"@types/react": "^19.1.3",
"astro": "^5.7.12",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"remark-toc": "^9.0.0",
"tailwindcss": "^4.1.6",
"tw-animate-css": "^1.2.9"
},
"dependencies": {
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"i18next": "^25.1.2",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-http-backend": "^3.0.2",
"lucide-react": "^0.509.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.5.1",
"react-toastify": "^11.0.5",
"tailwind-merge": "^3.2.0"
},
"exports": {
".": "./registry/index.js",
"./components/*": "./registry/components/*",
"./hooks/*": "./registry/hooks/*",
"./lib/*": "./registry/lib/*",
"./pages/*": "./registry/pages/*",
"./styles/*": "./registry/styles/*",
"./types/*": "./registry/types/*"
}
}

View File

@@ -0,0 +1,32 @@
{
"$schema": "https://ui.shadcn.com/schema/registry.json",
"name": "shadcn",
"homepage": "https://ui.shadcn.com",
"items": [
{
"name": "hello-world",
"type": "registry:block",
"title": "Hello World",
"description": "A simple hello world component.",
"files": [
{
"path": "registry/hello-world/hello-world.tsx",
"type": "registry:component"
}
]
},
{
"name": "basename",
"type": "registry:file",
"title": "Basename",
"description": "The basename for the router.",
"files": [
{
"path": "registry/modules/basename.ts",
"type": "registry:file",
"target": "src/modules/basename.ts"
}
]
}
]
}

View File

@@ -0,0 +1,27 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// 导入你的翻译资源
import enTranslation from './locales/en/translation.json';
import zhTranslation from './locales/zh/translation.json';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: {
translation: enTranslation,
},
zh: {
translation: zhTranslation,
},
},
fallbackLng: 'en',
interpolation: {
escapeValue: false, // React 已经处理了转义
},
});
export default i18n;

View File

@@ -0,0 +1,4 @@
{
"welcome": "Welcome to our site",
"about": "About us"
}

View File

@@ -0,0 +1,4 @@
{
"welcome": "欢迎来到我们的网站",
"about": "关于我们"
}

View File

@@ -0,0 +1,36 @@
---
export interface Props {
children: any;
}
---
<html lang='zh'>
<header>
<meta charset='UTF-8' />
<title>Docs</title>
<link
rel='stylesheet'
href='https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.8.1/github-markdown-light.min.css'
integrity='sha512-X175XRJAO6PHAUi8AA7GP8uUF5Wiv+w9bOi64i02CHKDQBsO1yy0jLSKaUKg/NhRCDYBmOLQCfKaTaXiyZlLrw=='
crossorigin='anonymous'
referrerpolicy='no-referrer'
/>
</header>
<body>
<header>
<slot name='header'>
<h1>My Site Header</h1>
</slot>
</header>
<main class='markdown-body' style='padding: 1rem'>
<slot />
</main>
<footer>
<slot name='footer'>
<p>Copyrignt &copy; 2025</p>
</slot>
</footer>
</body>
</html>

View File

@@ -0,0 +1,80 @@
'use client';
import * as React from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/a/button';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
type Option = {
value?: string;
label: string;
};
type AutoComplateProps = {
options: Option[];
placeholder?: string;
value?: string;
onChange?: (value: string) => void;
width?: string;
};
export function AutoComplate(props: AutoComplateProps) {
const [open, setOpen] = React.useState(false);
const [value, _setValue] = React.useState('');
const setValue = (value: string) => {
props?.onChange?.(value);
_setValue(value);
};
const showLabel = React.useMemo(() => {
const option = props.options.find((option) => option.value === value);
if (option) {
return option?.label;
}
if (props.value) return props.value;
if (value) return value;
return 'Select ...';
}, [value, props.value]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant='outline' role='combobox' aria-expanded={open} className={cn(props.width ? props.width : 'w-[400px]', 'justify-between')}>
{showLabel}
<ChevronsUpDown className='ml-2 h-4 w-4 shrink-0 opacity-50' />
</Button>
</PopoverTrigger>
<PopoverContent className={cn(props.width ? props.width : 'w-[400px]', ' p-0')}>
<Command>
<CommandInput
placeholder={props.placeholder ?? 'Search options...'}
onKeyDown={(e: any) => {
if (e.key === 'Enter') {
setOpen(false);
const value = e.target?.value || '';
setValue(value.trim());
}
}}
/>
<CommandList>
<CommandEmpty>No options found.</CommandEmpty>
<CommandGroup>
{props.options.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? '' : currentValue);
setOpen(false);
}}>
<Check className={cn('mr-2 h-4 w-4', value === framework.value ? 'opacity-100' : 'opacity-0')} />
{framework.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@@ -0,0 +1,18 @@
import { Button as UiButton, ButtonProps } from '@/components/ui/button';
import { cn } from '@/lib/utils';
export const IconButton: typeof UiButton = (props) => {
return <UiButton variant='ghost' size='icon' {...props} className={cn('h-8 w-8 cursor-pointer', props?.className)} />;
};
export const Button: typeof UiButton = (props) => {
return <UiButton variant='ghost' {...props} className={cn('cursor-pointer', props?.className)} />;
};
export const ButtonTextIcon = (props: ButtonProps & { icon: React.ReactNode }) => {
return (
<UiButton variant={'outline'} size='sm' {...props} className={cn('cursor-pointer flex items-center gap-2', props?.className)}>
{props.icon}
{props.children}
</UiButton>
);
};

View File

@@ -0,0 +1,100 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { useEffect, useMemo, useState } from 'react';
type useConfirmOptions = {
confrimProps: ConfirmProps;
};
type Fn = () => void;
export const useConfirm = (opts?: useConfirmOptions) => {
const [open, setOpen] = useState(false);
type ConfirmOptions = {
onOk?: Fn;
onCancel?: Fn;
};
const confirm = (opts?: ConfirmOptions) => {
setOpen(true);
};
const module = useMemo(() => {
return <Confirm {...opts?.confrimProps} hasTrigger={false} open={open} setOpen={setOpen} />;
}, [open]);
return {
module: module,
open,
setOpen,
confirm,
};
};
type ConfirmProps = {
children?: React.ReactNode;
tip?: React.ReactNode;
title?: string;
description?: string;
onOkText?: string;
onCancelText?: string;
onOk?: Fn;
onCancle?: Fn;
hasTrigger?: boolean;
open?: boolean;
setOpen?: (open: boolean) => void;
footer?: React.ReactNode;
};
export const Confirm = (props: ConfirmProps) => {
const [isOpen, setIsOpen] = useState(props.open);
const hasTrigger = props.hasTrigger ?? true;
useEffect(() => {
setIsOpen(props.open);
}, [props.open]);
return (
<AlertDialog
open={isOpen}
onOpenChange={(v) => {
setIsOpen(v);
props?.setOpen?.(v);
}}>
{hasTrigger && (
<>
{props?.children && <AlertDialogTrigger asChild>{props?.children ?? props?.tip ?? '提示'}</AlertDialogTrigger>}
{!props?.children && <AlertDialogTrigger>{props?.tip ?? '提示'}</AlertDialogTrigger>}
</>
)}
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{props?.title ?? '是否确认删除?'}</AlertDialogTitle>
<AlertDialogDescription>{props?.description ?? '此操作无法撤销,是否继续。'}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
{props?.footer && <div className='flex gap-2'>{props?.footer}</div>}
{!props?.footer && (
<>
<AlertDialogCancel
onClick={(e) => {
props?.onCancle?.();
e.stopPropagation();
}}>
{props?.onCancelText ?? '取消'}
</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
props?.onOk?.();
e.stopPropagation();
}}>
{props?.onOkText ?? '确定'}
</AlertDialogAction>{' '}
</>
)}
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -0,0 +1,31 @@
import { Select as UISelect, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
type Option = {
value: string;
label?: string;
};
type SelectProps = {
options?: Option[];
value?: string;
placeholder?: string;
onChange?: (value: string) => any;
};
export const Select = (props: SelectProps) => {
const options = props.options || [];
return (
<UISelect onValueChange={props.onChange} value={props.value}>
<SelectTrigger className='w-[180px]'>
<SelectValue placeholder={props.placeholder || '请选择'} />
</SelectTrigger>
<SelectContent>
{options.map((item, index) => {
return (
<SelectItem key={index} value={item.value}>
{item.label}
</SelectItem>
);
})}
</SelectContent>
</UISelect>
);
};

View File

@@ -0,0 +1,14 @@
import { Tooltip as UITooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
export const Tooltip = (props: { children?: React.ReactNode; title?: string }) => {
return (
<TooltipProvider>
<UITooltip>
<TooltipTrigger asChild>{props.children}</TooltipTrigger>
<TooltipContent>
<p>{props.title}</p>
</TooltipContent>
</UITooltip>
</TooltipProvider>
);
};

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { twMerge } from 'tailwind-merge';
const button = cva('button', {
variants: {
intent: {
primary: ['bg-blue-500', 'text-white', 'border-transparent'],
secondary: ['bg-white', 'text-gray-800', 'border-gray-400'],
},
size: {
small: ['text-sm', 'py-1', 'px-2'],
medium: ['text-base', 'py-2', 'px-4'],
},
disabled: {
false: null,
true: ['opacity-50', 'cursor-not-allowed'],
},
},
compoundVariants: [
{
intent: 'primary',
disabled: false,
class: 'hover:bg-blue-600',
},
{
intent: 'secondary',
disabled: false,
class: 'hover:bg-gray-100',
},
{ intent: 'primary', size: 'medium', class: 'uppercase' },
],
defaultVariants: {
disabled: false,
intent: 'primary',
size: 'medium',
},
});
export interface ButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'disabled'>, VariantProps<typeof button> {}
export const Button: React.FC<ButtonProps> = ({ className, intent, size, disabled, ...props }) => (
<button className={twMerge('cursor-pointer', button({ intent, size, disabled, className }))} disabled={disabled || undefined} {...props} />
);

View File

@@ -0,0 +1,56 @@
// components/card.ts
import type { VariantProps } from 'class-variance-authority';
import { cva, cx } from 'class-variance-authority';
import { cn } from '@/lib/utils';
/**
* Box
*/
export type BoxProps = VariantProps<typeof box>;
export const box = cva(['box', 'box-border'], {
variants: {
margin: { 0: 'm-0', 2: 'm-2', 4: 'm-4', 8: 'm-8' },
padding: { 0: 'p-0', 2: 'p-2', 4: 'p-4', 8: 'p-8' },
},
defaultVariants: {
margin: 0,
padding: 0,
},
});
/**
* Card
*/
type CardBaseProps = VariantProps<typeof cardBase>;
const cardBase = cva(['card', 'border-solid', 'border-slate-300', 'rounded'], {
variants: {
shadow: {
md: 'drop-shadow-md',
lg: 'drop-shadow-lg',
xl: 'drop-shadow-xl',
},
},
});
export interface CardProps extends BoxProps, CardBaseProps {}
export const card = ({ margin, padding, shadow }: CardProps = {}) => cx(box({ margin, padding }), cardBase({ shadow }));
type CardBlankProps = {
number?: number;
className?: string;
};
/**
* CardBlank 空的卡片,用于占位
* @param props
* @returns
*/
export const CardBlank = (props: CardBlankProps) => {
const { number = 4, className } = props;
return (
<>
{new Array(number).fill(0).map((_, index) => {
return <div key={index} className={cn('w-[300px] shark-0', className)}></div>;
})}
</>
);
};

View File

@@ -0,0 +1,74 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend'; // 引入 Backend 插件
import { useLayoutEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
export { useTranslation };
type I18NextProviderProps = {
children: React.ReactNode;
basename?: string;
noUse?: boolean;
};
export const initI18n = async (basename: string) => {
// 初始化 i18n
return new Promise((resolve) => {
i18n
.use(Backend) // 使用 Backend 插件
.use(initReactI18next)
.init(
{
backend: {
loadPath: `${basename}/locales/{{lng}}/{{ns}}.json`, // 指定 JSON 文件的路径
},
lng: 'zh', // 默认语言
fallbackLng: 'en', // 备用语言
interpolation: {
escapeValue: false, // react 已经安全地处理了转义
},
},
(e) => {
console.log('e', e);
resolve(true);
},
);
});
};
/**
* 国际化组件,初始化
* @param props
* @returns
*/
export const I18NextProvider = (props: I18NextProviderProps) => {
const { children, basename, noUse } = props;
if (noUse) {
return <>{children}</>;
}
const [init, setInit] = useState(false);
useLayoutEffect(() => {
initCheck();
}, []);
const initCheck = async () => {
let _currentBasename = '';
if (typeof basename === 'undefined') {
const local = localStorage.getItem('locale-basename');
if (local) {
_currentBasename = local;
} else {
_currentBasename = '';
}
} else {
_currentBasename = basename;
}
if (_currentBasename === '/') {
_currentBasename = '';
}
initI18n(_currentBasename);
setInit(true);
};
if (!init) {
return <></>;
}
return <>{children}</>;
};

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
export class ReactRenderer {
component: any;
element: HTMLElement;
ref: React.RefObject<any>;
props: any;
root: any;
constructor(component: any, { props }: any) {
this.component = component;
this.element = document.createElement('div');
this.ref = React.createRef();
this.props = {
...props,
ref: this.ref,
};
this.root = createRoot(this.element);
this.render();
}
updateProps(props: any) {
this.props = {
...this.props,
...props,
};
this.render();
}
render() {
this.root.render(React.createElement(this.component, this.props));
}
destroy() {
this.root.unmount();
}
}
export default ReactRenderer;

View File

@@ -0,0 +1,155 @@
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"
function AlertDialog({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return (
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
)
}
function AlertDialogPortal({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return (
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
)
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
)
}
function AlertDialogHeader({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function AlertDialogFooter({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="alert-dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn("text-lg font-semibold", className)}
{...props}
/>
)
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return (
<AlertDialogPrimitive.Action
className={cn(buttonVariants(), className)}
{...props}
/>
)
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: "outline" }), className)}
{...props}
/>
)
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

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

View File

@@ -0,0 +1,53 @@
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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
destructive:
'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline: 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
export type ButtonProps = React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
};
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : 'button';
return <Comp data-slot='button' className={cn(buttonVariants({ variant, size, className }))} {...props} />;
}
export { Button, buttonVariants };

View File

@@ -0,0 +1,175 @@
import * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
function Command({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
className
)}
{...props}
/>
)
}
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
}) {
return (
<Dialog {...props}>
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className="overflow-hidden p-0">
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[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>
)
}
function CommandInput({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div
data-slot="command-input-wrapper"
className="flex h-9 items-center gap-2 border-b px-3"
>
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn(
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
className
)}
{...props}
/>
)
}
function CommandEmpty({
...props
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return (
<CommandPrimitive.Empty
data-slot="command-empty"
className="py-6 text-center text-sm"
{...props}
/>
)
}
function CommandGroup({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
)
}
function CommandSeparator({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({
className,
...props
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@@ -0,0 +1,133 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,165 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} 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 } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
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
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@@ -0,0 +1,46 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover 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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@@ -0,0 +1,183 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover 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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
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)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@@ -0,0 +1,59 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground 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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@@ -0,0 +1,3 @@
export const HelloWorld = () => {
return <div>Hello Wrold</div>;
};

View File

@@ -0,0 +1,35 @@
{
"metadata": {
"share": "public"
},
"syncDirectory": [
{
"files": [
"components/**/*"
],
"registry": "https://kevisual.xiongxiao.me/root/ai/code/registry",
"replace": {}
},
{
"files": [
"lib/**/*"
],
"registry": "https://kevisual.xiongxiao.me/root/ai/code/registry",
"replace": {}
},
{
"files": [
"modules/**/*"
],
"registry": "https://kevisual.xiongxiao.me/root/ai/code/registry",
"replace": {}
},
{
"files": [
"styles/**/*"
],
"registry": "https://kevisual.xiongxiao.me/root/ai/code/registry",
"replace": {}
}
]
}

View File

@@ -0,0 +1,194 @@
import { EventEmitter } from 'eventemitter3';
type VideoStreamPlayerOptions = {
emitter?: EventEmitter;
};
export class VideoStreamPlayer {
emitter: EventEmitter;
audioContext: AudioContext;
audioBuffer: AudioBuffer | null = null;
audioQueue: Uint8Array[] = [];
decodedBuffers: AudioBuffer[] = []; // 存储已解码的音频缓冲区
currentSource: AudioBufferSourceNode | null = null;
audioElement: HTMLAudioElement;
processing: boolean = false;
canPlaying: boolean = false;
isPlaying: boolean = false;
bufferingThreshold: number = 3; // 预缓冲的音频块数量
decodePromises: Promise<void>[] = []; // 跟踪解码进程
nextPlayTime: number = 0; // 下一个音频片段的开始时间
playStatus: 'paused' | 'playing' | 'buffering' | 'ended' = 'paused';
constructor(opts?: VideoStreamPlayerOptions) {
this.emitter = opts?.emitter || new EventEmitter();
this.audioContext = new AudioContext();
this.audioElement = new Audio();
this.audioElement.autoplay = false;
// 确保在页面交互后恢复音频上下文(解决自动播放限制问题)
document.addEventListener(
'click',
() => {
if (this.audioContext.state === 'suspended') {
this.audioContext.resume();
}
},
{ once: true },
);
}
// 处理收到的音频数据
async appendAudioChunk(chunk: ArrayBuffer | Uint8Array | string) {
let audioData: Uint8Array;
// 处理不同类型的输入数据
if (typeof chunk === 'string') {
// 如果是base64编码的数据
const binary = atob(chunk);
audioData = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
audioData[i] = binary.charCodeAt(i);
}
} else if (chunk instanceof ArrayBuffer) {
audioData = new Uint8Array(chunk);
} else {
audioData = chunk;
}
// 将音频数据加入队列
this.audioQueue.push(audioData);
// 开始解码音频,不等待前面的解码完成
this.decodeAudio();
// 如果当前没有在播放且可以播放,并且已有足够缓冲则开始播放
if (!this.isPlaying && this.canPlaying && this.decodedBuffers.length >= this.bufferingThreshold) {
this.startPlaying();
}
}
// 异步解码音频,不阻塞主线程
async decodeAudio() {
if (this.processing || this.audioQueue.length === 0) return;
this.processing = true;
const chunk = this.audioQueue.shift()!;
try {
// 解码音频数据
const decodePromise = this.audioContext
.decodeAudioData(chunk.buffer.slice(0))
.then((audioBuffer) => {
this.decodedBuffers.push(audioBuffer);
// 如果已经可以开始播放但尚未播放,开始播放
if (this.canPlaying && !this.isPlaying && this.decodedBuffers.length >= this.bufferingThreshold) {
this.startPlaying();
}
const index = this.decodePromises.indexOf(decodePromise as any);
if (index > -1) {
this.decodePromises.splice(index, 1);
}
})
.catch((error) => {
console.error('音频解码错误:', error);
const index = this.decodePromises.indexOf(decodePromise as any);
if (index > -1) {
this.decodePromises.splice(index, 1);
}
});
this.decodePromises.push(decodePromise as any);
} finally {
this.processing = false;
// 继续处理队列中的下一个
if (this.audioQueue.length > 0) {
this.decodeAudio();
}
}
}
// 开始播放
startPlaying() {
if (this.decodedBuffers.length === 0 || this.isPlaying) return;
this.isPlaying = true;
this.nextPlayTime = this.audioContext.currentTime;
this.scheduleNextBuffer();
this.emitter.emit('play-start');
}
// 安排播放下一个音频缓冲区
scheduleNextBuffer() {
if (this.decodedBuffers.length === 0) {
// 没有更多缓冲区时,如果队列中也没有待解码的数据,就标记为未播放状态
if (this.audioQueue.length === 0 && this.decodePromises.length === 0) {
this.isPlaying = false;
this.emitter.emit('play-end', true);
}
return;
}
const audioBuffer = this.decodedBuffers.shift()!;
const source = this.audioContext.createBufferSource();
this.currentSource = source;
source.buffer = audioBuffer;
source.connect(this.audioContext.destination);
// 在确切的时间安排播放,确保无缝连接
source.start(this.nextPlayTime);
this.nextPlayTime = parseFloat((this.nextPlayTime + audioBuffer.duration).toFixed(4));
// console.log('audioBuffer.duration start', audioBuffer.duration, this.nextPlayTime);
// 在音频播放结束前安排下一个缓冲区(提前一点安排可以减少间隙)
const safetyOffset = Math.min(0.05, audioBuffer.duration / 2); // 至少提前50ms或一半时长
setTimeout(() => {
this.scheduleNextBuffer();
}, (audioBuffer.duration - safetyOffset) * 1000);
// 发出事件通知
this.emitter.emit('playing', { duration: audioBuffer.duration });
// 如果缓冲区不足,继续解码
if (this.decodedBuffers.length < this.bufferingThreshold && this.audioQueue.length > 0 && !this.processing) {
this.decodeAudio();
}
}
// 处理WebSocket接收到的音频数据
handleWebSocketAudio(data: any) {
if (data && data.audio) {
this.appendAudioChunk(data.audio);
}
}
// 停止播放
stop() {
if (this.currentSource) {
try {
this.currentSource.stop();
} catch (e) {
// 可能已经停止,忽略错误
}
this.currentSource.disconnect();
this.currentSource = null;
}
this.audioQueue = [];
this.decodedBuffers = [];
this.decodePromises = [];
this.processing = false;
this.isPlaying = false;
this.canPlaying = false;
this.nextPlayTime = 0;
this.emitter.emit('stopped');
}
setCanPlaying(canPlaying = true) {
this.canPlaying = canPlaying;
// 如果设置为可播放,且有足够的解码缓冲区,则开始播放
if (canPlaying && !this.isPlaying && this.decodedBuffers.length >= this.bufferingThreshold) {
this.startPlaying();
}
}
}

View File

@@ -0,0 +1,75 @@
import { EventEmitter } from 'eventemitter3';
type VideoPlayerOptions = {
url?: string;
emitter?: EventEmitter;
};
export class VideoPlayer {
url?: string;
isPlaying = false;
audio?: HTMLAudioElement;
emitter?: EventEmitter;
private endedHandler?: () => void;
constructor(opts?: VideoPlayerOptions) {
this.url = opts?.url;
this.emitter = opts?.emitter || new EventEmitter();
}
init() {
if (!this.emitter) {
this.emitter = new EventEmitter();
}
return this.emitter;
}
play(url?: string) {
if (this.isPlaying) {
return { code: 400 };
}
const playUrl = url || this.url;
if (!playUrl) {
return { code: 404 };
}
if (playUrl !== this.url) {
this.url = playUrl;
}
// 创建新的Audio对象前确保清理之前的资源
if (this.audio && this.endedHandler) {
this.audio.removeEventListener('ended', this.endedHandler);
}
this.audio = new Audio(playUrl);
this.audio.play();
this.isPlaying = true;
this.emitter?.emit('start', { url: playUrl, status: 'start' });
// 保存引用以便于后续移除
this.endedHandler = () => {
this.audio = undefined;
this.isPlaying = false;
this.emitter?.emit('stop', this.url);
};
this.audio.addEventListener('ended', this.endedHandler);
return { code: 200 };
}
stop() {
if (this.isPlaying) {
// 移除事件监听器
if (this.audio && this.endedHandler) {
this.audio.removeEventListener('ended', this.endedHandler);
}
this.audio?.pause();
this.audio = undefined;
this.isPlaying = false;
}
this.emitter?.emit('stop', this.url);
}
onStop(callback: (url: string) => void) {
this.emitter?.on('stop', callback);
return () => {
this.emitter?.off('stop', callback);
};
}
close() {
this.emitter?.removeAllListeners();
this.emitter = undefined;
}
}

View File

@@ -0,0 +1,21 @@
import { customAlphabet } from 'nanoid';
export const letter = 'abcdefghijklmnopqrstuvwxyz';
export const number = '0123456789';
const alphanumeric = `${letter}${number}`;
export const alphanumericWithDash = `${alphanumeric}-`;
export const uuid = customAlphabet(letter);
export const nanoid = customAlphabet(alphanumeric, 10);
export const nanoidWithDash = customAlphabet(alphanumericWithDash, 10);
/**
* 创建一个随机的 id以字母开头的字符串
* @param number
* @returns
*/
export const randomId = (number: number) => {
const _letter = uuid(1);
return `${_letter}${nanoid(number)}`;
};

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,4 @@
// @ts-ignore
export const basename = BASE_NAME;
console.log(basename);

View File

@@ -0,0 +1,13 @@
import { ToastContainer } from 'react-toastify';
type ToastProviderProps = {
children?: React.ReactNode;
};
export const ToastProvider = ({ children }: ToastProviderProps) => {
return (
<>
{children}
<ToastContainer />
</>
);
};

View File

@@ -0,0 +1,52 @@
import { toast } from 'react-toastify';
// Custom message component
const LoginMessage = (props: ToastLoginProps) => {
const lng = props.lng || 'zh';
const isZH = lng === 'zh';
const en = {
'Please login': 'Please login',
'Click here to go to the login page.': 'Click here to go to the login page.',
};
const zh = {
'Please login': '请登录',
'Click here to go to the login page.': '点击这里前往登录页面',
};
const t = isZH ? zh : en;
const handleClick = () => {
const currentUrl = window.location.href;
const redirect = encodeURIComponent(props?.redirectUrl || currentUrl);
const loginUrl = props?.loginUrl || '/user/login/';
const newUrl = location.origin + loginUrl + '?redirect=' + redirect;
window.open(newUrl, '_self');
};
return (
<div className='msg-container' onClick={handleClick} style={{ cursor: 'pointer' }}>
<p className='msg-title'>{t['Please login']}</p>
<p className='msg-description'>{t['Click here to go to the login page.']}</p>
</div>
);
};
type ToastLoginProps = {
/**
* 登录页面地址, /user/login
*/
loginUrl?: string;
/**
* 登录成功后跳转的地址, 默认是当前页面
*/
redirectUrl?: string;
lng?: 'en' | 'zh';
};
/**
* 登录提示
* @param props
* @example
* toastLogin({
* loginUrl: '/user/login/',
* redirectUrl: window.location.href,
* });
*/
export const toastLogin = (props: ToastLoginProps = {}) => {
toast.info(<LoginMessage {...props} />);
};

View File

@@ -0,0 +1,119 @@
@import 'tailwindcss';
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,78 @@
@import 'tailwindcss';
@theme {
--color-primary: #ffc107;
--color-secondary: #ffa000;
--color-text-primary: #000000;
--color-text-secondary: #000000;
--color-success: #28a745;
--color-scrollbar-thumb: #999999;
--color-scrollbar-track: rgba(0, 0, 0, 0.1);
--color-scrollbar-thumb-hover: #666666;
--scrollbar-color: #ffc107; /* 滚动条颜色 */
}
html,
body {
width: 100%;
height: 100%;
font-size: 16px;
font-family: 'Montserrat', sans-serif;
}
/* font-family */
@utility font-family-mon {
font-family: 'Montserrat', sans-serif;
}
@utility font-family-rob {
font-family: 'Roboto', sans-serif;
}
@utility font-family-int {
font-family: 'Inter', sans-serif;
}
@utility font-family-orb {
font-family: 'Orbitron', sans-serif;
}
@utility font-family-din {
font-family: 'DIN', sans-serif;
}
@utility flex-row-center {
@apply flex flex-row items-center justify-center;
}
@utility flex-col-center {
@apply flex flex-col items-center justify-center;
}
@utility scrollbar {
overflow: auto;
/* 整个滚动条 */
&::-webkit-scrollbar {
width: 3px;
height: 3px;
}
&::-webkit-scrollbar-track {
background-color: var(--color-scrollbar-track);
}
/* 滚动条有滑块的轨道部分 */
&::-webkit-scrollbar-track-piece {
background-color: transparent;
border-radius: 1px;
}
/* 滚动条滑块(竖向:vertical 横向:horizontal) */
&::-webkit-scrollbar-thumb {
cursor: pointer;
background-color: var(--color-scrollbar-thumb);
border-radius: 5px;
}
/* 滚动条滑块hover */
&::-webkit-scrollbar-thumb:hover {
background-color: var(--color-scrollbar-thumb-hover);
}
/* 同时有垂直和水平滚动条时交汇的部分 */
&::-webkit-scrollbar-corner {
display: block; /* 修复交汇时出现的白块 */
}
}

View File

@@ -0,0 +1,14 @@
// @ts-ignore
import { defineCollection, z } from 'astro:content';
import { glob, file } from 'astro/loaders'; // 不适用于旧版 API
const blog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
// pubDate: z.coerce.date(),
// updatedDate: z.coerce.date().optional(),
}),
});
export const collections = { blog };

View File

@@ -0,0 +1,5 @@
---
title: My Blog Post
description: This is a post on My Blog about win-win survival strategies.
---
Your blog post content here.

View File

@@ -0,0 +1,10 @@
---
title: test dir
description: This is a post on My Blog about win-win survival strategies.
---
# Download shadcn
```sh
pnpm dlx shadcn@latest add https://kevisual.xiongxiao.me/root/registry/r/basename.json
```

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -0,0 +1,20 @@
import { toastLogin } from '@/registry/modules/toast/ToastLogin';
import { ToastProvider } from '@/registry/modules/toast/Provider';
import { Button } from '@/registry/components/b/button/button';
import { useTranslation } from 'react-i18next';
import '@/registry/astro/i18n'; // 初始化i18n
export const ShowLogin = () => {
const { t } = useTranslation();
return (
<ToastProvider>
<Button
onClick={() => {
toastLogin();
console.log('clicked');
}}>
{t('welcome')} Click me
</Button>
</ToastProvider>
);
};

View File

@@ -0,0 +1,23 @@
---
import { getCollection, render } from 'astro:content';
import Main from '@/registry/astro/layouts/mdx/main.astro';
// 1. 为每个集合条目生成一个新路径
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map((post) => ({
params: { id: post.id },
props: { post },
}));
}
type Post = {
data: { title: string };
};
// 2. 对于你的模板,你可以直接从 prop 获取条目
const { post } = Astro.props as { post: Post };
const { Content } = await render(post);
---
<Main>
<h1 slot={'header'}>{post.data.title}</h1>
<Content />
</Main>

View File

@@ -0,0 +1,17 @@
---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
console.log('post', posts);
import { basename } from '@/registry/modules/basename';
---
<h1>My posts</h1>
<ul>
{
posts.map((post) => (
<li>
<a href={`${basename}/demo/${post.id}`}>{post.data.title}</a>
</li>
))
}
</ul>

View File

@@ -0,0 +1,76 @@
---
title: Markdown
layout: '@/registry/astro/layouts/mdx/main.astro'
---
## Heading 2
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu ante elementum, ultrices massa ut, ornare lacus. Praesent iaculis, ex ac pellentesque malesuada, arcu mi imperdiet purus, et molestie neque leo quis felis. Nunc et odio bibendum, vestibulum elit sit amet, viverra lorem.
### Heading 3
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu ante elementum, ultrices massa ut, ornare lacus. Praesent iaculis, ex ac pellentesque malesuada, arcu mi imperdiet purus, et molestie neque leo quis felis. Nunc et odio bibendum, vestibulum elit sit amet, viverra lorem.
#### Heading 4
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu ante elementum, ultrices massa ut, ornare lacus. Praesent iaculis, ex ac pellentesque malesuada, arcu mi imperdiet purus, et molestie neque leo quis felis. Nunc et odio bibendum, vestibulum elit sit amet, viverra lorem.
##### Heading 5
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu ante elementum, ultrices massa ut, ornare lacus. Praesent iaculis, ex ac pellentesque malesuada, arcu mi imperdiet purus, et molestie neque leo quis felis. Nunc et odio bibendum, vestibulum elit sit amet, viverra lorem.
###### Heading 6
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu ante elementum, ultrices massa ut, ornare lacus. Praesent iaculis, ex ac pellentesque malesuada, arcu mi imperdiet purus, et molestie neque leo quis felis. Nunc et odio bibendum, vestibulum elit sit amet, viverra lorem.
## Styling text
Fusce imperdiet, **tellus** ornare tempor _cursus_, tellus ipsum fringilla ~~quam~~, in venenatis neque augue vitae **_turpis_**. Nulla sed neque volutpat, eleifend purus <sub>sit amet</sub>, porttitor nisi. Ut id sodales lorem. Suspendisse <sup>auctor</sup> augue nisl, sed placerat enim porttitor at. Etiam eu ipsum suscipit, egestas ante non, eleifend nulla.
## Quoting text
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla in enim sem.
> Mauris dictum augue augue, ut accumsan tellus convallis ut. Nam vitae libero vestibulum, feugiat mi rhoncus, imperdiet dui. In eros diam, sagittis ultrices dolor ac, ultrices cursus sapien. Morbi fringilla porta purus, sed interdum urna luctus vitae.
## Quoting code
Curabitur congue ac `enim` id hendrerit.
```js
const foo = 'bar'
```
## Links
Fusce tincidunt urna at [ultricies](#_) sollicitudin.
## Lists
- Lorem
- Ipsum
- Dolor
- Sit
- Amet
1. Consectetur
2. Adipiscing
## Tables
| Column 1 | Column 2 | Column 3 | Column 4 |
| -------- | -------- | -------- | -------- |
| Cell 1-1 | Cell 1-2 | Cell 1-3 | Cell 1-4 |
| Cell 2-1 | Cell 2-2 | Cell 2-3 | Cell 2-4 |
| Cell 3-1 | Cell 3-2 | Cell 3-3 | Cell 3-4 |
| Cell 4-1 | Cell 4-2 | Cell 4-3 | Cell 4-4 |
| Cell 5-1 | Cell 5-2 | Cell 5-3 | Cell 5-4 |
| Cell 6-1 | Cell 6-2 | Cell 6-3 | Cell 6-4 |
## Details
<details>
<summary>Nullam nec posuere lorem.</summary>
Aenean tempor, orci eget ullamcorper luctus, nisl turpis pharetra mauris, sit amet tristique elit orci et sem. Aenean odio purus, suscipit quis accumsan in, blandit at ex.
</details>

View File

@@ -0,0 +1,10 @@
---
import '../styles/global.css';
import { ShowLogin } from '@/modules/demo/ShowLogin';
const url = new URL(Astro.request.url);
---
<h1 class='text-4xl'>Registry</h1>
<ShowLogin client:only="react" />

View File

@@ -0,0 +1,11 @@
---
// layout: '@/registry/astro/layouts/mdx/main.astro'
import Main from '@/registry/astro/layouts/mdx/main.astro';
const baseurl = 'http://localhost:4321/r';
---
<Main>
# Download shadcn ```sh pnpm dlx shadcn@latest add ${baseurl}/basename.json ```
<p>这是默认插槽的内容</p>
</Main>

View File

@@ -0,0 +1,119 @@
@import 'tailwindcss';
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,22 @@
{
"extends": "@kevisual/types/json/frontend.json",
"compilerOptions": {
"baseUrl": ".",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@kevisual"
],
"paths": {
"@/*": [
"src/*"
],
"@/registry/*": [
"registry/*"
]
},
},
"include": [
"src/**/*",
"registry/**/*"
],
}

View File

@@ -8,5 +8,10 @@
},
"keywords": [],
"author": "",
"license": "ISC"
"license": "ISC",
"packageManager": "pnpm@10.10.0",
"devDependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
}
}

64
packages/codemirror/.gitignore vendored Normal file
View File

@@ -0,0 +1,64 @@
node_modules
# mac
.DS_Store
.env*
!.env*example
dist
# build
build
logs
.turbo
pack-dist
# astro
.astro
# next
.next
# nuxt
.nuxt
# vercel
.vercel
# vuepress
.vuepress/dist
# coverage
coverage/
# typescript
*.tsbuildinfo
# debug logs
*.log
*.tmp
# vscode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# idea
.idea
# system
Thumbs.db
ehthumbs.db
Desktop.ini
# temp files
*.tmp
*.temp
# local development
*.local

View File

@@ -0,0 +1,2 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

View File

@@ -1,9 +1,9 @@
{
"name": "@kevisual/codemirror",
"version": "0.0.2",
"version": "0.0.12",
"description": "",
"main": "dist/editor.js",
"privite": false,
"private": false,
"scripts": {
"build": "rimraf -rf dist && rollup -c"
},
@@ -16,19 +16,47 @@
],
"author": "abearxiong",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"codemirror": "^6.0.1",
"@codemirror/lang-javascript": "^6.2.2",
"dependencies": {
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.3",
"@codemirror/lang-json": "^6.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"rollup": "^4.22.2",
"tslib": "^2.7.0",
"typescript": "^5.6.2"
"@codemirror/lang-markdown": "^6.3.2",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.7",
"codemirror": "^6.0.1",
"eventemitter3": "^5.0.1",
"prettier": "^3.5.3"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^12.1.2",
"rollup": "^4.40.2",
"tslib": "^2.8.1",
"typescript": "^5.8.3"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
},
"exports": {
".": {
"import": "./dist/editor.js",
"types": "./dist/editor.d.ts"
},
"./base": {
"import": "./dist/editor.base.js",
"types": "./dist/editor.base.d.ts"
},
"./json": {
"import": "./dist/editor.json.js",
"types": "./dist/editor.json.d.ts"
},
"./utils": {
"import": "./dist/editor.utils.js",
"types": "./dist/editor.utils.d.ts"
}
}
}

View File

@@ -1,6 +1,6 @@
# kevisual codemirror
```ts
import { createEditorInstance } from '@kevisual/codemirror';
const editor = createEditorInstance(ref.current!, { typescript: false });
editor.dom.style.height = '100%';
```

View File

@@ -1,14 +1,16 @@
import { nodeResolve } from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript';
const entrys = ['editor', 'editor.json'];
const entrys = ['editor', 'editor.json', 'editor.base', 'editor.utils'];
const configs = entrys.map((entry) => ({
input: `./src/${entry}.ts`, // 修改输入文件为 TypeScript 文件
output: {
file: `./dist/${entry}.js`,
// file: `./dist/${entry}.js`,
dir: './dist',
},
target: 'browser',
plugins: [
nodeResolve(),
// nodeResolve(),
typescript({
tsconfig: './tsconfig.json',
compilerOptions: {

View File

@@ -0,0 +1,92 @@
import { basicSetup } from 'codemirror';
import { EditorView } from '@codemirror/view';
import { StateEffect } from '@codemirror/state';
import { ViewUpdate } from '@codemirror/view';
import { formatKeymap } from './extensions/tab';
import EventEmitter from 'eventemitter3';
export type CodeEditor = EditorView & {
emitter?: EventEmitter;
};
let editor: CodeEditor = null;
export type EditorOptions = {
extensions?: any[];
hasBasicSetup?: boolean;
onChange?: (content: string) => void;
};
/**
* 创建单例
* @param el
* @returns
*/
const createEditorInstance = (el?: HTMLDivElement, opts?: EditorOptions) => {
if (editor && el) {
el.appendChild(editor.dom);
return editor;
} else if (editor) {
return editor;
}
const extensions = opts?.extensions || [];
extensions.push(formatKeymap);
const hasBaseicSetup = opts?.hasBasicSetup ?? true;
if (hasBaseicSetup) {
extensions.unshift(basicSetup);
}
const emitter = new EventEmitter();
createOnChangeListener(emitter, opts.onChange).appendTo(extensions);
editor = new EditorView({
extensions: extensions,
parent: el || document.body,
});
editor.emitter = emitter;
editor.dom.style.height = '100%';
return editor as CodeEditor;
};
/**
* 每次都创建新的实例
* @param el
* @returns
*/
export const createEditor = (el: HTMLDivElement, opts?: EditorOptions) => {
const extensions = opts?.extensions || [];
const hasBaseicSetup = opts?.hasBasicSetup ?? true;
if (hasBaseicSetup) {
extensions.unshift(basicSetup);
}
extensions.push(formatKeymap);
const emitter = new EventEmitter();
createOnChangeListener(emitter, opts.onChange).appendTo(extensions);
const editor = new EditorView({
extensions,
parent: el || document.body,
}) as CodeEditor;
editor.emitter = emitter;
editor.dom.style.height = '100%';
return editor as CodeEditor;
};
export const getEditor = () => editor;
export { editor, createEditorInstance };
export const createOnChangeListener = (emitter: EventEmitter, callback?: (content: string) => void) => {
const listener = EditorView.updateListener.of((update: ViewUpdate) => {
if (update.docChanged) {
const editor = update.view;
if (callback) {
callback(editor.state.doc.toString());
}
// 触发自定义事件
emitter.emit('change', editor.state.doc.toString());
}
});
// 返回监听器配置,而不是直接应用它
return {
extension: listener,
appendTo: (ext: any[]) => {
ext.push(listener);
},
};
};

View File

@@ -1,26 +1,13 @@
import { EditorView, basicSetup } from 'codemirror';
import { json } from '@codemirror/lang-json';
let editor: EditorView = null;
import * as Base from './editor.base';
/**
* 创建单例
* @param el
* @returns
*/
const createEditorInstance = (el?: HTMLDivElement) => {
if (editor && el) {
el.appendChild(editor.dom);
return editor;
} else if (editor) {
return editor;
}
editor = new EditorView({
extensions: [basicSetup, json()],
parent: el || document.body,
});
editor.dom.style.height = '100%';
return editor;
export const createEditorInstance = (el?: HTMLDivElement) => {
return Base.createEditorInstance(el, { extensions: [json()] });
};
/**
@@ -29,12 +16,9 @@ const createEditorInstance = (el?: HTMLDivElement) => {
* @returns
*/
export const createEditor = (el: HTMLDivElement) => {
const editor = new EditorView({
extensions: [basicSetup, json()],
parent: el || document.body,
});
editor.dom.style.height = '100%';
return editor;
return Base.createEditor(el, { extensions: [json()] });
};
export { editor, createEditorInstance };
export { Base };
export const editor = Base.editor;

View File

@@ -1,11 +1,27 @@
import { EditorView, basicSetup } from 'codemirror';
import { basicSetup } from 'codemirror';
import { EditorView } from '@codemirror/view';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { html } from '@codemirror/lang-html';
import { markdown } from '@codemirror/lang-markdown';
import { css } from '@codemirror/lang-css';
import { formatKeymap } from './extensions/tab';
import { createOnChangeListener } from './editor.base';
import EventEmitter from 'eventemitter3';
let editor: EditorView = null;
export type CodeEditor = EditorView & {
emitter?: EventEmitter;
};
let editor: CodeEditor = null;
type CreateOpts = {
jsx?: boolean;
typescript?: boolean;
type?: 'javascript' | 'json' | 'html' | 'markdown' | 'css';
hasBasicSetup?: boolean;
extensions?: any[];
hasKeymap?: boolean;
onChange?: (content: string) => void;
};
/**
* 创建单例
@@ -19,13 +35,42 @@ const createEditorInstance = (el?: HTMLDivElement, opts?: CreateOpts) => {
} else if (editor) {
return editor;
}
const { jsx, typescript } = opts || {};
const { type = 'javascript' } = opts || {};
const extensions = opts?.extensions || [];
const hasBaseicSetup = opts?.hasBasicSetup ?? true;
const hasKeymap = opts?.hasKeymap ?? true;
if (hasBaseicSetup) {
extensions.unshift(basicSetup);
}
if (hasKeymap) {
extensions.push(formatKeymap);
}
switch (type) {
case 'json':
extensions.push(json());
break;
case 'javascript':
extensions.push(javascript({ jsx: opts?.jsx, typescript: opts?.typescript }));
break;
case 'css':
extensions.push(css());
break;
case 'html':
extensions.push(html());
break;
case 'markdown':
extensions.push(markdown());
break;
}
const emitter = new EventEmitter();
createOnChangeListener(emitter, opts.onChange).appendTo(extensions);
editor = new EditorView({
extensions: [basicSetup, javascript({ jsx, typescript })],
extensions: extensions,
parent: el || document.body,
});
editor.dom.style.height = '100%';
return editor;
editor.emitter = emitter;
return editor as CodeEditor;
};
/**
@@ -34,12 +79,42 @@ const createEditorInstance = (el?: HTMLDivElement, opts?: CreateOpts) => {
* @returns
*/
export const createEditor = (el: HTMLDivElement, opts?: CreateOpts) => {
const { type = 'javascript' } = opts || {};
const extensions = opts?.extensions || [];
const hasBaseicSetup = opts?.hasBasicSetup ?? true;
const hasKeymap = opts?.hasKeymap ?? true;
if (hasBaseicSetup) {
extensions.unshift(basicSetup);
}
if (hasKeymap) {
extensions.push(formatKeymap);
}
switch (type) {
case 'json':
extensions.push(json());
break;
case 'javascript':
extensions.push(javascript({ jsx: opts?.jsx, typescript: opts?.typescript }));
break;
case 'css':
extensions.push(css());
break;
case 'html':
extensions.push(html());
break;
case 'markdown':
extensions.push(markdown());
break;
}
const emitter = new EventEmitter();
createOnChangeListener(emitter, opts.onChange).appendTo(extensions);
const editor = new EditorView({
extensions: [basicSetup, javascript({ jsx: opts?.jsx, typescript: opts?.typescript })],
extensions: extensions,
parent: el || document.body,
});
}) as CodeEditor;
editor.dom.style.height = '100%';
return editor;
editor.emitter = emitter;
return editor as CodeEditor;
};
export { editor, createEditorInstance };

View File

@@ -0,0 +1,59 @@
import { EditorView } from '@codemirror/view';
import { CodeEditor } from './editor.base';
type ChainOpts = {
editor?: CodeEditor;
};
export class Chain {
editor: CodeEditor | EditorView;
constructor(opts?: ChainOpts) {
this.editor = opts?.editor;
}
getEditor() {
return this.editor;
}
getContent() {
return this.editor?.state.doc.toString() || '';
}
setContent(content: string) {
if (this.editor) {
this.editor.dispatch({
changes: { from: 0, to: this.editor.state.doc.length, insert: content },
});
}
return this;
}
setEditor(editor: EditorView) {
this.editor = editor;
return this;
}
clearEditor() {
if (this.editor) {
this.editor.dispatch({
changes: { from: 0, to: this.editor.state.doc.length, insert: '' },
});
}
return this;
}
destroy() {
if (this.editor) {
this.editor.destroy();
this.editor = null;
}
return this;
}
static create(opts?: ChainOpts) {
return new Chain(opts);
}
setOnChange(callback: (content: string) => void) {
if (this.editor) {
const editor = this.editor as CodeEditor;
if (editor.emitter) {
editor.emitter.on('change', callback);
return () => {
editor.emitter.off('change', callback);
};
}
}
return () => {};
}
}

View File

@@ -0,0 +1,56 @@
import { EditorView, keymap } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { defaultKeymap, indentWithTab, insertTab } from '@codemirror/commands';
import prettier from 'prettier';
// import parserBabel from 'prettier/plugins/babel';
import parserEstree from 'prettier/plugins/estree';
// import parserHtml from 'prettier/plugins/html';
import parserTypescript from 'prettier/plugins/typescript';
// 格式化函数
// Function to format the code using Prettier
type FormatCodeOptions = {
type: 'typescript';
plugins?: any[];
};
async function formatCode(view: EditorView, opts?: FormatCodeOptions) {
const editor = view;
const code = editor.state.doc.toString();
const plugins = opts?.plugins || [];
plugins.push(parserEstree);
const parser = opts?.type || 'typescript';
if (parser === 'typescript') {
plugins.push(parserTypescript);
}
try {
const formattedCode = await prettier.format(code, {
parser: parser,
plugins: plugins,
});
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: formattedCode.trim(),
},
});
} catch (error) {
console.error('Error formatting code:', error);
}
}
export const formatKeymap = keymap.of([
{
// bug, 必须小写
key: 'alt-shift-f', // 快捷键绑定
// mac: 'cmd-shift-f',
run: (view) => {
formatCode(view);
return true; // 表示按键事件被处理
},
},
// indentWithTab, // Tab键自动缩进
{ key: 'Tab', run: insertTab }, // 在光标位置插入Tab字符
...defaultKeymap, // 默认快捷键
]);

View File

@@ -4,7 +4,7 @@ import { Fragment, useEffect, useMemo, useState } from 'react';
import { useDemoStore } from '../store';
import { useShallow } from 'zustand/react/shallow';
import { Form } from 'antd';
import { useNavigate } from 'react-router';
import { useNewNavigate } from '@/modules';
import { EditOutlined, SettingOutlined, LinkOutlined, SaveOutlined, DeleteOutlined, LeftOutlined } from '@ant-design/icons';
import clsx from 'clsx';
import { isObjectNull } from '@/utils/is-null';
@@ -84,7 +84,7 @@ const FormModal = () => {
);
};
export const List = () => {
const navicate = useNavigate();
const navicate = useNewNavigate();
const demoStore = useDemoStore(
useShallow((state) => {
return {
@@ -161,7 +161,13 @@ export const List = () => {
icon={<LinkOutlined />}></Button>
<Button
onClick={(e) => {
Modal.confirm({
title: 'Delete',
content: 'Are you sure delete this data?',
onOk: () => {
demoStore.deleteData(item.id);
},
});
e.stopPropagation();
}}
icon={<DeleteOutlined />}></Button>

View File

@@ -34,7 +34,7 @@ export const useDemoStore = create<DemoStore>((set, get) => {
if (res.code === 200) {
set({ list: res.data });
} else {
message.error(res.msg || 'Request failed');
message.error(res.message || 'Request failed');
}
},
updateData: async (data) => {
@@ -49,7 +49,7 @@ export const useDemoStore = create<DemoStore>((set, get) => {
set({ showEdit: false, formData: [] });
getList();
} else {
message.error(res.msg || 'Request failed');
message.error(res.message || 'Request failed');
}
},
deleteData: async (id) => {
@@ -63,7 +63,7 @@ export const useDemoStore = create<DemoStore>((set, get) => {
getList();
message.success('Success');
} else {
message.error(res.msg || 'Request failed');
message.error(res.message || 'Request failed');
}
},
};

2
packages/tailwind/.npmrc Normal file
View File

@@ -0,0 +1,2 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

View File

@@ -0,0 +1,40 @@
@import 'tailwindcss';
@utility {
.btn {
@apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}
.card {
@apply bg-white shadow-md rounded-lg p-4;
.card-title {
@apply text-lg font-bold;
}
.card-subtitle {
@apply text-sm text-gray-500;
}
.card-description {
@apply text-gray-700 break-words;
}
.card-code {
@apply bg-gray-100 p-2;
}
.card-body {
@apply text-gray-700;
}
.card-key {
@apply text-xs text-gray-400;
}
.card-footer {
@apply text-sm text-gray-500;
}
}
}
@utilities {
.layout-menu {
@apply bg-gray-900 p-2 text-white flex justify-between h-12;
}
.bg-custom-blue {
background-color: #3490dc;
}
}

View File

@@ -0,0 +1,87 @@
@import 'tailwindcss';
@theme {
--color-primary: #ffc107;
--color-secondary: #ffa000;
--color-text-primary: #000000;
--color-text-secondary: #000000;
--color-success: #28a745;
--color-scrollbar-thumb: #999999;
--color-scrollbar-track: rgba(0, 0, 0, 0.1);
--color-scrollbar-thumb-hover: #666666;
--scrollbar-color: #ffc107; /* 滚动条颜色 */
}
html,
body {
width: 100%;
height: 100%;
font-size: 16px;
font-family: 'Montserrat', sans-serif;
h1 {
@apply text-2xl font-bold;
}
h2 {
@apply text-xl font-bold;
}
h3 {
@apply text-lg font-bold;
}
}
/* font-family */
@utility font-family-mon {
font-family: 'Montserrat', sans-serif;
}
@utility font-family-rob {
font-family: 'Roboto', sans-serif;
}
@utility font-family-int {
font-family: 'Inter', sans-serif;
}
@utility font-family-orb {
font-family: 'Orbitron', sans-serif;
}
@utility font-family-din {
font-family: 'DIN', sans-serif;
}
@utility flex-row-center {
@apply flex flex-row items-center justify-center;
}
@utility flex-col-center {
@apply flex flex-col items-center justify-center;
}
@utility scrollbar {
overflow: auto;
/* 整个滚动条 */
&::-webkit-scrollbar {
width: 3px;
height: 3px;
}
&::-webkit-scrollbar-track {
background-color: var(--color-scrollbar-track);
}
/* 滚动条有滑块的轨道部分 */
&::-webkit-scrollbar-track-piece {
background-color: transparent;
border-radius: 1px;
}
/* 滚动条滑块(竖向:vertical 横向:horizontal) */
&::-webkit-scrollbar-thumb {
cursor: pointer;
background-color: var(--color-scrollbar-thumb);
border-radius: 5px;
}
/* 滚动条滑块hover */
&::-webkit-scrollbar-thumb:hover {
background-color: var(--color-scrollbar-thumb-hover);
}
/* 同时有垂直和水平滚动条时交汇的部分 */
&::-webkit-scrollbar-corner {
display: block; /* 修复交汇时出现的白块 */
}
}

View File

@@ -0,0 +1,14 @@
@import 'tailwindcss';
@utility {
.loading {
@apply w-full h-full flex justify-center items-center;
> div {
@apply w-20 h-20 border-t-8 border-b-8 rounded-full animate-spin;
}
}
.loading-sm {
@apply w-4 h-4 border-t-2 border-b-2 rounded-full animate-spin;
}
}

View File

@@ -0,0 +1,31 @@
@import 'tailwindcss';
@utility .scrollbar {
/* 整个滚动条 */
&::-webkit-scrollbar {
width: 3px;
height: 3px;
}
/* 滚动条有滑块的轨道部分 */
&::-webkit-scrollbar-track-piece {
background-color: transparent;
border-radius: 1px;
}
/* 滚动条滑块(竖向:vertical 横向:horizontal) */
&::-webkit-scrollbar-thumb {
cursor: pointer;
background-color: black;
border-radius: 5px;
}
/* 滚动条滑块hover */
&::-webkit-scrollbar-thumb:hover {
background-color: #999999;
}
/* 同时有垂直和水平滚动条时交汇的部分 */
&::-webkit-scrollbar-corner {
display: block; /* 修复交汇时出现的白块 */
}
}

View File

@@ -0,0 +1,9 @@
export const extend = {
fontFamily: {
mon: ['Montserrat', 'sans-serif'], // 定义自定义字体族
rob: ['Roboto', 'sans-serif'],
int: ['Inter', 'sans-serif'],
orb: ['Orbitron', 'sans-serif'],
din: ['DIN', 'sans-serif'],
},
};

View File

@@ -0,0 +1,3 @@
@import "./css/globals.css";
@import "./css/loading.css";
/* @import "./css/scrollbar.css" */

View File

@@ -1,15 +1,28 @@
{
"name": "@kevisual/tailwind",
"version": "1.0.0",
"version": "1.0.3",
"description": "",
"main": "index.js",
"main": "plugin/index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"files": [
"plugins"
"plugins",
"css",
"extends",
"tailwind.config.js",
"src",
"index.css",
"dist"
],
"keywords": [],
"author": "",
"license": "ISC"
"author": "abearxiong",
"license": "ISC",
"exports": {
".": "./plugins/index.js",
"./main.css": "./index.css",
"./css": "./css/globals.css",
"./loading": "./css/loading.css"
}
}

View File

@@ -20,6 +20,7 @@ const flexCenter = plugin(function ({ addUtilities }) {
'.card-body': {},
'.card-footer': {},
'.card-key': {},
'.loading': {},
});
});

View File

@@ -0,0 +1,51 @@
# tailwind 收集模块
```mjs
import path from 'path';
const root = path.resolve(process.cwd());
const contents = ['./src/**/*.{ts,tsx,html}', './src/**/*.css'];
const content = contents.map((item) => path.join(root, item));
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ['class'],
content: content,
plugins: [
require('@tailwindcss/aspect-ratio'), //
require('@tailwindcss/typography'),
require('tailwindcss-animate'),
require('./plugins/index'),
],
theme: {
extend: {
fontFamily: {
mon: ['Montserrat', 'sans-serif'], // 定义自定义字体族
rob: ['Roboto', 'sans-serif'],
int: ['Inter', 'sans-serif'],
orb: ['Orbitron', 'sans-serif'],
din: ['DIN', 'sans-serif'],
},
},
screen: {
sm: '640px',
// => @media (min-width: 640px) { ... }
md: '768px',
// => @media (min-width: 768px) { ... }
lg: '1024px',
// => @media (min-width: 1024px) { ... }
xl: '1280px',
// => @media (min-width: 1280px) { ... }
'2xl': '1536px',
// => @media (min-width: 1536px) { ... }
'3xl': '1920px',
// => @media (min-width: 1920) { ... }
'4xl': '2560px',
// => @media (min-width: 2560) { ... }
},
},
};
```

View File

@@ -0,0 +1,47 @@
import path from 'path';
const root = path.resolve(process.cwd());
const contents = ['./src/**/*.{ts,tsx,html}', './src/**/*.css']
const content = contents.map((item) => path.join(root, item));
/** @type {import('tailwindcss').Config} */
export default {
darkMode: ['class'],
content: content,
plugins: [
require('@tailwindcss/aspect-ratio'), //
require('@tailwindcss/typography'),
require('tailwindcss-animate'),
require('./plugins/index'),
],
theme: {
extend: {
fontFamily: {
mon: ['Montserrat', 'sans-serif'], // 定义自定义字体族
rob: ['Roboto', 'sans-serif'],
int: ['Inter', 'sans-serif'],
orb: ['Orbitron', 'sans-serif'],
din: ['DIN', 'sans-serif'],
},
},
screen: {
sm: '640px',
// => @media (min-width: 640px) { ... }
md: '768px',
// => @media (min-width: 768px) { ... }
lg: '1024px',
// => @media (min-width: 1024px) { ... }
xl: '1280px',
// => @media (min-width: 1280px) { ... }
'2xl': '1536px',
// => @media (min-width: 1536px) { ... }
'3xl': '1920px',
// => @media (min-width: 1920) { ... }
'4xl': '2560px',
// => @media (min-width: 2560) { ... }
},
},
};

View File

@@ -15,39 +15,39 @@
"author": "",
"license": "ISC",
"dependencies": {
"@abearxiong/ui": "0.0.1-alpha.0",
"@kevisual/codemirror": "workspace:^",
"@kevisual/ui": "workspace:^",
"antd": "^5.20.6",
"antd": "^5.25.1",
"clsx": "^2.1.1",
"immer": "^10.1.1",
"lodash-es": "^4.17.21",
"nanoid": "^5.0.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router": "^6.26.2",
"react-router-dom": "^6.26.2",
"react-toastify": "^10.0.5",
"zustand": "^4.5.5"
"nanoid": "^5.1.5",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.6.0",
"react-router-dom": "^7.6.0",
"react-toastify": "^11.0.5",
"zustand": "^5.0.4"
},
"devDependencies": {
"@eslint/js": "^9.10.0",
"@eslint/js": "^9.26.0",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.5.5",
"@types/react": "^18.3.8",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"eslint": "^9.10.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.12",
"globals": "^15.9.0",
"tailwind-merge": "^2.5.2",
"tailwindcss": "^3.4.12",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.6",
"@types/node": "^22.15.17",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"@vitejs/plugin-react": "^4.4.1",
"autoprefixer": "^10.4.21",
"eslint": "^9.26.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.1.0",
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.6",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.6.2",
"typescript-eslint": "^8.6.0",
"vite": "^5.4.6"
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.0",
"vite": "^6.3.5"
}
}

View File

@@ -13,8 +13,8 @@ export const App = () => {
}}>
<Router>
<Routes>
<Route path='/' element={<Navigate to='/model/' />} />
<Route path='/modal/*' element={<FlowApps />} />
<Route path='/' element={<Navigate to='/modal/' />} />
{/* <Route path='/modal/*' element={<FlowApps />} /> */}
<Route path='/codemirror/*' element={<CodeMirrorApp />} />
<Route path='/404' element={<div>404</div>} />
<Route path='*' element={<div>404</div>} />

View File

@@ -1,15 +1 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
h1 {
@apply text-2xl font-bold;
}
h2 {
@apply text-xl font-bold;
}
h3 {
@apply text-lg font-bold;
}
}
@import 'tailwindcss';

View File

@@ -1,6 +1,8 @@
import { createRoot } from 'react-dom/client';
import { App } from './App.tsx';
// import './tailwind.css';
const App2 = () => {
return <div>hello</div>;
};
import './globals.css';
import './index.css';

View File

@@ -1,4 +1,4 @@
import { createEditorInstance } from '@kevisual/codemirror/dist/editor.json';
import { createEditor } from '@kevisual/codemirror/json';
import { useEffect, useRef } from 'react';
export const App = () => {
const ref = useRef<HTMLDivElement>(null);
@@ -6,7 +6,7 @@ export const App = () => {
init();
}, []);
const init = () => {
const editor = createEditorInstance(ref.current!);
const editor = createEditor(ref.current!);
editor.dom.style.height = '100%';
};
return (

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