Initial commit: Re-connected to Gitea

This commit is contained in:
2026-03-22 04:39:45 +08:00
commit 8e76dd7a7b
103 changed files with 7132 additions and 0 deletions

51
packages/ui/package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "@defgov/ui",
"version": "0.0.0",
"private": true,
"type": "module",
"sideEffects": [
"*.css"
],
"main": "./dist/index.es.js",
"module": "./dist/index.es.js",
"types": "./dist/index.d.ts",
"style": "./dist/index.css",
"exports": {
".": {
"import": "./dist/index.es.js",
"types": "./dist/index.d.ts"
},
"./index.css": "./dist/index.css"
},
"files": [
"dist"
],
"scripts": {
"dev": "pnpm gen-index && tsc && vite build -w",
"build": "pnpm gen-index && tsc && vite build",
"gen-index": "ts-node scripts/generate-index.ts && ts-node scripts/generate-index-css.ts"
},
"devDependencies": {
"@tsconfig/vite-react": "^7.0.2",
"@types/node": "^25.1.0",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"autoprefixer": "^10.4.24",
"glob": "^13.0.3",
"prettier": "^3.8.1",
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-dts": "^4.5.4"
},
"dependencies": {
"@floating-ui/react": "^0.27.18",
"@tailwindcss/vite": "^4.2.1",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"tailwind-merge": "^3.5.0",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.2.1"
}
}

View File

@@ -0,0 +1,157 @@
// scripts/generate-index-use.ts
import { writeFileSync, readFileSync } from "fs";
import { extname, relative, resolve, basename } from "path";
import { globSync } from "glob";
interface Config {
targetDirs: string[];
includeExtensions: string[];
importSyntax: "@import";
importSyntaxTail?: string;
excludeFilePattern: RegExp[];
excludeDirs: string[];
warnDuplicateTailwindImport: boolean;
indexFileName: string;
}
// 配置项
const srcDirConfig: Config = {
targetDirs: ["src"], // 包含的文件夹
includeExtensions: [".css"], // 目标文件后缀
importSyntax: "@import", // 导入时使用的语法
excludeFilePattern: [/index\.css/, /index\.scss/, /\.(test|spec)\./], // 正则若匹配则排除
// 排除的文件夹
excludeDirs: [
"__tests__",
"tests",
"story",
"stories",
"types",
"node_modules",
"dist",
"build",
],
warnDuplicateTailwindImport: true,
indexFileName: "index.css", // 生成的入口文件名称
};
// ======================================================================
/**
* 判断是否是有效 CSS 文件
* @param filePath 文件绝对路径
* @returns 是否为有效文件
*/
function isValidFile(filePath: string, config: Config): boolean {
// 过滤排除的文件
const filenameWithExt = filePath.split(/[\\/]/).pop()!; // “\\” 匹配反斜杠 “\”,“/” 匹配正斜杠 “/”pop 返回最后一个 “/” 后面的内容
const shouldExcludeFile = config.excludeFilePattern.some((pattern) =>
pattern.test(filenameWithExt),
);
if (shouldExcludeFile) {
return false; // 如果匹配到排除规则,跳过当前文件
}
// 过滤排除的文件夹
const normalizedFilePath = filePath.replace(/\\/g, "/"); // “\\” 匹配反斜杠 “\”,“/” 匹配正斜杠 “/”
const shouldExcludeDir = config.excludeDirs.some(
(dir) => normalizedFilePath.includes(`/${dir}/`), // 前后都有“/” 是为了避免误匹配到文件名(如 test.js 不会被排除)
);
if (shouldExcludeDir) return false; // 如果匹配到排除规则,跳过当前文件
// 过滤非目标扩展名,只有指定的才通过
const ext = extname(filePath);
const isValidExt = config.includeExtensions.includes(ext);
if (!isValidExt) {
return false; // 如果不是指定扩展名,跳过当前文件
}
// @import "tailwindcss" 重复警告,生成的入口文件会自动加一条,其他文件无需重复导入
if (config.warnDuplicateTailwindImport) {
try {
const fileContent = readFileSync(filePath, "utf-8");
if (
fileContent.includes('@import "tailwindcss"') ||
fileContent.includes("@import 'tailwindcss'")
) {
console.warn(
`${filePath} 中含有重复的 @import "tailwindcss" 导入,应手动删除`,
);
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
console.warn(`⚠️ 读取文件内容失败(跳过过滤): ${filePath}`, errorMsg);
}
}
return true;
}
// ======================================================================
/**
* 生成 “@import” 导入语句
*/
function generateIndexFile(config: Config) {
const [targetDir] = config.targetDirs;
const dirPath = resolve(process.cwd(), targetDir); //目标文件夹绝对路径, 通常是 /project/src
const searchPattern = resolve(dirPath, "**", "*.*"); // 搜索路径 “dirPath/**/*”
const allFiles = globSync(searchPattern, {
nodir: true, // 不匹配文件夹,只匹配文件
absolute: true, // 返回从根目录开始的绝对路径
windowsPathsNoEscape: true, // 禁用 Windows 路径的反斜杠转义
dot: false, // 不配隐藏文件或目录(以 . 开头的文件)
follow: true, // 跟踪符号链接symlinks继续解析并返回链接的目标文件或目录
});
const validFiles = allFiles.filter((value) => isValidFile(value, config)); // 过滤出要导入的有效 CSS 文件
console.log(`✅ 有效 CSS 文件数量: ${validFiles.length}`);
validFiles.sort(); // 排序
const importStatements = validFiles.map((file) => {
const relPath = relative(dirPath, file); // 算出 src 与 css文件 的相对路径
let importPath = "./" + relPath.replace(/\\/g, "/"); // 给相对路径前面加上 “./”,并将 “\\” 替换成 “/”
// 拼接 @import 语句
if (config.importSyntaxTail !== undefined) {
return `${config.importSyntax} '${importPath}' ${config.importSyntaxTail};`;
} else {
return `${config.importSyntax} '${importPath}';`;
}
});
// index.css 文件的顶部内容
const indexContent = `
@import "tailwindcss";
${importStatements.join("\n")}
`.trim(); // 使用 trim 移除开头多余的换行
const indexFilePath = resolve(dirPath, config.indexFileName);
// 创建和写入 index.css 文件
writeFileSync(indexFilePath, indexContent, "utf-8");
console.log(`✅ 成功生成 ${config.indexFileName}: ${indexFilePath}`);
}
// ======================================================================
/**
* 主函数
*/
function main() {
try {
generateIndexFile(srcDirConfig);
} catch (e) {
const errorMsg = e instanceof Error ? e.message : String(e);
console.error(
`❌ 文件夹 ${srcDirConfig.targetDirs} 扫描和生成 ${srcDirConfig.indexFileName} 失败:`,
errorMsg,
);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,148 @@
// scripts/generate-index.ts
import { writeFileSync } from "fs";
import { extname, relative, resolve } from "path";
import { globSync } from "glob";
interface Config {
targetDirs: string[];
includeExtensions: string[];
excludeKeywords: {
dirs: string[];
fileSuffixes: string[];
filePatterns: RegExp[];
};
}
const CONFIG: Config = {
targetDirs: ["src"],
includeExtensions: [".ts", ".tsx", ".vue"],
excludeKeywords: {
dirs: ["__tests__", "tests", "story", "stories", "types"],
fileSuffixes: [".d.ts"],
filePatterns: [
/^index\.(ts|tsx|js|jsx)$/, // index文件
/\.(test|spec)\./, // 测试文件 .test.xxx / .spec.xxx
/\.(story|stories)\./, // Storybook文件 .story.xxx / .stories.xxx
],
},
};
/**
* 统一路径分隔符(兼容 Windows/Linux
* @param path 原始路径
* @returns 标准化路径(全部转为 /
*/
const normalizePath = (path: string): string => path.replace(/\\/g, "/");
/**
* 检查文件是否在排除目录中
* @param filePath 文件路径
* @returns 是否在排除目录
*/
const isInExcludeDir = (filePath: string): boolean => {
const normalizedPath = normalizePath(filePath);
return CONFIG.excludeKeywords.dirs.some((dir) =>
normalizedPath.includes(`/${dir}/`),
);
};
/**
* 检查文件是否匹配排除正则
* @param fileName 文件名
* @returns 是否匹配排除规则
*/
const isMatchExcludePattern = (fileName: string): boolean => {
return CONFIG.excludeKeywords.filePatterns.some((pattern) =>
pattern.test(fileName),
);
};
/**
* 检查文件是否为排除后缀(如 .d.ts
* @param filePath 文件路径
* @returns 是否为排除后缀
*/
const isExcludeSuffix = (filePath: string): boolean => {
return CONFIG.excludeKeywords.fileSuffixes.some((suffix) =>
filePath.endsWith(suffix),
);
};
function isValidFile(filePath: string): boolean {
const fileName = filePath.split(/[\\/]/).pop()!;
// 按优先级过滤:目录 > 后缀 > 文件名规则 > 扩展名
if (isInExcludeDir(filePath)) return false;
if (isExcludeSuffix(filePath)) return false;
if (isMatchExcludePattern(fileName)) return false;
const ext = extname(filePath);
const isValidExt = CONFIG.includeExtensions.includes(ext);
return isValidExt;
}
// ========== 5. 生成索引文件(错误处理 + 语法规范) ==========
function generateIndexFile(dirPath: string): void {
const searchPattern = resolve(dirPath, "**", "*.*");
const allFiles = globSync(searchPattern, {
nodir: true,
absolute: true,
windowsPathsNoEscape: true,
dot: false,
follow: true,
});
if (allFiles.length === 0) {
return;
}
const validFiles = allFiles.filter(isValidFile);
if (validFiles.length === 0) {
return;
}
validFiles.sort();
// 生成导出语句
const exportStatements = validFiles.map((file) => {
const relPath = relative(dirPath, file);
const importPath = `./${relPath.replace(/\.[^.]+$/, "").replace(/\\/g, "/")}`;
return `export * from '${importPath}';`;
});
// 生成索引文件内容(模板字符串格式化)
const indexContent = `
import './index.scss';
${exportStatements.join("\n")}
`;
const indexFilePath = resolve(dirPath, "index.ts");
writeFileSync(indexFilePath, indexContent, "utf-8");
}
// ========== 6. 主函数(完善错误处理 + 类型安全) ==========
function main(): void {
const [targetDir] = CONFIG.targetDirs;
if (!targetDir) {
console.error(`❌ 未配置目标扫描目录,请检查 CONFIG.targetDirs`);
process.exit(1);
}
const absTargetDir = resolve(process.cwd(), targetDir);
try {
generateIndexFile(absTargetDir);
} catch (error) {
// 处理 unknown 类型错误TypeScript 最佳实践)
const errorMsg = error instanceof Error ? error.message : String(error);
console.error(`❌ 生成 index.ts 失败: ${errorMsg}`);
process.exit(1);
}
}
// 执行主函数
main();

View File

@@ -0,0 +1,16 @@
export const CheckIndicatorOutlineSvg = (
props: React.SVGProps<SVGSVGElement>,
) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M19 5v14H5V5zm0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2"
/>
</svg>
);

View File

@@ -0,0 +1,14 @@
export const CheckIndicatorSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m0 16H5V5h14zM17.99 9l-1.41-1.42l-6.59 6.59l-2.58-2.57l-1.42 1.41l4 3.99z"
/>
</svg>
);

View File

@@ -0,0 +1,19 @@
export const ChevronRightSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m9 6l6 6l-6 6"
/>
</svg>
);

View File

@@ -0,0 +1,14 @@
export const CutSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M12.14 9.342L7.37 2.329a.75.75 0 1 0-1.24.844l5.13 7.545l-2.395 3.743a4 4 0 1 0 1.178.943l2.135-3.337l2.065 3.036a4 4 0 1 0 1.261-.813l-2.447-3.597l.002-.002zM4.5 18a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0m10 0a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0m-.562-8.684l3.943-6.162a.75.75 0 1 0-1.263-.808L13.02 7.968z"
/>
</svg>
);

View File

@@ -0,0 +1,14 @@
export const DownloadSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M18 15v3H6v-3H4v3c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-3zm-1-4l-1.41-1.41L13 12.17V4h-2v8.17L8.41 9.59L7 11l5 5z"
/>
</svg>
);

View File

@@ -0,0 +1,20 @@
export const FileSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<g
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M14 3v4a1 1 0 0 0 1 1h4" />
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2" />
</g>
</svg>
);

View File

@@ -0,0 +1,14 @@
export const KeySvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M7 14q-.825 0-1.412-.587T5 12t.588-1.412T7 10t1.413.588T9 12t-.587 1.413T7 14m0 4q-2.5 0-4.25-1.75T1 12t1.75-4.25T7 6q1.675 0 3.038.825T12.2 9h8.375q.2 0 .388.075t.337.225l2 2q.15.15.212.325t.063.375t-.063.375t-.212.325l-3.175 3.175q-.125.125-.3.2t-.35.1t-.35-.025t-.325-.175L17.5 15l-1.425 1.075q-.125.1-.275.15t-.3.05t-.313-.05t-.287-.15L13.375 15H12.2q-.8 1.35-2.163 2.175T7 18m0-2q1.4 0 2.463-.85T10.875 13H14l1.45 1.025v.013v-.013L17.5 12.5l1.775 1.375L21.15 12h-.012h.012l-1-1v-.012V11h-9.275q-.35-1.3-1.412-2.15T7 8Q5.35 8 4.175 9.175T3 12t1.175 2.825T7 16"
/>
</svg>
);

View File

@@ -0,0 +1,18 @@
export const MoonSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 3h.393a7.5 7.5 0 0 0 7.92 12.446A9 9 0 1 1 12 2.992z"
/>
</svg>
);

View File

@@ -0,0 +1,14 @@
export const PasteSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M12.753 2c1.158 0 2.111.875 2.234 2h1.763a2.25 2.25 0 0 1 2.245 2.096L19 6.25a.75.75 0 0 1-.647.742L18.249 7a.75.75 0 0 1-.742-.647L17.5 6.25a.75.75 0 0 0-.648-.743L16.75 5.5h-2.132a2.24 2.24 0 0 1-1.865.993H9.247a2.24 2.24 0 0 1-1.865-.992L5.25 5.5a.75.75 0 0 0-.743.648L4.5 6.25v13.505c0 .38.282.693.648.743l.102.007h3a.75.75 0 0 1 .743.647l.007.102a.75.75 0 0 1-.75.75h-3a2.25 2.25 0 0 1-2.245-2.095L3 19.755V6.25a2.25 2.25 0 0 1 2.096-2.245L5.25 4h1.763a2.247 2.247 0 0 1 2.234-2zm5.997 6a2.25 2.25 0 0 1 2.245 2.096l.005.154v9.5a2.25 2.25 0 0 1-2.096 2.245L18.75 22h-6.5a2.25 2.25 0 0 1-2.245-2.096L10 19.75v-9.5a2.25 2.25 0 0 1 2.096-2.245L12.25 8zm0 1.5h-6.5a.75.75 0 0 0-.743.648l-.007.102v9.5c0 .38.282.694.648.743l.102.007h6.5a.75.75 0 0 0 .743-.648l.007-.102v-9.5a.75.75 0 0 0-.648-.743zm-5.997-6H9.247a.747.747 0 0 0 0 1.493h3.506a.747.747 0 1 0 0-1.493"
/>
</svg>
);

View File

@@ -0,0 +1,16 @@
export const RadioIndicatorOutlineSvg = (
props: React.SVGProps<SVGSVGElement>,
) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2m0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8s-3.58 8-8 8"
/>
</svg>
);

View File

@@ -0,0 +1,15 @@
export const RadioIndicatorSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10s10-4.48 10-10S17.52 2 12 2m0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8s8 3.58 8 8s-3.58 8-8 8"
/>
<circle cx="12" cy="12" r="5" fill="currentColor" />
</svg>
);

View File

@@ -0,0 +1,14 @@
export const SearchSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l5.6 5.6q.275.275.275.7t-.275.7t-.7.275t-.7-.275l-5.6-5.6q-.75.6-1.725.95T9.5 16m0-2q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"
/>
</svg>
);

View File

@@ -0,0 +1,15 @@
export const SettingSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
fill-rule="evenodd"
d="M12.563 3.2h-1.126l-.645 2.578l-.647.2a6.3 6.3 0 0 0-1.091.452l-.599.317l-2.28-1.368l-.796.797l1.368 2.28l-.317.598a6.3 6.3 0 0 0-.453 1.091l-.199.647l-2.578.645v1.126l2.578.645l.2.647q.173.568.452 1.091l.317.599l-1.368 2.28l.797.796l2.28-1.368l.598.317q.523.278 1.091.453l.647.199l.645 2.578h1.126l.645-2.578l.647-.2a6.3 6.3 0 0 0 1.091-.452l.599-.317l2.28 1.368l.796-.797l-1.368-2.28l.317-.598q.278-.523.453-1.091l.199-.647l2.578-.645v-1.126l-2.578-.645l-.2-.647a6.3 6.3 0 0 0-.452-1.091l-.317-.599l1.368-2.28l-.797-.796l-2.28 1.368l-.598-.317a6.3 6.3 0 0 0-1.091-.453l-.647-.199zm2.945 2.17l1.833-1.1a1 1 0 0 1 1.221.15l1.018 1.018a1 1 0 0 1 .15 1.221l-1.1 1.833q.33.62.54 1.3l2.073.519a1 1 0 0 1 .757.97v1.438a1 1 0 0 1-.757.97l-2.073.519q-.21.68-.54 1.3l1.1 1.833a1 1 0 0 1-.15 1.221l-1.018 1.018a1 1 0 0 1-1.221.15l-1.833-1.1q-.62.33-1.3.54l-.519 2.073a1 1 0 0 1-.97.757h-1.438a1 1 0 0 1-.97-.757l-.519-2.073a7.5 7.5 0 0 1-1.3-.54l-1.833 1.1a1 1 0 0 1-1.221-.15L4.42 18.562a1 1 0 0 1-.15-1.221l1.1-1.833a7.5 7.5 0 0 1-.54-1.3l-2.073-.519A1 1 0 0 1 2 12.72v-1.438a1 1 0 0 1 .757-.97l2.073-.519q.21-.68.54-1.3L4.27 6.66a1 1 0 0 1 .15-1.221L5.438 4.42a1 1 0 0 1 1.221-.15l1.833 1.1q.62-.33 1.3-.54l.519-2.073A1 1 0 0 1 11.28 2h1.438a1 1 0 0 1 .97.757l.519 2.073q.68.21 1.3.54zM12 14.8a2.8 2.8 0 1 0 0-5.6a2.8 2.8 0 0 0 0 5.6m0 1.2a4 4 0 1 1 0-8a4 4 0 0 1 0 8"
/>
</svg>
);

View File

@@ -0,0 +1,29 @@
export const SpinnerSvg = (props: React.SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity="0.25"
/>
<path
fill="currentColor"
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
>
<animateTransform
attributeName="transform"
dur="0.75s"
repeatCount="indefinite"
type="rotate"
values="0 12 12;360 12 12"
/>
</path>
</svg>
);
};

View File

@@ -0,0 +1,18 @@
export const SunSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 12a4 4 0 1 0 8 0a4 4 0 1 0-8 0m-5 0h1m8-9v1m8 8h1m-9 8v1M5.6 5.6l.7.7m12.1-.7l-.7.7m0 11.4l.7.7m-12.1-.7l-.7.7"
/>
</svg>
);

View File

@@ -0,0 +1,20 @@
export const UserSvg = (props: React.SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M19.618 21.25c0-3.602-4.016-6.53-7.618-6.53s-7.618 2.928-7.618 6.53M12 11.456a4.353 4.353 0 1 0 0-8.706a4.353 4.353 0 0 0 0 8.706"
/>
</svg>
);
};

View File

@@ -0,0 +1,19 @@
export const VolumeHighSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
id="a"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
d="M5.91,9h-3.29c-.21,0-.38.17-.38.38v5.25c0,.21.17.38.38.38h3.26c.17,0,.34.06.48.17l4.29,3.51c.17.12.4.09.52-.08.05-.06.07-.14.07-.22V5.63c0-.21-.17-.37-.38-.37-.08,0-.16.03-.22.07l-4.29,3.51c-.13.11-.29.17-.46.17M15,15c.46-.91.75-1.91.75-3s-.28-2.08-.75-3M17.25,17.25c.91-1.59,1.5-3,1.5-5.25s-.56-3.64-1.5-5.25M19.5,19.5c1.41-2.16,2.25-4.29,2.25-7.5s-.84-5.3-2.25-7.5"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
</svg>
);

View File

@@ -0,0 +1,45 @@
export const VolumeLowSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
id="a"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
d="M5.91,9h-3.29c-.21,0-.38.17-.38.38v5.25c0,.21.17.38.38.38h3.26c.17,0,.34.06.48.17l4.29,3.51c.17.12.4.09.52-.08.05-.06.07-.14.07-.22V5.63c0-.21-.17-.37-.38-.37-.08,0-.16.03-.22.07l-4.29,3.51c-.13.11-.29.17-.46.17"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
<path
d="M15,15c.46-.91.75-1.91.75-3s-.28-2.08-.75-3"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
<path
d="M17.25,17.25c.91-1.59,1.5-3,1.5-5.25s-.56-3.64-1.5-5.25"
fill="none"
opacity=".2"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
<path
d="M19.5,19.5c1.41-2.16,2.25-4.29,2.25-7.5s-.84-5.3-2.25-7.5"
fill="none"
opacity=".2"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
</svg>
);

View File

@@ -0,0 +1,44 @@
export const VolumeMediumSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
id="a"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
d="M5.91,9h-3.29c-.21,0-.38.17-.38.38v5.25c0,.21.17.38.38.38h3.26c.17,0,.34.06.48.17l4.29,3.51c.17.12.4.09.52-.08.05-.06.07-.14.07-.22V5.63c0-.21-.17-.37-.38-.37-.08,0-.16.03-.22.07l-4.29,3.51c-.13.11-.29.17-.46.17"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
<path
d="M15,15c.46-.91.75-1.91.75-3s-.28-2.08-.75-3"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
<path
d="M17.25,17.25c.91-1.59,1.5-3,1.5-5.25s-.56-3.64-1.5-5.25"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
<path
d="M19.5,19.5c1.41-2.16,2.25-4.29,2.25-7.5s-.84-5.3-2.25-7.5"
fill="none"
opacity=".2"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
</svg>
);

View File

@@ -0,0 +1,41 @@
export const VolumeMuteSvg = (props: React.SVGProps<SVGSVGElement>) => (
<svg
id="a"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
{...props}
>
<path
d="M5.91,9h-3.29c-.21,0-.38.17-.38.38v5.25c0,.21.17.38.38.38h3.26c.17,0,.34.06.48.17l4.29,3.51c.17.12.4.09.52-.08.05-.06.07-.14.07-.22V5.63c0-.21-.17-.37-.38-.37-.08,0-.16.03-.22.07l-4.29,3.51c-.13.11-.29.17-.46.17"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
<line
x1="14.8"
y1="8.57"
x2="21.2"
y2="15.46"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
<line
x1="14.79"
y1="15.45"
x2="21.19"
y2="8.55"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
/>
</svg>
);

View File

@@ -0,0 +1,9 @@
import type { CSSProperties } from "react";
export type CommonProps = {
className?: string;
style?: CSSProperties;
children?: React.ReactNode;
key?: string | number;
disabled?: boolean;
};

14
packages/ui/src/index.css Normal file
View File

@@ -0,0 +1,14 @@
@import "tailwindcss";
@import './styles/theme/theme-base.css';
@import './styles/theme/theme-variant.css';
@import './styles/utility/brand.css';
@import './styles/utility/gap.css';
@import './styles/utility/height-inline.css';
@import './styles/utility/height.css';
@import './styles/utility/loading.css';
@import './styles/utility/margin-right.css';
@import './styles/utility/padding.css';
@import './styles/utility/skin.css';
@import './styles/utility/variant.css';
@import './styles/utility/width-inline.css';
@import './styles/utility/width.css';

52
packages/ui/src/index.ts Normal file
View File

@@ -0,0 +1,52 @@
import './index.scss';
export * from './assets/svg/CheckIndicatorOutlineSvg.tsx';
export * from './assets/svg/CheckIndicatorSvg.tsx';
export * from './assets/svg/ChevronRightSvg.tsx';
export * from './assets/svg/CutSvg.tsx';
export * from './assets/svg/DownloadSvg.tsx';
export * from './assets/svg/FileSvg.tsx';
export * from './assets/svg/KeySvg.tsx';
export * from './assets/svg/MoonSvg.tsx';
export * from './assets/svg/PasteSvg.tsx';
export * from './assets/svg/RadioIndicatorOutlineSvg.tsx';
export * from './assets/svg/RadioIndicatorSvg.tsx';
export * from './assets/svg/SearchSvg.tsx';
export * from './assets/svg/SettingSvg.tsx';
export * from './assets/svg/SpinnerSvg.tsx';
export * from './assets/svg/SunSvg.tsx';
export * from './assets/svg/UserSvg.tsx';
export * from './assets/svg/VolumeHighSvg.tsx';
export * from './assets/svg/VolumeLowSvg.tsx';
export * from './assets/svg/VolumeMediumSvg.tsx';
export * from './assets/svg/VolumeMuteSvg.tsx';
export * from './common/CommonProps.ts';
export * from './lv1-fundamental/Box/Box.tsx';
export * from './lv1-fundamental/Slot/Slot.tsx';
export * from './lv1-fundamental/ThemeProvider/ThemeContext.ts';
export * from './lv1-fundamental/ThemeProvider/ThemeProvider.tsx';
export * from './lv1-fundamental/ThemeProvider/useThemeContext.ts';
export * from './lv2-sized/Root/RootInline.recipe';
export * from './lv2-sized/Root/RootInline.ts';
export * from './lv2-sized/ItemRoot/ItemRoot.recipe.ts';
export * from './lv2-sized/ItemRoot/ItemRoot.tsx';
export * from './lv2-sized/Section/Section.recipe.ts';
export * from './lv2-sized/Section/Section.tsx';
export * from './lv3-partial/Icon/Icon.recipe.ts';
export * from './lv3-partial/Icon/Icon.tsx';
export * from './lv3-partial/Indicator/Indicator.recipe.ts';
export * from './lv3-partial/Indicator/Indicator.tsx';
export * from './lv3-partial/Label/Label.style.ts';
export * from './lv3-partial/Label/Label.tsx';
export * from './lv3-partial/Tooltip/Tooltip.tsx';
export * from './lv4-normal/Button/Button.recipe.ts';
export * from './lv4-normal/Button/Button.tsx';
export * from './lv4-normal/Checkbox/Checkbox.recipe.ts';
export * from './lv4-normal/Checkbox/Checkbox.tsx';
export * from './lv4-normal/Radio/Radio.recipe.ts';
export * from './lv4-normal/Radio/Radio.tsx';
export * from './lv4-normal/Radio/RadioGroup.recipe.ts';
export * from './lv4-normal/Radio/RadioGroup.tsx';
export * from './lv4-normal/Radio/RadioGroupContext.ts';

View File

@@ -0,0 +1,5 @@
import { tv } from "tailwind-variants";
export const boxRoot = tv({
base: "flex justify-center items-center",
});

View File

@@ -0,0 +1,47 @@
import React from "react";
import { useThemeContext } from "../ThemeProvider/useThemeContext";
import { cn } from "tailwind-variants";
import type { CommonProps } from "@/common/CommonProps";
import { boxRoot } from "./Box.recipe";
// 别名<约束>=值
// 千万不要 C = As extend React.ElementType这样子连等号会切断推导
type AsProp<C extends React.ElementType> = { as?: C };
type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
export type PolymorphicProps<C extends React.ElementType, P = {}> = (P &
AsProp<C>) &
Omit<React.ComponentPropsWithRef<C>, PropsToOmit<C, P>>;
interface BoxProps<C extends React.ElementType> extends CommonProps {
as?: C;
}
const Box = <C extends React.ElementType = "div">(
props: PolymorphicProps<C, BoxProps<C>>,
ref?: React.ComponentPropsWithRef<C>["ref"],
) => {
const { as: Component = "div", children, className, ...rest } = props;
const { themeClass } = useThemeContext();
if (!themeClass) {
throw new Error("Box must be used within a ThemeProvider");
}
const boxRootClass = cn(themeClass, boxRoot(), className);
return (
<Component ref={ref} className={boxRootClass} {...(rest as any)}>
{children}
</Component>
);
};
export type BoxComponent = <C extends React.ElementType = "div">(
props: PolymorphicProps<C, BoxProps<C>> & {
ref?: React.ComponentPropsWithRef<C>["ref"];
},
) => React.ReactElement | null;
export default Box as BoxComponent;

View File

@@ -0,0 +1,50 @@
import * as React from "react";
import { cn } from "tailwind-variants";
export interface SlotProps extends React.HTMLAttributes<HTMLElement> {
children: React.ReactNode;
}
export const Slot = React.forwardRef<HTMLElement, SlotProps>(
(
{
children,
onClick: externalOnClick,
style: styleProp,
className: classNameProp,
...props
},
ref,
) => {
if (!React.isValidElement(children)) return null;
const child = children as React.ReactElement<Record<string, unknown>>;
const mergedClassName = cn(child.props.className as string, classNameProp);
const mergedStyle = {
...(child.props.style ?? {}),
...(styleProp ?? {}),
} as React.CSSProperties;
const childOnClick = child.props.onClick as
| ((e: React.MouseEvent<HTMLElement>) => void)
| undefined;
const mergedOnClick = (e: React.MouseEvent<HTMLElement>) => {
childOnClick?.(e);
externalOnClick?.(e);
};
return React.cloneElement(child, {
...child.props,
...props,
ref,
style: mergedStyle,
className: mergedClassName,
onClick: mergedOnClick,
});
},
);
Slot.displayName = "Slot";

View File

@@ -0,0 +1,9 @@
import { createContext } from "react";
export type Theme = "light" | "dark";
export const ThemeContext = createContext<{
theme?: Theme;
toggleTheme?: () => void;
themeClass?: string;
} | null>(null);

View File

@@ -0,0 +1,32 @@
import { useState, type ReactNode } from "react";
import { type Theme, ThemeContext } from "./ThemeContext";
import { cn } from "tailwind-variants";
type ThemeProviderProps = {
children?: ReactNode;
defaultTheme?: Theme;
};
export const ThemeProvider = ({
children,
defaultTheme,
}: ThemeProviderProps) => {
const [theme, setTheme] = useState<Theme>(defaultTheme || "light");
const toggleTheme = () => setTheme(theme === "light" ? "dark" : "light");
const frameworkClass = "dg";
const themeClass = cn(frameworkClass, theme);
return (
<ThemeContext.Provider value={{ theme, toggleTheme, themeClass }}>
<div
className={"dg-theme-provider"}
style={
theme == "light" ? { background: "white" } : { background: "black" }
}
>
{children}
</div>
</ThemeContext.Provider>
);
};

View File

@@ -0,0 +1,8 @@
import { useContext } from "react";
import { ThemeContext } from "./ThemeContext";
export function useThemeContext() {
const context = useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used within ThemeProvider");
return context;
}

View File

@@ -0,0 +1,119 @@
import { tv } from "tailwind-variants";
export const inlineRootRecipe = tv({
base: "relative overflow-hidden flex flex-nowrap",
variants: {
size: {
xs: "text-xs h-inline-xs",
sm: "text-sm h-inline-sm",
md: "text-md h-inline-md",
lg: "text-lg h-inline-lg",
xl: "text-xl h-inline-xl",
"2xl": "text-2xl h-inline-2xl",
},
shape: {
square: "rounded-none",
rounded: "",
circle: "rounded-full",
},
variant: {
filled: "variant-filled",
outline: "variant-outline",
subtle: "variant-subtle",
},
brand: {
success: "brand-success",
danger: "brand-danger",
info: "brand-info",
warning: "brand-warning",
emphasis: "brand-emphasis",
},
iconOnly: {
true: "",
false: "",
},
disabled: {
true: "",
false: "",
},
},
compoundVariants: [
{
shape: "rounded",
size: "xs",
class: "rounded-xs",
},
{
shape: "rounded",
size: "sm",
class: "rounded-sm",
},
{
shape: "rounded",
size: "md",
class: "rounded-md",
},
{
shape: "rounded",
size: "lg",
class: "rounded-lg",
},
{
shape: "rounded",
size: "xl",
class: "rounded-xl",
},
{
shape: "rounded",
size: "2xl",
class: "rounded-2xl",
},
// --------------------------------------------------
{
iconOnly: true,
size: "xs",
class: "w-inline-xs",
},
{
iconOnly: true,
size: "sm",
class: "w-inline-sm",
},
{
iconOnly: true,
size: "md",
class: "w-inline-md",
},
{
iconOnly: true,
size: "lg",
class: "w-inline-lg",
},
{
iconOnly: true,
size: "xl",
class: "w-inline-xl",
},
{
iconOnly: true,
size: "2xl",
class: "w-inline-2xl",
},
// --------------------------------------------------
{
disabled: true,
variant: "filled",
class: "variant-filled-disabled",
},
{
disabled: true,
variant: "outline",
class: "variant-outline-disabled",
},
{
disabled: true,
variant: "subtle",
class: "variant-subtle-disabled",
},
],
});

View File

@@ -0,0 +1,75 @@
import type { CommonProps } from "@/common/CommonProps";
import { cn } from "tailwind-variants";
import Box, { type PolymorphicProps } from "@/lv1-fundamental/Box/Box";
import { inlineRootRecipe } from "./IinlineRoot.recipe";
interface ItemInlineRootProps<C extends React.ElementType> extends CommonProps {
as?: C;
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
shape?: "square" | "rounded" | "circle";
variant?: "filled" | "outline" | "subtle";
brand?: "success" | "danger" | "info" | "warning" | "emphasis";
iconOnly?: boolean;
}
const ItemInlineRoot = <C extends React.ElementType = "span">(
props: PolymorphicProps<C, ItemInlineRootProps<C>>,
ref?: React.ComponentPropsWithRef<C>["ref"],
) => {
const {
as = "span",
size = "sm",
shape = "square",
brand,
iconOnly = false,
variant,
onClick,
disabled = false,
className,
children,
...rest
} = props;
const handleClick = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
if (disabled) {
e.preventDefault();
return;
}
onClick?.(e);
};
const itemInlineRootClass = cn(
inlineRootRecipe({
variant,
size,
shape,
iconOnly,
disabled,
brand,
}),
className,
);
return (
<Box
as={as}
className={itemInlineRootClass}
onClick={handleClick}
disabled={disabled}
ref={ref}
{...(rest as any)}
>
{children}
</Box>
);
};
ItemInlineRoot.displayName = "ItemInlineRoot";
export type ItemInlineRootComponent = <C extends React.ElementType = "span">(
props: PolymorphicProps<C, ItemInlineRootProps<C>> & {
ref?: React.ComponentPropsWithRef<C>["ref"];
},
) => React.ReactElement | null;
export default ItemInlineRoot as ItemInlineRootComponent;

View File

@@ -0,0 +1,123 @@
import { tv } from "tailwind-variants";
export const itemRootRecipe = tv({
base: "relative select-none overflow-hidden flex flex-nowrap",
variants: {
size: {
xs: "text-xs h-xs px-xs gap-xs",
sm: "text-sm h-sm px-sm gap-sm",
md: "text-md h-md px-md gap-md",
lg: "text-lg h-lg px-lg gap-lg",
xl: "text-xl h-xl px-xl gap-xl",
"2xl": "text-2xl h-2xl px-2xl gap-2xl",
},
shape: {
square: "rounded-none",
rounded: "",
circle: "rounded-full",
},
variant: {
filled: "variant-filled",
outline: "variant-outline",
subtle: "variant-subtle",
},
brand: {
success: "brand-success",
danger: "brand-danger",
info: "brand-info",
warning: "brand-warning",
emphasis: "brand-emphasis",
},
iconOnly: {
true: "",
false: "",
},
hasShadow: {
true: "shadow-xl",
false: "",
},
disabled: {
true: "",
false: "",
},
},
compoundVariants: [
{
shape: "rounded",
size: "xs",
class: "rounded-xs",
},
{
shape: "rounded",
size: "sm",
class: "rounded-sm",
},
{
shape: "rounded",
size: "md",
class: "rounded-md",
},
{
shape: "rounded",
size: "lg",
class: "rounded-lg",
},
{
shape: "rounded",
size: "xl",
class: "rounded-xl",
},
{
shape: "rounded",
size: "2xl",
class: "rounded-2xl",
},
// --------------------------------------------------
{
iconOnly: true,
size: "xs",
class: "w-xs",
},
{
iconOnly: true,
size: "sm",
class: "w-sm",
},
{
iconOnly: true,
size: "md",
class: "w-md",
},
{
iconOnly: true,
size: "lg",
class: "w-lg",
},
{
iconOnly: true,
size: "xl",
class: "w-xl",
},
{
iconOnly: true,
size: "2xl",
class: "w-2xl",
},
// --------------------------------------------------
{
disabled: true,
variant: "filled",
class: "variant-filled-disabled",
},
{
disabled: true,
variant: "outline",
class: "variant-outline-disabled",
},
{
disabled: true,
variant: "subtle",
class: "variant-subtle-disabled",
},
],
});

View File

@@ -0,0 +1,81 @@
import type { CommonProps } from "@/common/CommonProps";
import { itemRootRecipe } from "./ItemRoot.recipe";
import { cn } from "tailwind-variants";
import Box, { type PolymorphicProps } from "@/lv1-fundamental/Box/Box";
interface ItemRootProps<C extends React.ElementType> extends CommonProps {
as?: C;
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
shape?: "square" | "rounded" | "circle";
variant?: "filled" | "outline" | "subtle";
brand?: "success" | "danger" | "info" | "warning" | "emphasis";
hasShadow?: boolean;
iconOnly?: boolean;
}
const ItemRoot = <C extends React.ElementType = "div">(
props: PolymorphicProps<C, ItemRootProps<C>>,
ref?: React.ComponentPropsWithRef<C>["ref"],
) => {
const {
as = "div",
size = "sm",
shape = "rounded",
brand: propBrand,
iconOnly = false,
variant,
hasShadow = false,
onClick,
disabled = false,
className,
children,
...rest
} = props;
const brand =
variant == "filled" && propBrand == undefined ? "info" : propBrand;
const handleClick = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
if (disabled) {
e.preventDefault();
return;
}
onClick?.(e);
};
const itemRootClass = cn(
itemRootRecipe({
variant,
size,
shape,
iconOnly,
disabled,
hasShadow,
brand,
}),
className,
);
return (
<Box
as={as}
className={itemRootClass}
onClick={handleClick}
disabled={disabled}
ref={ref}
{...(rest as any)}
>
{children}
</Box>
);
};
ItemRoot.displayName = "ItemRoot";
export type ItemRootComponent = <C extends React.ElementType = "div">(
props: PolymorphicProps<C, ItemRootProps<C>> & {
ref?: React.ComponentPropsWithRef<C>["ref"];
},
) => React.ReactElement | null;
export default ItemRoot as ItemRootComponent;

View File

@@ -0,0 +1,12 @@
import { tv } from "tailwind-variants";
export const sectionRoot = tv({
base: "",
variants: {
align: {
start: "justify-start",
center: "justify-center",
end: "justify-end",
},
},
});

View File

@@ -0,0 +1,23 @@
import type { CommonProps } from "@/common/CommonProps";
import { forwardRef } from "react";
import { sectionRoot } from "./Section.recipe";
import Box from "@/lv1-fundamental/Box/Box";
import { cn } from "tailwind-variants";
type SectionProps = CommonProps & {
align?: "start" | "center" | "end";
};
export const Section = forwardRef<HTMLDivElement, SectionProps>(
(props, ref) => {
const { className, align, children, ...rest } = props;
const sectionRootClass = cn(sectionRoot({ align }), className);
return (
<Box className={sectionRootClass} ref={ref} {...rest}>
{children}
</Box>
);
},
);

View File

@@ -0,0 +1,19 @@
import { tv } from "tailwind-variants";
export const iconRoot = tv({
base: "dg-icon",
});
export const iconSvg = tv({
base: "dg-icon-svg",
variants: {
size: {
xs: "dg-icon-svg_size--xs",
sm: "dg-icon-svg_size--sm",
md: "dg-icon-svg_size--md",
lg: "dg-icon-svg_size--lg",
xl: "dg-icon-svg_size--xl",
"2xl": "dg-icon-svg_size--2xl",
},
},
});

View File

@@ -0,0 +1,25 @@
import type { CommonProps } from "@/common/CommonProps";
import { forwardRef } from "react";
import { iconRoot, iconSvg } from "./Icon.recipe.ts";
import { Slot } from "@/lv1-fundamental/Slot/Slot";
import { cn } from "tailwind-variants";
import InlineRoot from "@/lv2-sized/InlineRoot/InlineRoot.tsx";
type IconProps = CommonProps & {
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
};
export const Icon = forwardRef<HTMLSpanElement, IconProps>((props, ref) => {
const { size, children, className, ...rest } = props;
const iconRootClass = cn(iconRoot(), className);
const iconSvgClass = iconSvg({ size });
return (
<InlineRoot size={size} className={iconRootClass} ref={ref} {...rest}>
<Slot className={iconSvgClass}>{children}</Slot>
</InlineRoot>
);
});
Icon.displayName = "Icon";

View File

@@ -0,0 +1,5 @@
import { tv } from "tailwind-variants";
export const indicatorInput = tv({ base: "pointer-none" });
export const indicatorBoxSvg = tv({ base: "absolute z-10" });
export const indicatorCheckSvg = tv({ base: "absolute z-20" });

View File

@@ -0,0 +1,92 @@
import { forwardRef, type ReactNode } from "react";
import {
// indicatorBoxSvg,
// indicatorCheckSvg,
indicatorInput,
} from "./Indicator.recipe";
import type { CommonProps } from "@/common/CommonProps";
import InlineRoot from "@/lv2-sized/InlineRoot/InlineRoot";
// import { CheckIndicatorOutlineSvg } from "@/assets/svg/CheckIndicatorOutlineSvg";
// import { RadioIndicatorOutlineSvg } from "@/assets/svg/RadioIndicatorOutlineSvg";
// import { RadioIndicatorSvg } from "@/assets/svg/RadioIndicatorSvg";
// import { CheckIndicatorSvg } from "@/assets/svg/CheckIndicatorSvg";
type IndicatorProps = CommonProps & {
size?: "xs" | "sm";
type?: "checkbox" | "radio";
boxSvg?: ReactNode;
checkSvg?: ReactNode;
checked?: boolean;
id?: string;
};
export const Indicator = forwardRef<HTMLInputElement, IndicatorProps>(
(props, ref) => {
const {
size = "sm",
type = "checkbox",
// boxSvg,
// checkSvg,
checked,
id,
disabled,
} = props;
// const currentBoxSvg = boxSvg ? (
// boxSvg
// ) : type == "checkbox" ? (
// <CheckIndicatorOutlineSvg />
// ) : (
// <RadioIndicatorOutlineSvg />
// );
// const currentCheckSvg = checkSvg ? (
// checkSvg
// ) : type == "checkbox" ? (
// <CheckIndicatorSvg />
// ) : (
// <RadioIndicatorSvg />
// );
const indicatorInputClass = indicatorInput();
// const indicatorBoxSvgClass = indicatorBoxSvg();
// const indicatorCheckSvgClass = indicatorCheckSvg();
return (
<InlineRoot size={size} iconOnly={true}>
<InlineRoot
as="input"
id={id}
type={type}
size={size}
ref={ref}
iconOnly={true}
checked={checked} // 必须显式绑定,但还需要阻止默认行为
// 此处仅用于阻止默认行为,不要处理 click 逻辑
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
e.preventDefault(); // 阻止默认行为,防止闪烁
return false;
}}
// 此处仅用于阻止默认行为,不要处理 keydown 逻辑
onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
e.preventDefault(); // 阻止默认行为,防止闪烁
return false;
}}
disabled={disabled} // 交互类 disabled 需要手动传入,而不是依靠父元素灰色滤镜
className={indicatorInputClass}
/>
{/* <Icon className={indicatorBoxSvgClass} size={size}>
{currentBoxSvg}
</Icon>
{checked && (
<Icon className={indicatorCheckSvgClass} size={size}>
{currentCheckSvg}
</Icon>
)} */}
</InlineRoot>
);
},
);
Indicator.displayName = "Indicator";

View File

@@ -0,0 +1,30 @@
@use "../../styles/index.scss" as *;
.dg-label {
@include add-specificity(3) {
}
}
.dg-label_align--start {
@include add-specificity(3) {
@include justify-start;
}
}
.dg-label_align--center {
@include add-specificity(3) {
@include justify-center;
}
}
.dg-label_align--end {
@include add-specificity(3) {
@include justify-end;
}
}
.dg-label_is-subtext--true {
@include add-specificity(3) {
@include color-subtext;
}
}
.dg-label_is-subtext--false {
@include add-specificity(3) {
}
}

View File

@@ -0,0 +1,16 @@
import { tv } from "tailwind-variants";
export const labelRoot = tv({
base: "dg-label",
variants: {
align: {
start: "dg-label_align--start",
center: "dg-label_align--center",
end: "dg-label_align--end",
},
isSubText: {
true: "dg-label_is-subtext--true",
false: "dg-label_is-subtext--false",
},
},
});

View File

@@ -0,0 +1,39 @@
import type { CommonProps } from "@/common/CommonProps";
import { forwardRef } from "react";
import { cn } from "tailwind-variants";
import { labelRoot } from "./Label.style";
import { RootInline } from "@/lv2-sized/Root/RootInline";
type LabelProps = CommonProps & {
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
align?: "start" | "center" | "end";
isSubText?: boolean;
for?: string;
};
export const Label = forwardRef<HTMLLabelElement, LabelProps>((props, ref) => {
const {
size,
isSubText = false,
align,
children,
className,
...rest
} = props;
const labelRootClass = cn(labelRoot({ align, isSubText }), className);
return (
<RootInline
as="label"
size={size}
className={labelRootClass}
ref={ref}
{...rest}
>
{children}
</RootInline>
);
});
Label.displayName = "Label";

View File

@@ -0,0 +1,7 @@
@use "../../styles/index.scss" as *;
.dg-tooltip-dropdown {
@include add-specificity(3) {
border: 1px solid;
}
}

View File

@@ -0,0 +1,70 @@
import { Slot } from "@/lv1-fundamental/Slot/Slot";
import Root from "@/lv2-sized/ItemRoot/ItemRoot";
import type { CommonProps } from "@/common/CommonProps";
import {
autoUpdate,
flip,
FloatingPortal,
offset,
shift,
useDismiss,
useFloating,
useFocus,
useHover,
useInteractions,
useRole,
type Placement,
} from "@floating-ui/react";
import { useState, type ReactNode } from "react";
type TooltipProps = CommonProps & {
title?: ReactNode;
placement?: Placement;
};
export const Tooltip = (props: TooltipProps) => {
const { children, placement = "top", title } = props;
const [open, setOpen] = useState(false);
const { refs, floatingStyles, context } = useFloating({
open: open,
onOpenChange: setOpen,
placement: placement,
middleware: [offset(4), flip(), shift()],
whileElementsMounted: autoUpdate,
});
const hover = useHover(context, {
delay: { open: 0, close: 0 },
});
const focus = useFocus(context);
const dismiss = useDismiss(context);
const role = useRole(context, { role: "tooltip" });
const { getReferenceProps, getFloatingProps } = useInteractions([
hover,
focus,
dismiss,
role,
]);
return (
<>
<Slot ref={refs.setReference} {...getReferenceProps()}>
{children}
</Slot>
{open && (
<FloatingPortal>
<Root
size="fit"
ref={refs.setFloating}
style={floatingStyles}
hasShadow={true}
{...getFloatingProps()}
>
{title}
</Root>
</FloatingPortal>
)}
</>
);
};

View File

@@ -0,0 +1,31 @@
@use "../../styles/index.scss" as *;
.dg-button {
@include add-specificity(4) {
padding-block: 0;
}
}
.dg-button-content {
@include add-specificity(4) {
}
}
.dg-button_isloading--true {
@include add-specificity(4) {
@include loading-true;
}
}
.dg-button_isloading--false {
@include add-specificity(4) {
@include loading-false;
}
}
.dg-button-loading-icon {
@include add-specificity(4) {
@include absolute;
}
}
.dg-button-icon {
@include add-specificity(4) {
}
}

View File

@@ -0,0 +1,93 @@
import Box from "@/lv1-fundamental/Box/Box";
import Root from "@/lv2-sized/ItemRoot/ItemRoot";
import { Icon } from "@/lv3-partial/Icon/Icon";
import { Label } from "@/lv3-partial/Label/Label";
import type { CommonProps } from "@/common/CommonProps";
import { forwardRef } from "react";
import {
buttonContent,
buttonIcon,
buttonLoadingIcon,
buttonRoot,
} from "./Button.recipe";
import { cn } from "tailwind-variants";
import { SpinnerSvg } from "@/assets/svg/SpinnerSvg";
type ButtonProps = CommonProps & {
size?: "xs" | "sm" | "md" | "lg";
shape?: "circle" | "rounded" | "square";
variant?: "filled" | "outline" | "subtle";
isLoading?: boolean;
loadingIcon?: React.ReactNode;
iconSvg?: React.ReactNode;
iconOnly?: boolean;
hideIcon?: boolean; // not a state just a attribute
onClick?: () => void;
};
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
const {
size = "md",
shape = "rounded",
variant = "filled",
iconOnly = false,
iconSvg,
hideIcon = false,
isLoading,
loadingIcon,
children,
className,
disabled,
...rest
} = props;
const buttonRootClass = cn(buttonRoot({ iconOnly }), className);
const buttonIconClass = buttonIcon();
const buttonContentClass =
(isLoading != undefined && buttonContent({ isLoading })) || "";
const buttonLoadingIconClass = buttonLoadingIcon({
isLoading,
});
return (
<Root
ref={ref}
as="button"
size={size}
variant={variant}
shape={shape}
iconOnly={iconOnly}
className={buttonRootClass}
{...rest}
>
<Box className={buttonContentClass}>
{iconOnly && (
// if iconOnly
<Icon size={size} className={buttonIconClass}>
{iconSvg && !hideIcon ? iconSvg : null}
</Icon>
)}
{!iconOnly &&
// if not iconOnly
iconSvg &&
!hideIcon ? (
<Icon size={size}>{iconSvg}</Icon>
) : null}
{!iconOnly ? <Label size={size}>{children}</Label> : null}
</Box>
{isLoading != undefined && (
<Icon size={size} className={buttonLoadingIconClass}>
<SpinnerSvg />
</Icon>
)}
</Root>
);
},
);
Button.displayName = "Button";

View File

@@ -0,0 +1,8 @@
import { tv } from "tailwind-variants";
export const checkboxRoot = tv({
base: "dg-checkbox",
});
export const checkboxLabel = tv({
base: "dg-checkbox-label",
});

View File

@@ -0,0 +1,13 @@
@use "../../styles/index.scss" as *;
.dg-checkbox {
@include add-specificity(4) {
cursor: pointer;
padding-block: 0;
}
}
.dg-checkbox-label {
@include add-specificity(4) {
cursor: pointer;
}
}

View File

@@ -0,0 +1,103 @@
import { RootInline } from "@/lv2-sized/Root/RootInline";
import { Icon } from "@/lv3-partial/Icon/Icon";
import { Label } from "@/lv3-partial/Label/Label";
import type { CommonProps } from "@/common/CommonProps";
import { forwardRef, useState, type ReactNode } from "react";
import { cn } from "tailwind-variants";
import { checkboxLabel, checkboxRoot } from "./Checkbox.recipe";
import Root from "@/lv2-sized/ItemRoot/ItemRoot";
import { Indicator } from "@/lv3-partial/Indicator/Indicator";
type CheckboxProps = CommonProps & {
id?: string;
checked?: boolean;
defaultChecked?: boolean;
onChange?: (v: boolean) => void;
size?: "xs" | "sm";
isPlaceholder?: boolean;
indicatorBoxSvg?: ReactNode;
indicatorCheckSvg?: ReactNode;
iconSvg?: ReactNode;
};
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
(props, ref) => {
const {
id,
checked: controllerChecked,
defaultChecked = false,
onChange,
size = "sm",
isPlaceholder = false,
indicatorBoxSvg,
indicatorCheckSvg,
iconSvg,
className,
children,
disabled = false,
...rest
} = props;
const isControlled = controllerChecked !== undefined;
const [innerChecked, setInnerChecked] = useState(defaultChecked ?? false);
const currentChecked = isControlled ? controllerChecked : innerChecked;
const handleClick = (_e: React.MouseEvent<HTMLInputElement>) => {
// 若受控,点击不变
if (isControlled) {
return false;
}
// 若不受控,点击变更
const next = !currentChecked;
setInnerChecked(next);
onChange?.(next);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// 若受控,点击不变
if (isControlled) {
return false;
}
// 若不受控,点击变更
const next = !currentChecked;
if (e.key === " " || e.key === "Enter") {
setInnerChecked(next);
}
onChange?.(next);
};
const checkboxRootClass = cn(checkboxRoot(), className);
const checkboxLabelClass = checkboxLabel();
return (
<Root
size={size}
className={checkboxRootClass}
onClick={handleClick}
onKeyDown={handleKeyDown}
disabled={disabled} // 不同于 input 的 disabled此处仅提供灰色滤镜
{...rest}
>
{isPlaceholder ? (
<RootInline size={size} iconOnly={true} />
) : (
<Indicator
size={size}
disabled={disabled}
type="checkbox"
checked={currentChecked}
ref={ref}
/>
)}
{iconSvg ? <Icon size={size}>{iconSvg}</Icon> : null}
<Label size={size} for={id} className={checkboxLabelClass}>
{children}
</Label>
</Root>
);
},
);
Checkbox.displayName = "Checkbox";

View File

@@ -0,0 +1,9 @@
import { tv } from "tailwind-variants";
export const radioRoot = tv({
base: "dg-radio",
});
export const radioLabel = tv({
base: "dg-radio-label",
});

View File

@@ -0,0 +1,13 @@
@use "../../styles/index.scss" as *;
.dg-radio {
@include add-specificity(4) {
cursor: pointer;
padding-block: 0;
}
}
.dg-radio-label {
@include add-specificity(4) {
cursor: pointer;
}
}

View File

@@ -0,0 +1,114 @@
import { RootInline } from "@/lv2-sized/Root/RootInline";
import { Icon } from "@/lv3-partial/Icon/Icon";
import { Label } from "@/lv3-partial/Label/Label";
import type { CommonProps } from "@/common/CommonProps";
import { forwardRef, useContext, useState, type ReactNode } from "react";
import { cn } from "tailwind-variants";
import { radioLabel, radioRoot } from "./Radio.recipe";
import { RadioGroupContext } from "./RadioGroupContext";
import { Indicator } from "@/lv3-partial/Indicator/Indicator";
import Root from "@/lv2-sized/ItemRoot/ItemRoot";
type RadioProps = CommonProps & {
id?: string;
name?: string;
value: string;
checked?: boolean;
defaultChecked?: boolean;
onChange?: (name: string, value: boolean) => void;
size?: "xs" | "sm";
isPlaceholder?: boolean;
indicatorBoxSvg?: ReactNode;
indicatorCheckSvg?: ReactNode;
iconSvg?: ReactNode;
};
export const Radio = forwardRef<HTMLInputElement, RadioProps>((props, ref) => {
const {
checked: controllerChecked,
defaultChecked = false,
onChange,
size = "sm",
isPlaceholder = false,
indicatorBoxSvg,
indicatorCheckSvg,
iconSvg,
className,
children,
disabled = false,
id,
name,
value,
...rest
} = props;
const ctx = useContext(RadioGroupContext);
if (!ctx) {
throw new Error("Radio must be used within a RadioGroup");
}
const isControlled = controllerChecked !== undefined;
const [innerChecked, setInnerChecked] = useState(defaultChecked ?? false);
const currentChecked = isControlled ? controllerChecked : innerChecked;
const currentSize = ctx.size ?? size;
const currentName = ctx.name ?? name;
const handleClick = (_e: React.MouseEvent<HTMLInputElement>) => {
// 若受控,点击不变
if (isControlled) {
return false;
}
// 若不受控,点击变更
const next = !currentChecked;
setInnerChecked(next);
onChange?.(currentName, next);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
// 若受控,点击不变
if (isControlled) {
return false;
}
// 若不受控,点击变更
const next = !currentChecked;
if (e.key === " " || e.key === "Enter") {
setInnerChecked(next);
}
onChange?.(currentName, next);
};
const radioRootClass = cn(radioRoot(), className);
const radioLabelClass = radioLabel();
return (
<Root
size={currentSize}
className={radioRootClass}
onClick={handleClick}
onKeyDown={handleKeyDown}
disabled={disabled} // 不同于 input 的 disabled此处仅提供灰色滤镜
{...rest}
>
{isPlaceholder ? (
<RootInline size={currentSize} iconOnly={true} />
) : (
<Indicator
size={currentSize}
disabled={disabled}
type="radio"
checked={currentChecked}
ref={ref}
/>
)}
{iconSvg ? <Icon size={currentSize}>{iconSvg}</Icon> : null}
<Label size={currentSize} for={id} className={radioLabelClass}>
{children}
</Label>
</Root>
);
});
Radio.displayName = "Radio";

View File

@@ -0,0 +1,15 @@
import { tv } from "tailwind-variants";
export const radioGroupRoot = tv({
base: "dg-radio-group",
variants: {
direction: {
horizontal: "dg-radio-group_direction--horizontal",
vertical: "dg-radio-group_direction--vertical",
},
},
});
export const radioGroupList = tv({
base: "dg-radio-group-list",
});

View File

@@ -0,0 +1,24 @@
@use "../../styles/index.scss" as *;
.dg-radio-group {
@include add-specificity(4) {
@include flex-col;
}
}
.dg-radio-group_direction--horizontal {
@include add-specificity(4) {
@include flex-row;
}
}
.dg-radio-group_direction--vertical {
@include add-specificity(4) {
@include flex-col;
}
}
.dg-radio-group-list {
@include add-specificity(4) {
@include flex-col;
@include justify-start;
@include items-start;
}
}

View File

@@ -0,0 +1,59 @@
import type { CommonProps } from "@/common/CommonProps";
import { RadioGroupContext } from "./RadioGroupContext";
import { useState } from "react";
import { Label } from "@/lv3-partial/Label/Label";
import Root from "@/lv2-sized/ItemRoot/ItemRoot";
import { radioGroupList, radioGroupRoot } from "./RadioGroup.recipe";
import Box from "@/lv1-fundamental/Box/Box";
type RadioGroupProps = CommonProps & {
name: string;
label?: string;
defaultValue?: string;
onChange?: (value: string) => void;
size?: "xs" | "sm";
direction?: "vertical" | "horizontal";
};
export const RadioGroup = (props: RadioGroupProps) => {
const {
name,
defaultValue,
onChange,
label,
size = "sm",
direction = "horizontal",
className,
children,
...rest
} = props;
const [value, setValue] = useState(defaultValue ?? "");
const handleChange = (v: string) => {
setValue(v);
onChange?.(v);
};
const radioGroupRootClass = radioGroupRoot({ direction });
const radioGroupListClass = radioGroupList();
return (
<RadioGroupContext.Provider
value={{ value: value, onChange: handleChange, name, size }}
>
<Root className={radioGroupRootClass} {...rest}>
<Box className={radioGroupListClass}>
{label && (
<Root size={size}>
<Label size={size}>
{label}
</Label>
</Root>
)}
{children}
</Box>
</Root>
</RadioGroupContext.Provider>
);
};

View File

@@ -0,0 +1,12 @@
import { createContext } from "react";
export type RadioGroupContextType = {
name: string;
onChange: (v: string) => void;
value: string;
size: "xs" | "sm";
};
export const RadioGroupContext = createContext<RadioGroupContextType | null>(
null,
);

View File

@@ -0,0 +1,36 @@
@theme {
--danger-fg: var(--base-fg);
--danger-bg-low-hover: var(--color-red-100);
--danger-bg-low-active: var(--color-red-200);
--danger-bg: var(--color-red-600);
--danger-bg-high-hover: var(--color-red-700);
--danger-bg-high-active: var(--color-red-800);
--danger-border-color: var(--color-transparent);
--success-fg: var(--base-fg);
--success-bg-low-hover: var(--color-green-100);
--success-bg-low-active: var(--color-green-200);
--success-bg: var(--color-green-600);
--success-bg-high-hover: var(--color-green-700);
--success-bg-high-active: var(--color-green-800);
--info-fg: var(--base-fg);
--info-bg-low-hover: var(--color-sky-100);
--info-bg-low-active: var(--color-sky-200);
--info-bg: var(--color-sky-600);
--info-bg-high-hover: var(--color-sky-700);
--info-bg-high-active: var(--color-sky-800);
--info-border-color: var(--color-transparent);
--warning-fg: var(--base-fg);
--warning-bg-low-hover: var(--color-amber-100);
--warning-bg-low-active: var(--color-amber-200);
--warning-bg: var(--color-amber-600);
--warning-bg-high-hover: var(--color-amber-700);
--warning-bg-high-active: var(--color-amber-800);
--warning-border-color: var(--color-transparent);
--emphasis-fg: var(--base-fg);
--emphasis-bg-low-hover: var(--color-gray-100);
--emphasis-bg-low-active: var(--color-gray-200);
--emphasis-bg: var(--color-gray-600);
--emphasis-bgr-high-hover: var(--color-gray-700);
--emphasis-bg-high-active: var(--color-gray-800);
--emphasis-border-color: var(--color-transparent);
}

View File

@@ -0,0 +1,25 @@
@theme {
--filled-fg: var(--color-white);
--filled-fg-hover: var(--brand-fg);
--filled-fg-active: var(--brand-fg);
--filled-bg: var(--brand-bg);
--filled-bg-hover: var(--brand-bg-high-hover);
--filled-bg-active: var(--brand-bg-high-active);
--filled-border-color: var(--brand-border-color);
--outline-fg: var(--brand-bg);
--outline-fg-hover: var(--brand-bg);
--outline-fg-active: var(--brand-bg);
--outline-bg: var(--brand-border-color);
--outline-bg-hover: var(--brand-bg-low-hover);
--outline-bg-active: var(--brand-bg-low-active);
--outline-border-color: var(--brand-bg);
--subtle-fg: var(--brand-bg);
--subtle-fg-hover: var(--brand-bg);
--subtle-fg-active: var(--brand-bg);
--subtle-bg: var(--brand-border-color);
--subtle-bg-hover: var(--brand-bg-low-hover);
--subtle-bg-active: var(--brand-bg-low-active);
--subtle-border-color: var(--brand-border-color);
}

View File

@@ -0,0 +1,45 @@
@utility brand-info {
--brand-fg: var(--info-fg);
--brand-bg-low-hover: var(--info-bg-low-hover);
--brand-bg-low-active: var(--info-bg-low-active);
--brand-bg: var(--info-bg);
--brand-bg-high-hover: var(--info-bg-high-hover);
--brand-bg-high-active: var(--info-bg-high-active);
--brand-border-color: var(--info-border-color);
}
@utility brand-danger {
--brand-fg: var(--danger-fg);
--brand-bg-low-hover: var(--danger-bg-low-hover);
--brand-bg-low-active: var(--danger-bg-low-active);
--brand-bg: var(--danger-bg);
--brand-bg-high-hover: var(--danger-bg-high-hover);
--brand-bg-high-active: var(--danger-bg-high-active);
--brand-border-color: var(--danger-border-color);
}
@utility brand-success {
--brand-fg: var(--success-fg);
--brand-bg-low-hover: var(--success-bg-low-hover);
--brand-bg-low-active: var(--success-bg-low-active);
--brand-bg: var(--success-bg);
--brand-bg-high-hover: var(--success-bg-high-hover);
--brand-bg-high-active: var(--success-bg-high-active);
--brand-border-color: var(--success-border-color);
}
@utility brand-warning {
--brand-fg: var(--warning-fg);
--brand-bg-low-hover: var(--warning-bg-low-hover);
--brand-bg-low-active: var(--warning-bg-low-active);
--brand-bg: var(--warning-bg);
--brand-bg-high-hover: var(--warning-bg-high-hover);
--brand-bg-high-active: var(--warning-bg-high-active);
--brand-border-color: var(--warning-border-color);
}
@utility brand-emphasis {
--brand-fg: var(--emphasis-fg);
--brand-bg-low-hover: var(--emphasis-bg-low-hover);
--brand-bg-low-active: var(--emphasis-bg-low-active);
--brand-bg: var(--emphasis-bg);
--brand-bg-high-hover: var(--emphasis-bg-high-hover);
--brand-bg-high-active: var(--emphasis-bg-high-active);
--brand-border-color: var(--emphasis-border-color);
}

View File

@@ -0,0 +1,18 @@
@utility gap-xs {
gap: calc(var(--spacing) * 0.5);
}
@utility gap-sm {
gap: calc(var(--spacing) * 2);
}
@utility gap-md {
gap: calc(var(--spacing) * 3);
}
@utility gap-lg {
gap: calc(var(--spacing) * 4);
}
@utility gap-xl {
gap: calc(var(--spacing) * 5);
}
@utility gap-2xl {
gap: calc(var(--spacing) * 6);
}

View File

@@ -0,0 +1,36 @@
@utility h-xs {
height: calc(var(--spacing) * 5);
}
@utility h-sm {
height: calc(var(--spacing) * 6);
}
@utility h-md {
height: calc(var(--spacing) * 8);
}
@utility h-lg {
height: calc(var(--spacing) * 9);
}
@utility h-xl {
height: calc(var(--spacing) * 11);
}
@utility h-2xl {
height: calc(var(--spacing) * 16);
}
@utility h-inline-xs {
height: var(--text-xs--line-height);
}
@utility h-inline-sm {
height: var(--text-sm--line-height);
}
@utility h-inline-md {
height: var(--text-base--line-height);
}
@utility h-inline-lg {
height: var(--text-lg--line-height);
}
@utility h-inline-xl {
height: var(--text-xl--line-height);
}
@utility h-inline-2xl {
height: var(--text-2xl--line-height);
}

View File

@@ -0,0 +1,16 @@
@utility loading-true {
position: "absolute";
top: "50%";
left: "50%";
transform: "translate(-50%, -50%)";
opacity: 1;
transition: "top 0.15s ease-in, opacity 0.1s ease-in";
}
@utility loading-false {
position: "absolute";
top: "-50%";
left: "50%";
transform: "translate(-50%, -50%)";
opacity: 0;
transition: "top 0.15s ease-out, opacity 0.1s ease-out";
}

View File

@@ -0,0 +1,21 @@
@utility mr-none {
margin-right: 0;
}
@utility mr-xs {
margin-right: calc(var(--spacing) * 1);
}
@utility mr-sm {
margin-right: calc(var(--spacing) * 2);
}
@utility mr-md {
margin-right: calc(var(--spacing) * 3);
}
@utility mr-lg {
margin-right: calc(var(--spacing) * 4);
}
@utility mr-xl {
margin-right: calc(var(--spacing) * 2);
}
@utility mr-2xl {
margin-right: calc(var(--spacing) * 2);
}

View File

@@ -0,0 +1,36 @@
@utility px-xs {
padding-inline: calc(var(--spacing) * 2);
}
@utility px-sm {
padding-inline: calc(var(--spacing) * 3);
}
@utility px-md {
padding-inline: calc(var(--spacing) * 4);
}
@utility px-lg {
padding-inline: calc(var(--spacing) * 5);
}
@utility px-xl {
padding-inline: calc(var(--spacing) * 6);
}
@utility px-2xl {
padding-inline: calc(var(--spacing) * 8);
}
@utility py-xs {
padding-block: calc(var(--spacing) * 1);
}
@utility py-sm {
padding-block: calc(var(--spacing) * 2);
}
@utility py-md {
padding-block: calc(var(--spacing) * 3);
}
@utility py-lg {
padding-block: calc(var(--spacing) * 4);
}
@utility py-xl {
padding-block: calc(var(--spacing) * 5);
}
@utility py-2xl {
padding-block: calc(var(--spacing) * 6);
}

View File

@@ -0,0 +1,13 @@
@utility light {
--base-fg: var(--color-gray-950);
--base-bg: var(--color-white);
--base-fg-subtext: var(--color-gray-500);
--base-bg-subtext: var(--color-gray-200);
}
@utility dark {
--base-fg: var(--color-gray-50);
--base-bg: var(--color-black);
--base-fg-subtext: var(--color-gray-400);
--base-bg-subtext: var(--color-gray-600);
}

View File

@@ -0,0 +1,137 @@
@utility variant-filled {
color: var(--filled-fg);
background-color: var(--filled-bg);
border-color: var(--filled-border-color);
&:hover {
background-color: var(--filled-bg-hover);
}
&:active {
background-color: var(--filled-bg-active);
}
&:focus-visible {
background-color: var(--filled-bg-hover);
}
}
@utility variant-outline {
color: var(--outline-fg);
background-color: var(--outline-bg);
border-color: var(--outline-border-color);
&:hover {
background-color: var(--outline-bg-hover);
}
&:active {
background-color: var(--outline-bg-active);
}
&:focus-visible {
background-color: var(--outline-bg-hover);
}
}
@utility variant-subtle {
color: var(--subtle-fg);
background-color: var(--subtle-bg);
border-color: var(--subtle-border-color);
&:hover {
background-color: var(--subtle-bg-hover);
}
&:active {
background-color: var(--subtle-bg-active);
}
&:focus-visible {
background-color: var(--subtle-bg-hover);
}
}
@utility variant-filled-disabled {
color: var(--filled-fg);
background-color: var(--filled-bg);
border-color: var(--filled-border-color);
filter: grayscale(50%);
&:hover {
color: var(--filled-fg);
background-color: var(--filled-bg);
border-color: var(--filled-border-color);
filter: grayscale(50%);
}
&:active {
color: var(--filled-fg);
background-color: var(--filled-bg);
border-color: var(--filled-border-color);
filter: grayscale(50%);
}
&:focus-visible {
color: var(--filled-fg);
background-color: var(--filled-bg);
border-color: var(--filled-border-color);
filter: grayscale(50%);
}
}
@utility variant-outline-disabled {
color: var(--outline-fg);
background-color: var(--outline-bg);
border-color: var(--outline-border-color);
filter: grayscale(50%);
&:hover {
color: var(--outline-fg);
background-color: var(--outline-bg);
border-color: var(--outline-border-color);
filter: grayscale(50%);
}
&:active {
color: var(--outline-fg);
background-color: var(--outline-bg);
border-color: var(--outline-border-color);
filter: grayscale(50%);
}
&:focus-visible {
color: var(--outline-fg);
background-color: var(--outline-bg);
border-color: var(--outline-border-color);
filter: grayscale(50%);
}
}
@utility variant-subtle-disabled {
color: var(--subtle-fg);
background-color: var(--subtle-bg);
border-color: var(--subtle-border-color);
filter: grayscale(50%);
&:hover {
color: var(--subtle-fg);
background-color: var(--subtle-bg);
border-color: var(--subtle-border-color);
filter: grayscale(50%);
}
&:active {
color: var(--subtle-fg);
background-color: var(--subtle-bg);
border-color: var(--subtle-border-color);
filter: grayscale(50%);
}
&:focus-visible {
color: var(--subtle-fg);
background-color: var(--subtle-bg);
border-color: var(--subtle-border-color);
filter: grayscale(50%);
}
}

View File

@@ -0,0 +1,36 @@
@utility w-xs {
width: calc(var(--spacing) * 5);
}
@utility w-sm {
width: calc(var(--spacing) * 6);
}
@utility w-md {
width: calc(var(--spacing) * 8);
}
@utility w-lg {
width: calc(var(--spacing) * 9);
}
@utility w-xl {
width: calc(var(--spacing) * 11);
}
@utility w-2xl {
width: calc(var(--spacing) * 16);
}
@utility w-inline-xs {
width: var(--text-xs--line-height);
}
@utility w-inline-sm {
width: var(--text-sm--line-height);
}
@utility w-inline-md {
width: var(--text-base--line-height);
}
@utility w-inline-lg {
width: var(--text-lg--line-height);
}
@utility w-inline-xl {
width: var(--text-xl--line-height);
}
@utility w-inline-2xl {
width: var(--text-2xl--line-height);
}

25
packages/ui/tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"extends": "@tsconfig/vite-react/tsconfig.json",
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"outDir": "./dist",
"rootDir": "./src",
"declaration": true, // 补充:生成类型声明文件,供其他子包引用
"skipLibCheck": true, // 补充:跳过第三方库类型校验,避免冲突、提升速度
"types": ["vite/client"]
},
"include": ["src"],
"exclude": [
"node_modules",
"dist",
"vite.config.ts",
"example/**/*",
"scripts/**/*",
".storybook/**/*",
"stories/**/*"
]
}

View File

@@ -0,0 +1,78 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import dts from "vite-plugin-dts";
import { resolve } from "node:path";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig(({ mode }) => {
const isProduction = mode === "production";
return {
// React 核心插件
plugins: [
tailwindcss(),
react(),
dts({
include: ["src/**/*"],
exclude: ["src/**/*.test.ts", "src/**/*.stories.tsx"],
outDir: "./dist",
rollupTypes: true, // 合并类型声明文件
}),
],
// 路径别名与后缀配置
resolve: {
alias: {
"@": resolve(__dirname, "./src"),
},
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"],
},
// 纯 ES 模式打包配置(移除 UMD 相关)
build: {
lib: {
entry: resolve(__dirname, "./src/index.ts"),
formats: ["es"], // 仅保留 ES 模块格式
fileName: () => "index.es.js", // 固定 ES 模式文件名
},
rollupOptions: {
// 排除 React 相关依赖(用户项目自行引入)
external: ["react", "react-dom"],
output: {
compact: isProduction, // 生产环境压缩代码格式
globals: {
react: "React",
"react-dom": "ReactDOM",
},
assetFileNames: (assetInfo) => {
if (assetInfo.name && assetInfo.name.endsWith(".css")) {
return "index.css";
}
return assetInfo.name || "[name].[ext]";
},
},
// 移除rollupOptions 下无效的 exclude 配置
// exclude: ["example/**/*", "scripts/**/*"],
},
outDir: "./dist",
cssCodeSplit: false,
sourcemap: true,
minify: isProduction ? "esbuild" : false,
emptyOutDir: true,
},
// TS 兼容配置
esbuild: {
ignoreAnnotations: true,
// 移除esbuild 下无效的 exclude 配置
// exclude: ["example/**/*", "scripts/**/*"],
},
// 新增:使用 Vite 官方支持的方式排除文件
// 通过 optimizeDeps.exclude 排除依赖,通过 build.assetsInclude 反向控制,
// 同时结合 tsconfig.json 的 exclude 确保 TS 编译也排除目标文件
optimizeDeps: {
exclude: ["example", "scripts"],
},
};
});