diff --git a/apps/web/package.json b/apps/web/package.json
index 0eff085..f10e338 100755
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -32,6 +32,7 @@
"@hookform/resolvers": "^3.9.1",
"@nice/client": "workspace:^",
"@nice/common": "workspace:^",
+ "@nice/config": "workspace:^",
"@nice/iconer": "workspace:^",
"@nice/utils": "workspace:^",
"mind-elixir": "workspace:^",
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 54a93a3..7f00070 100755
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -24,7 +24,7 @@ function App() {
theme={{
algorithm: theme.defaultAlgorithm,
token: {
- colorPrimary: "#2e75b6",
+ colorPrimary: "#0088E8",
},
components: {},
}}>
diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx
new file mode 100644
index 0000000..d549a1e
--- /dev/null
+++ b/apps/web/src/app/main/courses/components/CourseCard.tsx
@@ -0,0 +1,48 @@
+import { Card, Rate, Tag } from 'antd';
+import { Course } from '../mockData';
+import { UserOutlined, ClockCircleOutlined } from '@ant-design/icons';
+
+interface CourseCardProps {
+ course: Course;
+}
+
+export default function CourseCard({ course }: CourseCardProps) {
+ return (
+
+ }
+ >
+
+
+ {course.title}
+
+
{course.instructor}
+
+
+ {course.rating}
+
+
+
+
+ {course.enrollments} 人在学
+
+
+
+ {course.duration}
+
+
+
+ {course.category}
+ {course.level}
+
+
+
+ );
+}
diff --git a/apps/web/src/app/main/courses/components/CourseList.tsx b/apps/web/src/app/main/courses/components/CourseList.tsx
new file mode 100644
index 0000000..3d67b1c
--- /dev/null
+++ b/apps/web/src/app/main/courses/components/CourseList.tsx
@@ -0,0 +1,44 @@
+import { Pagination, Empty } from 'antd';
+import { Course } from '../mockData';
+import CourseCard from './CourseCard';
+
+interface CourseListProps {
+ courses: Course[];
+ total: number;
+ pageSize: number;
+ currentPage: number;
+ onPageChange: (page: number) => void;
+}
+
+export default function CourseList({
+ courses,
+ total,
+ pageSize,
+ currentPage,
+ onPageChange,
+}: CourseListProps) {
+ return (
+
+ {courses.length > 0 ? (
+ <>
+
+ {courses.map(course => (
+
+ ))}
+
+
+ >
+ ) : (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/app/main/courses/components/FilterSection.tsx b/apps/web/src/app/main/courses/components/FilterSection.tsx
new file mode 100644
index 0000000..93e0e6f
--- /dev/null
+++ b/apps/web/src/app/main/courses/components/FilterSection.tsx
@@ -0,0 +1,54 @@
+import { Checkbox, Divider, Radio, Space } from 'antd';
+import { categories, levels } from '../mockData';
+
+interface FilterSectionProps {
+ selectedCategory: string;
+ selectedLevel: string;
+ onCategoryChange: (category: string) => void;
+ onLevelChange: (level: string) => void;
+}
+
+export default function FilterSection({
+ selectedCategory,
+ selectedLevel,
+ onCategoryChange,
+ onLevelChange,
+}: FilterSectionProps) {
+ return (
+
+
+
课程分类
+ onCategoryChange(e.target.value)}
+ className="flex flex-col space-y-3"
+ >
+ 全部课程
+ {categories.map(category => (
+
+ {category}
+
+ ))}
+
+
+
+
+
+
+
难度等级
+ onLevelChange(e.target.value)}
+ className="flex flex-col space-y-3"
+ >
+ 全部难度
+ {levels.map(level => (
+
+ {level}
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/app/main/courses/mockData.ts b/apps/web/src/app/main/courses/mockData.ts
new file mode 100644
index 0000000..096d174
--- /dev/null
+++ b/apps/web/src/app/main/courses/mockData.ts
@@ -0,0 +1,44 @@
+export interface Course {
+ id: string;
+ title: string;
+ description: string;
+ instructor: string;
+ price: number;
+ originalPrice: number;
+ category: string;
+ level: string;
+ thumbnail: string;
+ rating: number;
+ enrollments: number;
+ duration: string;
+}
+
+export const categories = [
+ "计算机科学",
+ "数据科学",
+ "商业管理",
+ "人工智能",
+ "软件开发",
+ "网络安全",
+ "云计算",
+ "前端开发",
+ "后端开发",
+ "移动开发"
+];
+
+export const levels = ["入门", "初级", "中级", "高级"];
+
+export const mockCourses: Course[] = Array.from({ length: 50 }, (_, i) => ({
+ id: `course-${i + 1}`,
+ title: `${categories[i % categories.length]}课程 ${i + 1}`,
+ description: "本课程将带你深入了解该领域的核心概念和实践应用,通过实战项目提升你的专业技能。",
+ instructor: `讲师 ${i + 1}`,
+ price: Math.floor(Math.random() * 500 + 99),
+ originalPrice: Math.floor(Math.random() * 1000 + 299),
+ category: categories[i % categories.length],
+ level: levels[i % levels.length],
+ thumbnail: `/api/placeholder/280/160`,
+ rating: Number((Math.random() * 2 + 3).toFixed(1)),
+ enrollments: Math.floor(Math.random() * 10000),
+ duration: `${Math.floor(Math.random() * 20 + 10)}小时`
+}));
\ No newline at end of file
diff --git a/apps/web/src/app/main/courses/page.tsx b/apps/web/src/app/main/courses/page.tsx
new file mode 100644
index 0000000..f852e00
--- /dev/null
+++ b/apps/web/src/app/main/courses/page.tsx
@@ -0,0 +1,73 @@
+import { useState, useMemo } from 'react';
+import { mockCourses } from './mockData';
+import FilterSection from './components/FilterSection';
+import CourseList from './components/CourseList';
+
+export default function CoursesPage() {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [selectedCategory, setSelectedCategory] = useState('');
+ const [selectedLevel, setSelectedLevel] = useState('');
+ const pageSize = 12;
+
+ const filteredCourses = useMemo(() => {
+ return mockCourses.filter(course => {
+ const matchCategory = !selectedCategory || course.category === selectedCategory;
+ const matchLevel = !selectedLevel || course.level === selectedLevel;
+ return matchCategory && matchLevel;
+ });
+ }, [selectedCategory, selectedLevel]);
+
+ const paginatedCourses = useMemo(() => {
+ const startIndex = (currentPage - 1) * pageSize;
+ return filteredCourses.slice(startIndex, startIndex + pageSize);
+ }, [filteredCourses, currentPage]);
+
+ const handlePageChange = (page: number) => {
+ setCurrentPage(page);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ return (
+
+
+
+ {/* 左侧筛选区域 */}
+
+
+ {
+ setSelectedCategory(category);
+ setCurrentPage(1);
+ }}
+ onLevelChange={level => {
+ setSelectedLevel(level);
+ setCurrentPage(1);
+ }}
+ />
+
+
+
+ {/* 右侧课程列表区域 */}
+
+
+
+
+ 共找到 {filteredCourses.length} 门课程
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/app/main/home/components/CategorySection.tsx b/apps/web/src/app/main/home/components/CategorySection.tsx
new file mode 100644
index 0000000..638e429
--- /dev/null
+++ b/apps/web/src/app/main/home/components/CategorySection.tsx
@@ -0,0 +1,162 @@
+import React, { useState, useCallback } from 'react';
+import { Typography, Button } from 'antd';
+import { stringToColor } from '@nice/common';
+
+const { Title, Text } = Typography;
+
+interface CourseCategory {
+ name: string;
+ count: number;
+ description: string;
+}
+
+const courseCategories: CourseCategory[] = [
+ {
+ name: '计算机基础',
+ count: 120,
+ description: '计算机组成原理、操作系统、网络等基础知识'
+ },
+ {
+ name: '编程语言',
+ count: 85,
+ description: 'Python、Java、JavaScript等主流编程语言'
+ },
+ {
+ name: '人工智能',
+ count: 65,
+ description: '机器学习、深度学习、自然语言处理等前沿技术'
+ },
+ {
+ name: '数据科学',
+ count: 45,
+ description: '数据分析、数据可视化、商业智能等'
+ },
+ {
+ name: '云计算',
+ count: 38,
+ description: '云服务、容器化、微服务架构等'
+ },
+ {
+ name: '网络安全',
+ count: 42,
+ description: '网络安全基础、渗透测试、安全防护等'
+ }
+];
+
+const CategorySection = () => {
+ const [hoveredIndex, setHoveredIndex] = useState(null);
+ const [showAll, setShowAll] = useState(false);
+
+ const handleMouseEnter = useCallback((index: number) => {
+ setHoveredIndex(index);
+ }, []);
+
+ const handleMouseLeave = useCallback(() => {
+ setHoveredIndex(null);
+ }, []);
+
+ const displayedCategories = showAll
+ ? courseCategories
+ : courseCategories.slice(0, 8);
+
+ return (
+
+
+
+
+ 探索课程分类
+
+
+ 选择你感兴趣的方向,开启学习之旅
+
+
+
+ {displayedCategories.map((category, index) => {
+ const categoryColor = stringToColor(category.name);
+ const isHovered = hoveredIndex === index;
+
+ return (
+
handleMouseEnter(index)}
+ onMouseLeave={handleMouseLeave}
+ role="button"
+ tabIndex={0}
+ aria-label={`查看${category.name}课程类别`}
+ >
+
+
+
+
+
+
+
+ {category.name}
+
+
+ {category.count} 门课程
+
+
+
+ {category.description}
+
+
+ 了解更多
+
+ →
+
+
+
+
+ );
+ })}
+
+ {courseCategories.length > 8 && (
+
+
+
+ )}
+
+
+ );
+};
+
+export default CategorySection;
\ No newline at end of file
diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx
new file mode 100644
index 0000000..4818e00
--- /dev/null
+++ b/apps/web/src/app/main/home/components/CoursesSection.tsx
@@ -0,0 +1,206 @@
+import React, { useState, useMemo } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { Button, Card, Typography, Tag, Progress } from 'antd';
+import {
+ PlayCircleOutlined,
+ UserOutlined,
+ ClockCircleOutlined,
+ TeamOutlined,
+ StarOutlined,
+ ArrowRightOutlined,
+} from '@ant-design/icons';
+
+const { Title, Text } = Typography;
+
+interface Course {
+ id: number;
+ title: string;
+ instructor: string;
+ students: number;
+ rating: number;
+ level: string;
+ duration: string;
+ category: string;
+ progress: number;
+ thumbnail: string;
+}
+
+interface CoursesSectionProps {
+ title: string;
+ description: string;
+ courses: Course[];
+ initialVisibleCoursesCount?: number;
+}
+
+const CoursesSection: React.FC = ({
+ title,
+ description,
+ courses,
+ initialVisibleCoursesCount = 8,
+}) => {
+ const navigate = useNavigate();
+ const [selectedCategory, setSelectedCategory] = useState('全部');
+ const [visibleCourses, setVisibleCourses] = useState(initialVisibleCoursesCount);
+
+ const categories = useMemo(() => {
+ const allCategories = courses.map((course) => course.category);
+ return ['全部', ...Array.from(new Set(allCategories))];
+ }, [courses]);
+
+ const filteredCourses = useMemo(() => {
+ return selectedCategory === '全部'
+ ? courses
+ : courses.filter((course) => course.category === selectedCategory);
+ }, [selectedCategory, courses]);
+
+ const displayedCourses = filteredCourses.slice(0, visibleCourses);
+
+ return (
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+
+ {categories.map((category) => (
+ setSelectedCategory(category)}
+ className={`px-4 py-2 text-base cursor-pointer hover:scale-105 transform transition-all duration-300 ${selectedCategory === category
+ ? 'shadow-[0_2px_8px_-4px_rgba(59,130,246,0.5)]'
+ : 'hover:shadow-md'
+ }`}
+ >
+ {category}
+
+ ))}
+
+
+
+ {displayedCourses.map((course) => (
+
+
+
+
+ {course.progress > 0 && (
+
+ )}
+
+ }
+ >
+
+
+
+ {course.category}
+
+
+ {course.level}
+
+
+
+ {course.title}
+
+
+
+
+ {course.instructor}
+
+
+
+
+
+ {course.duration}
+
+
+
+ {course.students.toLocaleString()}
+
+
+
+ {course.rating}
+
+
+
+
+
+
+
+ ))}
+
+
+ {filteredCourses.length >= visibleCourses && (
+
+
+
+
navigate('/courses')}
+ className="cursor-pointer tracking-widest text-gray-500 hover:text-primary font-medium flex items-center gap-2 transition-all duration-300 ease-in-out"
+ >
+ 查看更多
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default CoursesSection;
diff --git a/apps/web/src/app/main/home/components/FeaturedTeachersSection.tsx b/apps/web/src/app/main/home/components/FeaturedTeachersSection.tsx
new file mode 100644
index 0000000..c880b07
--- /dev/null
+++ b/apps/web/src/app/main/home/components/FeaturedTeachersSection.tsx
@@ -0,0 +1,222 @@
+import React, { useRef } from 'react';
+import { Typography, Tag, Carousel } from 'antd';
+import { StarFilled, UserOutlined, ReadOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
+
+const { Title, Text } = Typography;
+
+interface Teacher {
+ name: string;
+ title: string;
+ avatar: string;
+ courses: number;
+ students: number;
+ rating: number;
+ description: string;
+}
+
+const featuredTeachers: Teacher[] = [
+ {
+ name: '张教授',
+ title: '资深前端开发专家',
+ avatar: '/images/teacher1.jpg',
+ courses: 12,
+ students: 25000,
+ rating: 4.9,
+ description: '前 BAT 高级工程师,10年+开发经验'
+ },
+ {
+ name: '李教授',
+ title: '算法与数据结构专家',
+ avatar: '/images/teacher2.jpg',
+ courses: 8,
+ students: 18000,
+ rating: 4.8,
+ description: '计算机博士,专注算法教育8年'
+ },
+ {
+ name: '王博士',
+ title: '人工智能研究员',
+ avatar: '/images/teacher3.jpg',
+ courses: 15,
+ students: 30000,
+ rating: 4.95,
+ description: '人工智能领域专家,曾主导多个大型AI项目'
+ },
+ {
+ name: '陈教授',
+ title: '云计算架构师',
+ avatar: '/images/teacher4.jpg',
+ courses: 10,
+ students: 22000,
+ rating: 4.85,
+ description: '知名云服务提供商技术总监,丰富的实战经验'
+ },
+ {
+ name: '郑老师',
+ title: '移动开发专家',
+ avatar: '/images/teacher5.jpg',
+ courses: 14,
+ students: 28000,
+ rating: 4.88,
+ description: '资深移动端开发者,著名互联网公司技术专家'
+ }
+];
+
+const generateGradientColors = (name: string) => {
+ // 优化的哈希函数
+ const hash = name.split('').reduce((acc, char, index) => {
+ return char.charCodeAt(0) + ((acc << 5) - acc) + index;
+ }, 0);
+
+ // 定义蓝色色相范围(210-240)
+ const blueHueStart = 210;
+ const blueHueRange = 30;
+
+ // 基础蓝色色相 - 将哈希值映射到蓝色范围内
+ const baseHue = blueHueStart + Math.abs(hash % blueHueRange);
+
+ // 生成第二个蓝色色相,保持在蓝色范围内
+ let secondHue = baseHue + 15; // 在基础色相的基础上略微偏移
+ if (secondHue > blueHueStart + blueHueRange) {
+ secondHue -= blueHueRange;
+ }
+
+ // 基于输入字符串的特征调整饱和度和亮度
+ const nameLength = name.length;
+ const saturation = Math.max(65, Math.min(85, 75 + (nameLength % 10))); // 65-85%范围
+ const lightness = Math.max(45, Math.min(65, 55 + (hash % 10))); // 45-65%范围
+
+ // 为第二个颜色稍微调整饱和度和亮度,创造层次感
+ const saturation2 = Math.max(60, saturation - 5);
+ const lightness2 = Math.min(70, lightness + 5);
+
+ return {
+ from: `hsl(${Math.round(baseHue)}, ${Math.round(saturation)}%, ${Math.round(lightness)}%)`,
+ to: `hsl(${Math.round(secondHue)}, ${Math.round(saturation2)}%, ${Math.round(lightness2)}%)`
+ };
+};
+
+const FeaturedTeachersSection: React.FC = () => {
+ const carouselRef = useRef(null);
+
+ const settings = {
+ dots: true,
+ infinite: true,
+ speed: 500,
+ slidesToShow: 4,
+ slidesToScroll: 1,
+ autoplay: true,
+ autoplaySpeed: 5000,
+ responsive: [
+ {
+ breakpoint: 1024,
+ settings: {
+ slidesToShow: 2,
+ slidesToScroll: 1
+ }
+ },
+ {
+ breakpoint: 640,
+ settings: {
+ slidesToShow: 1,
+ slidesToScroll: 1
+ }
+ }
+ ]
+ };
+
+ const TeacherCard = ({ teacher }: { teacher: Teacher }) => {
+ const gradientColors = generateGradientColors(teacher.name);
+ return (
+
+
+
+

+
+
+
+
+ {teacher.name}
+
+
+ {teacher.title}
+
+
+
+
+ {teacher.description}
+
+
+
+
+
+
+ {teacher.courses}
+
+
课程
+
+
+
+
+ {(teacher.students / 1000).toFixed(1)}k
+
+
学员
+
+
+
+
+ {teacher.rating}
+
+
评分
+
+
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+ 优秀讲师
+
+
+ 业界专家实战分享,传授独家经验
+
+
+
+
+
+ {featuredTeachers.map((teacher, index) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
+
+export default FeaturedTeachersSection;
diff --git a/apps/web/src/app/main/home/components/HeroSection.tsx b/apps/web/src/app/main/home/components/HeroSection.tsx
new file mode 100644
index 0000000..b577173
--- /dev/null
+++ b/apps/web/src/app/main/home/components/HeroSection.tsx
@@ -0,0 +1,157 @@
+import React, { useRef, useCallback } from 'react';
+import { Button, Carousel, Typography } from 'antd';
+import {
+ TeamOutlined,
+ BookOutlined,
+ StarOutlined,
+ ClockCircleOutlined,
+ LeftOutlined,
+ RightOutlined
+} from '@ant-design/icons';
+import type { CarouselRef } from 'antd/es/carousel';
+
+const { Title, Text } = Typography;
+
+interface CarouselItem {
+ title: string;
+ desc: string;
+ image: string;
+ action: string;
+ color: string;
+}
+
+interface PlatformStat {
+ icon: React.ReactNode;
+ value: string;
+ label: string;
+}
+
+const carouselItems: CarouselItem[] = [
+ {
+ title: '探索编程世界',
+ desc: '从零开始学习编程,开启你的技术之旅',
+ image: '/images/banner1.jpg',
+ action: '立即开始',
+ color: 'from-blue-600/90'
+ },
+ {
+ title: '人工智能课程',
+ desc: '掌握AI技术,引领未来发展',
+ image: '/images/banner2.jpg',
+ action: '了解更多',
+ color: 'from-purple-600/90'
+ }
+];
+
+const platformStats: PlatformStat[] = [
+ { icon: , value: '50,000+', label: '注册学员' },
+ { icon: , value: '1,000+', label: '精品课程' },
+ { icon: , value: '98%', label: '好评度' },
+ { icon: , value: '100万+', label: '学习时长' }
+];
+
+const HeroSection = () => {
+ const carouselRef = useRef(null);
+
+ const handlePrev = useCallback(() => {
+ carouselRef.current?.prev();
+ }, []);
+
+ const handleNext = useCallback(() => {
+ carouselRef.current?.next();
+ }, []);
+
+ return (
+
+
+
+ {carouselItems.map((item, index) => (
+
+
+
+
+
+ {/* Content Container */}
+
+
+
+ {item.title}
+
+
+ {item.desc}
+
+
+
+
+
+ ))}
+
+
+ {/* Navigation Buttons */}
+
+
+
+
+ {/* Stats Container */}
+
+
+ {platformStats.map((stat, index) => (
+
+
+ {stat.icon}
+
+
+ {stat.value}
+
+
{stat.label}
+
+ ))}
+
+
+
+ );
+};
+
+export default HeroSection;
\ No newline at end of file
diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx
index b749462..2d645a0 100644
--- a/apps/web/src/app/main/home/page.tsx
+++ b/apps/web/src/app/main/home/page.tsx
@@ -1,25 +1,125 @@
-import GraphEditor from "@web/src/components/common/editor/graph/GraphEditor";
-import FileUploader from "@web/src/components/common/uploader/FileUploader";
+import HeroSection from './components/HeroSection';
+import CategorySection from './components/CategorySection';
+import CoursesSection from './components/CoursesSection';
+import FeaturedTeachersSection from './components/FeaturedTeachersSection';
-import React, { useState, useCallback } from "react";
-import * as tus from "tus-js-client";
-interface TusUploadProps {
- onSuccess?: (response: any) => void;
- onError?: (error: Error) => void;
-}
-const HomePage: React.FC = ({ onSuccess, onError }) => {
- return (
-
-
-
-
-
- {/*
-
-
*/}
- {/*
*/}
-
- );
+const HomePage = () => {
+ const mockCourses = [
+ {
+ id: 1,
+ title: 'Python 零基础入门',
+ instructor: '张教授',
+ students: 12000,
+ rating: 4.8,
+ level: '入门',
+ duration: '36小时',
+ category: '编程语言',
+ progress: 0,
+ thumbnail: '/images/course1.jpg',
+ },
+ {
+ id: 2,
+ title: '数据结构与算法',
+ instructor: '李教授',
+ students: 8500,
+ rating: 4.9,
+ level: '进阶',
+ duration: '48小时',
+ category: '计算机基础',
+ progress: 35,
+ thumbnail: '/images/course2.jpg',
+ },
+ {
+ id: 3,
+ title: '前端开发实战',
+ instructor: '王教授',
+ students: 10000,
+ rating: 4.7,
+ level: '中级',
+ duration: '42小时',
+ category: '前端开发',
+ progress: 68,
+ thumbnail: '/images/course3.jpg',
+ },
+ {
+ id: 4,
+ title: 'Java企业级开发',
+ instructor: '刘教授',
+ students: 9500,
+ rating: 4.6,
+ level: '高级',
+ duration: '56小时',
+ category: '编程语言',
+ progress: 0,
+ thumbnail: '/images/course4.jpg',
+ },
+ {
+ id: 5,
+ title: '人工智能基础',
+ instructor: '陈教授',
+ students: 11000,
+ rating: 4.9,
+ level: '中级',
+ duration: '45小时',
+ category: '人工智能',
+ progress: 20,
+ thumbnail: '/images/course5.jpg',
+ },
+ {
+ id: 6,
+ title: '大数据分析',
+ instructor: '赵教授',
+ students: 8000,
+ rating: 4.8,
+ level: '进阶',
+ duration: '50小时',
+ category: '数据科学',
+ progress: 45,
+ thumbnail: '/images/course6.jpg',
+ },
+ {
+ id: 7,
+ title: '云计算实践',
+ instructor: '孙教授',
+ students: 7500,
+ rating: 4.7,
+ level: '高级',
+ duration: '48小时',
+ category: '云计算',
+ progress: 15,
+ thumbnail: '/images/course7.jpg',
+ },
+ {
+ id: 8,
+ title: '移动应用开发',
+ instructor: '周教授',
+ students: 9000,
+ rating: 4.8,
+ level: '中级',
+ duration: '40小时',
+ category: '移动开发',
+ progress: 0,
+ thumbnail: '/images/course8.jpg',
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+ );
};
-export default HomePage;
+export default HomePage;
\ No newline at end of file
diff --git a/apps/web/src/app/main/layout/MainFooter.tsx b/apps/web/src/app/main/layout/MainFooter.tsx
new file mode 100644
index 0000000..1356335
--- /dev/null
+++ b/apps/web/src/app/main/layout/MainFooter.tsx
@@ -0,0 +1,68 @@
+import { CloudOutlined, FileSearchOutlined, HomeOutlined, MailOutlined, PhoneOutlined } from '@ant-design/icons';
+import { Layout, Typography } from 'antd';
+export function MainFooter() {
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/main/layout/MainHeader.tsx b/apps/web/src/app/main/layout/MainHeader.tsx
new file mode 100644
index 0000000..d7d089b
--- /dev/null
+++ b/apps/web/src/app/main/layout/MainHeader.tsx
@@ -0,0 +1,65 @@
+import { useState } from 'react';
+import { Input, Layout, Avatar, Button, Dropdown } from 'antd';
+import { SearchOutlined, UserOutlined } from '@ant-design/icons';
+import { useAuth } from '@web/src/providers/auth-provider';
+import { useNavigate } from 'react-router-dom';
+import { UserMenu } from './UserMenu';
+import { NavigationMenu } from './NavigationMenu';
+
+const { Header } = Layout;
+
+export function MainHeader() {
+ const [searchValue, setSearchValue] = useState('');
+ const { isAuthenticated, user } = useAuth();
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
navigate('/')}
+ className="text-2xl font-bold bg-gradient-to-r from-primary-600 via-primary-500 to-primary-400 bg-clip-text text-transparent hover:scale-105 transition-transform cursor-pointer"
+ >
+ 烽火慕课
+
+
+
+
+
+ }
+ placeholder="搜索课程"
+ className="w-72 rounded-full"
+ value={searchValue}
+ onChange={(e) => setSearchValue(e.target.value)}
+ />
+
+ {isAuthenticated ? (
+
}
+ trigger={['click']}
+ placement="bottomRight"
+ >
+
+ {(user?.showname || user?.username || '')[0]?.toUpperCase()}
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/app/main/layout/MainLayout.tsx b/apps/web/src/app/main/layout/MainLayout.tsx
new file mode 100644
index 0000000..8ec4204
--- /dev/null
+++ b/apps/web/src/app/main/layout/MainLayout.tsx
@@ -0,0 +1,18 @@
+import { Layout } from 'antd';
+import { Outlet } from 'react-router-dom';
+import { MainHeader } from './MainHeader';
+import { MainFooter } from './MainFooter';
+
+const { Content } = Layout;
+
+export function MainLayout() {
+ return (
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/app/main/layout/NavigationMenu.tsx b/apps/web/src/app/main/layout/NavigationMenu.tsx
new file mode 100644
index 0000000..b1a4fb6
--- /dev/null
+++ b/apps/web/src/app/main/layout/NavigationMenu.tsx
@@ -0,0 +1,31 @@
+import { Menu } from 'antd';
+import { useNavigate, useLocation } from 'react-router-dom';
+
+const menuItems = [
+ { key: 'home', path: '/', label: '首页' },
+ { key: 'courses', path: '/courses', label: '全部课程' },
+ { key: 'paths', path: '/paths', label: '学习路径' }
+];
+
+export const NavigationMenu = () => {
+ const navigate = useNavigate();
+ const { pathname } = useLocation();
+ const selectedKey = menuItems.find(item => item.path === pathname)?.key || '';
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/apps/web/src/app/main/layout/UserMenu.tsx b/apps/web/src/app/main/layout/UserMenu.tsx
new file mode 100644
index 0000000..97396fd
--- /dev/null
+++ b/apps/web/src/app/main/layout/UserMenu.tsx
@@ -0,0 +1,49 @@
+import { Avatar, Menu, Dropdown } from 'antd';
+import { LogoutOutlined, SettingOutlined } from '@ant-design/icons';
+import { useAuth } from '@web/src/providers/auth-provider';
+import { useNavigate } from 'react-router-dom';
+
+export const UserMenu = () => {
+ const { isAuthenticated, logout, user } = useAuth();
+ const navigate = useNavigate();
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/apps/web/src/app/main/paths/page.tsx b/apps/web/src/app/main/paths/page.tsx
new file mode 100644
index 0000000..0b4edfd
--- /dev/null
+++ b/apps/web/src/app/main/paths/page.tsx
@@ -0,0 +1,3 @@
+export default function PathsPage() {
+ return <>paths>
+}
\ No newline at end of file
diff --git a/apps/web/src/app/main/self/courses/page.tsx b/apps/web/src/app/main/self/courses/page.tsx
new file mode 100644
index 0000000..36b358f
--- /dev/null
+++ b/apps/web/src/app/main/self/courses/page.tsx
@@ -0,0 +1,7 @@
+export default function MyCoursePage() {
+ return (
+
+ My Course Page
+
+ )
+}
\ No newline at end of file
diff --git a/apps/web/src/app/main/self/profiles/page.tsx b/apps/web/src/app/main/self/profiles/page.tsx
new file mode 100644
index 0000000..70ac75b
--- /dev/null
+++ b/apps/web/src/app/main/self/profiles/page.tsx
@@ -0,0 +1,3 @@
+export default function ProfilesPage() {
+ return <>Profiles>
+}
\ No newline at end of file
diff --git a/apps/web/src/components/layout/main/MainLayout.tsx b/apps/web/src/components/layout/main/MainLayout.tsx
deleted file mode 100644
index 7134d37..0000000
--- a/apps/web/src/components/layout/main/MainLayout.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { useState, useEffect, useRef, ReactNode } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { TopNavBar } from '@web/src/components/layout/main/top-nav-bar';
-import { navItems, notificationItems } from '@web/src/components/layout/main/nav-data';
-import { Sidebar } from '@web/src/components/layout/main/side-bar';
-import { Outlet } from 'react-router-dom';
-
-
-export function MainLayout() {
- const [sidebarOpen, setSidebarOpen] = useState(true);
- const [notifications, setNotifications] = useState(3);
- const [recentSearches] = useState([
- 'React Fundamentals',
- 'TypeScript Advanced',
- 'Tailwind CSS Projects',
- ]);
-
- return (
-
-
-
-
- {sidebarOpen && }
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/apps/web/src/components/layout/main/nav-data.tsx b/apps/web/src/components/layout/main/nav-data.tsx
deleted file mode 100644
index 1f2bea1..0000000
--- a/apps/web/src/components/layout/main/nav-data.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { NavItem } from '@nice/client';
-import {
- HomeIcon,
- BookOpenIcon,
- UserGroupIcon,
- Cog6ToothIcon,
- BellIcon,
- HeartIcon,
- AcademicCapIcon,
- UsersIcon,
- PresentationChartBarIcon
-} from '@heroicons/react/24/outline';
-
-export const navItems: NavItem[] = [
- { icon: , label: '探索知识', path: '/' },
- { icon: , label: '我的学习', path: '/courses/student' },
- { icon: , label: '我的授课', path: '/courses/instructor' },
- { icon: , label: '学习社区', path: '/community' },
- { icon: , label: '应用设置', path: '/settings' },
-];
-export const notificationItems = [
- {
- icon: ,
- title: "New Course Available",
- description: "Advanced TypeScript Programming is now available",
- time: "2 hours ago",
- isUnread: true,
- },
- {
- icon: ,
- title: "Course Recommendation",
- description: "Based on your interests: React Native Development",
- time: "1 day ago",
- isUnread: true,
- },
- {
- icon: ,
- title: "Certificate Ready",
- description: "Your React Fundamentals certificate is ready to download",
- time: "2 days ago",
- isUnread: true,
- },
-];
\ No newline at end of file
diff --git a/apps/web/src/components/layout/main/notifications-dropdown.tsx b/apps/web/src/components/layout/main/notifications-dropdown.tsx
deleted file mode 100644
index 439fa71..0000000
--- a/apps/web/src/components/layout/main/notifications-dropdown.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { useState, useRef, useEffect } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { NotificationsPanel } from './notifications-panel';
-import { BellIcon } from '@heroicons/react/24/outline';
-import { useClickOutside } from '@web/src/hooks/useClickOutside';
-
-interface NotificationsDropdownProps {
- notifications: number;
- notificationItems: Array;
-}
-
-export function NotificationsDropdown({ notifications, notificationItems }: NotificationsDropdownProps) {
- const [showNotifications, setShowNotifications] = useState(false);
- const notificationRef = useRef(null);
- useClickOutside(notificationRef, () => setShowNotifications(false));
- return (
-
-
setShowNotifications(!showNotifications)}
- >
-
-
- {notifications > 0 && (
-
- {notifications}
-
- )}
-
-
-
- {showNotifications && (
-
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/apps/web/src/components/layout/main/notifications-panel.tsx b/apps/web/src/components/layout/main/notifications-panel.tsx
deleted file mode 100644
index 43ef2b4..0000000
--- a/apps/web/src/components/layout/main/notifications-panel.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { ClockIcon } from '@heroicons/react/24/outline';
-import { motion } from 'framer-motion';
-
-interface NotificationsPanelProps {
- notificationItems: Array<{
- icon: React.ReactNode;
- title: string;
- description: string;
- time: string;
- isUnread: boolean;
- }>;
-}
-
-export function NotificationsPanel({ notificationItems }: NotificationsPanelProps) {
- return (
-
-
-
-
Notifications
-
- Mark all as read
-
-
-
-
-
- {notificationItems.map((item, index) => (
-
-
-
- {item.icon}
-
-
-
{item.title}
-
{item.description}
-
-
- {item.time}
-
-
-
-
- ))}
-
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/apps/web/src/components/layout/main/search-bar.tsx b/apps/web/src/components/layout/main/search-bar.tsx
deleted file mode 100644
index a965660..0000000
--- a/apps/web/src/components/layout/main/search-bar.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useState, useRef, useEffect } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { SearchDropdown } from './search-dropdown';
-import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline';
-import { useClickOutside } from '@web/src/hooks/useClickOutside';
-
-interface SearchBarProps {
- recentSearches: string[];
-}
-
-export function SearchBar({ recentSearches }: SearchBarProps) {
- const [searchFocused, setSearchFocused] = useState(false);
- const [searchQuery, setSearchQuery] = useState('');
- const searchRef = useRef(null);
- useClickOutside(searchRef, () => setSearchFocused(false))
- return (
-
-
-
- setSearchFocused(true)}
- value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
- />
- {searchQuery && (
- setSearchQuery('')}
- >
-
-
- )}
-
-
-
- );
-}
\ No newline at end of file
diff --git a/apps/web/src/components/layout/main/search-dropdown.tsx b/apps/web/src/components/layout/main/search-dropdown.tsx
deleted file mode 100644
index 60ed49f..0000000
--- a/apps/web/src/components/layout/main/search-dropdown.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
-import { motion } from 'framer-motion';
-
-interface SearchDropdownProps {
- searchFocused: boolean;
- searchQuery: string;
- recentSearches: string[];
- setSearchQuery: (query: string) => void;
-}
-
-export function SearchDropdown({
- searchFocused,
- searchQuery,
- recentSearches,
- setSearchQuery
-}: SearchDropdownProps) {
- if (!searchFocused) return null;
-
- return (
-
-
-
Recent Searches
-
- {recentSearches.map((search, index) => (
- setSearchQuery(search)}
- >
-
- {search}
-
- ))}
-
-
- {searchQuery && (
-
-
-
-
- Search for "{searchQuery}"
-
-
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/apps/web/src/components/layout/main/side-bar.tsx b/apps/web/src/components/layout/main/side-bar.tsx
deleted file mode 100644
index e85cfcf..0000000
--- a/apps/web/src/components/layout/main/side-bar.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { motion } from 'framer-motion';
-import { useNavigate, useLocation } from 'react-router-dom';
-import { NavItem } from '@nice/client';
-
-interface SidebarProps {
- navItems: Array;
-}
-
-export function Sidebar({ navItems }: SidebarProps) {
- const navigate = useNavigate();
- const location = useLocation();
- return (
-
-
- {navItems.map((item, index) => {
- const isActive = location.pathname === item.path;
- return (
- {
- navigate(item.path)
- }}
- className={`flex items-center gap-3 w-full p-3 rounded-lg transition-colors
- ${isActive
- ? 'bg-blue-50 text-blue-600'
- : 'text-gray-700 hover:bg-blue-50 hover:text-blue-600'
- }`}
- >
- {item.icon}
- {item.label}
-
- );
- })}
-
-
- );
-}
\ No newline at end of file
diff --git a/apps/web/src/components/layout/main/top-nav-bar.tsx b/apps/web/src/components/layout/main/top-nav-bar.tsx
deleted file mode 100644
index 0d5aae6..0000000
--- a/apps/web/src/components/layout/main/top-nav-bar.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-
-import { NotificationsDropdown } from './notifications-dropdown';
-import { SearchBar } from './search-bar';
-import { UserMenuDropdown } from './usermenu-dropdown';
-import { Bars3Icon } from '@heroicons/react/24/outline';
-
-interface TopNavBarProps {
- sidebarOpen: boolean;
- setSidebarOpen: (open: boolean) => void;
- notifications: number;
- notificationItems: Array;
- recentSearches: string[];
-}
-export function TopNavBar({
- sidebarOpen,
- setSidebarOpen,
- notifications,
- notificationItems,
- recentSearches
-}: TopNavBarProps) {
- return (
-
- );
-}
\ No newline at end of file
diff --git a/apps/web/src/components/layout/main/usermenu-dropdown.tsx b/apps/web/src/components/layout/main/usermenu-dropdown.tsx
deleted file mode 100644
index 846a043..0000000
--- a/apps/web/src/components/layout/main/usermenu-dropdown.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import { useState, useRef, useEffect } from 'react';
-import { motion, AnimatePresence } from 'framer-motion';
-import { ArrowLeftStartOnRectangleIcon, Cog6ToothIcon, QuestionMarkCircleIcon, UserCircleIcon } from '@heroicons/react/24/outline';
-import { useAuth } from '@web/src/providers/auth-provider';
-import { Avatar } from '../../presentation/user/Avatar';
-import { useClickOutside } from '@web/src/hooks/useClickOutside';
-
-export function UserMenuDropdown() {
- const [showMenu, setShowMenu] = useState(false);
- const menuRef = useRef(null);
- const { user, logout } = useAuth()
- useClickOutside(menuRef, () => setShowMenu(false));
- const menuItems = [
- { icon: , label: '个人信息', action: () => { } },
- { icon: , label: '设置', action: () => { } },
- { icon: , label: '帮助', action: () => { } },
- { icon: , label: '注销', action: () => { logout() } },
- ];
-
- return (
-
-
setShowMenu(!showMenu)}
- className="w-10 h-10" // 移除了边框相关的类
- >
-
-
-
-
- {showMenu && (
-
-
-
{user?.showname}
-
{user?.username}
-
-
-
- {menuItems.map((item, index) => (
-
- {item.icon}
- {item.label}
-
- ))}
-
-
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
index d623ec5..95e81a7 100755
--- a/apps/web/src/index.css
+++ b/apps/web/src/index.css
@@ -16,7 +16,7 @@
}
.ag-custom-dragging-class {
- @apply border-b-2 border-primaryHover;
+ @apply border-b-2 border-blue-200;
}
.ant-popover-inner {
@@ -71,7 +71,7 @@
border-radius: 10px;
border: 2px solid #f0f0f0;
- @apply hover:bg-primaryHover transition-all bg-gray-400 ease-in-out rounded-full;
+ @apply hover:bg-blue-200 transition-all bg-gray-400 ease-in-out rounded-full;
}
/* 鼠标悬停在滚动块上 */
@@ -123,4 +123,4 @@
.custom-table .ant-table-tbody>tr:last-child>td {
border-bottom: none;
/* 去除最后一行的底部边框 */
-}
+}
\ No newline at end of file
diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx
index 30bcd56..f55410b 100755
--- a/apps/web/src/routes/index.tsx
+++ b/apps/web/src/routes/index.tsx
@@ -13,7 +13,6 @@ import RoleAdminPage from "../app/admin/role/page";
import WithAuth from "../components/utils/with-auth";
import LoginPage from "../app/login";
import BaseSettingPage from "../app/admin/base-setting/page";
-import { MainLayout } from "../components/layout/main/MainLayout";
import StudentCoursesPage from "../app/main/courses/student/page";
import InstructorCoursesPage from "../app/main/courses/instructor/page";
import HomePage from "../app/main/home/page";
@@ -23,6 +22,8 @@ import CourseContentForm from "../components/models/course/editor/form/CourseCon
import { CourseGoalForm } from "../components/models/course/editor/form/CourseGoalForm";
import CourseSettingForm from "../components/models/course/editor/form/CourseSettingForm";
import CourseEditorLayout from "../components/models/course/editor/layout/CourseEditorLayout";
+import { MainLayout } from "../app/main/layout/MainLayout";
+import CoursesPage from "../app/main/courses/page";
interface CustomIndexRouteObject extends IndexRouteObject {
name?: string;
@@ -61,6 +62,16 @@ export const routes: CustomRouteObject[] = [
index: true,
element: ,
},
+ {
+ path: "courses",
+ element:
+ },
+ {
+ path: "my-courses"
+ },
+ {
+ path: "profiles"
+ },
{
path: "courses",
children: [
diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js
deleted file mode 100755
index 92cbb80..0000000
--- a/apps/web/tailwind.config.js
+++ /dev/null
@@ -1,53 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: [
- "./index.html",
- "./src/**/*.{js,ts,jsx,tsx}",
- ],
- theme: {
- extend: {
- colors: {
- primary: "var(--color-primary)",
- primaryActive: "var(--color-primary-active)",
- primaryHover: "var(--color-primary-hover)",
- error: "var(--color-error)",
- warning: "var(--color-warning)",
- info: "var(--color-info)",
- success: "var(--color-success)",
- link: "var(--color-link)",
- highlight: "var(--color-highlight)",
- },
- backgroundColor: {
- layout: "var(--color-bg-layout)",
- mask: "var(--color-bg-mask)",
- container: "var(--color-bg-container)",
- textHover: "var(--color-bg-text-hover)",
- primary: "var(--color-bg-primary)",
- error: "var(--color-error-bg)",
- warning: "var(--color-warning-bg)",
- info: "var(--color-info-bg)",
- success: "var(--color-success-bg)",
- },
- textColor: {
- default: "var(--color-text)",
- quaternary: "var(--color-text-quaternary)",
- placeholder: "var(--color-text-placeholder)",
- description: "var(--color-text-description)",
- secondary: "var(--color-text-secondary)",
- tertiary: "var(--color-text-tertiary)",
- primary: "var(--color-text-primary)",
- heading: "var(--color-text-heading)",
- label: "var(--color-text-label)",
- lightSolid: "var(--color-text-lightsolid)"
- },
- borderColor: {
- default: "var(--color-border)",
- },
- boxShadow: {
- elegant: '0 3px 6px -2px rgba(46, 117, 182, 0.10), 0 2px 4px -1px rgba(46, 117, 182, 0.05)'
-
- }
- },
- },
- plugins: [],
-}
diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts
new file mode 100755
index 0000000..e8df47b
--- /dev/null
+++ b/apps/web/tailwind.config.ts
@@ -0,0 +1,2 @@
+import { NiceTailwindConfig } from "@nice/config"
+export default NiceTailwindConfig
\ No newline at end of file
diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts
index 083b229..f2c377a 100755
--- a/packages/common/src/utils.ts
+++ b/packages/common/src/utils.ts
@@ -79,32 +79,28 @@ interface MappingConfig {
hasChildrenField?: string; // Optional, in case the structure has nested items
childrenField?: string;
}
-export function stringToColor(str: string): string {
- let hash = 0;
+/**
+ * 将字符串转换为优雅的颜色值
+ * @param str - 输入字符串
+ * @param saturation - 饱和度(0-100),默认为75
+ * @param lightness - 亮度(0-100),默认为65
+ * @returns 返回HSL格式的颜色字符串
+ */
+export function stringToColor(str: string, saturation: number = 75, lightness: number = 65): string {
+ let hash = 0;
+
+ // 使用字符串生成哈希值
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
-
- let color = '#';
- for (let i = 0; i < 3; i++) {
- let value = (hash >> (i * 8)) & 0xFF;
-
- // Adjusting the value to avoid dark, gray or too light colors
- if (value < 100) {
- value += 100; // Avoids too dark colors
- }
- if (value > 200) {
- value -= 55; // Avoids too light colors
- }
-
- // Ensure the color is not gray by adjusting R, G, B individually
- value = Math.floor((value + 255) / 2);
-
- color += ('00' + value.toString(16)).slice(-2);
- }
-
- return color;
+
+ // 将哈希值转换为0-360的色相值
+ const hue = Math.abs(hash % 360);
+
+ // 使用HSL颜色空间生成颜色
+ // 固定饱和度和亮度以确保颜色的优雅性
+ return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}
diff --git a/packages/config/package.json b/packages/config/package.json
new file mode 100644
index 0000000..744ac15
--- /dev/null
+++ b/packages/config/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@nice/config",
+ "version": "1.0.0",
+ "main": "./dist/index.js",
+ "module": "./dist/index.mjs",
+ "types": "./dist/index.d.ts",
+ "private": true,
+ "scripts": {
+ "build": "tsup",
+ "dev": "tsup --watch",
+ "clean": "rimraf dist",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@nice/utils": "workspace:^",
+ "color": "^4.2.3",
+ "nanoid": "^5.0.9"
+ },
+ "peerDependencies": {
+ "react": "18.2.0",
+ "react-dom": "18.2.0"
+ },
+ "devDependencies": {
+ "@types/color": "^4.2.0",
+ "@types/dagre": "^0.7.52",
+ "@types/node": "^20.3.1",
+ "@types/react": "18.2.38",
+ "@types/react-dom": "18.2.15",
+ "concurrently": "^8.0.0",
+ "rimraf": "^6.0.1",
+ "ts-node": "^10.9.1",
+ "tsup": "^8.3.5",
+ "typescript": "^5.5.4"
+ }
+}
\ No newline at end of file
diff --git a/packages/config/src/colors.ts b/packages/config/src/colors.ts
new file mode 100644
index 0000000..50d976a
--- /dev/null
+++ b/packages/config/src/colors.ts
@@ -0,0 +1,24 @@
+import Color from "color";
+import { ColorScale } from "./types";
+
+export function generateColorScale(baseColor: string): ColorScale {
+ const color = Color(baseColor);
+ const steps = [-0.4, -0.32, -0.24, -0.16, -0.08, 0, 0.08, 0.16, 0.24, 0.32, 0.4];
+ const keys = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
+
+ const scale = Object.fromEntries(
+ keys.map((key, index) => [
+ key,
+ color.lighten(-steps[index]).rgb().string()
+ ])
+ ) as ColorScale;
+
+ return {
+ ...scale,
+ DEFAULT: scale[500]
+ };
+}
+export function withAlpha(color: string, alpha: number): string {
+
+ return Color(color).alpha(alpha).toString();
+}
diff --git a/packages/config/src/constants.ts b/packages/config/src/constants.ts
new file mode 100644
index 0000000..f063003
--- /dev/null
+++ b/packages/config/src/constants.ts
@@ -0,0 +1,19 @@
+import { generateTheme} from "./generator";
+import { ThemeSeed } from "./types";
+
+// 添加默认的主题配置
+export const USAFSeed: ThemeSeed = {
+ colors: {
+ primary: '#003087', // 深蓝色
+ secondary: '#71767C', // 灰色
+ neutral: '#4A4A4A', // 中性灰色
+ success: '#287233', // 绿色
+ warning: '#FF9F1C', // 警告橙色
+ error: '#AF1E2D', // 红色
+ info: '#00538E', // 信息蓝色
+
+ },
+
+ isDark: false
+};
+export const defaultTheme = generateTheme(USAFSeed)
\ No newline at end of file
diff --git a/packages/config/src/context.tsx b/packages/config/src/context.tsx
new file mode 100644
index 0000000..872a160
--- /dev/null
+++ b/packages/config/src/context.tsx
@@ -0,0 +1,50 @@
+import React, { createContext, useContext, useMemo, useState } from 'react';
+import type { ThemeSeed, ThemeToken } from './types';
+import { USAFSeed } from './constants';
+import { createTailwindTheme, injectThemeVariables } from './styles';
+import { generateTheme } from './generator';
+interface ThemeContextValue {
+ token: ThemeToken
+ setTheme: (options: ThemeSeed) => void;
+ toggleDarkMode: () => void;
+}
+const ThemeContext = createContext(null);
+export function ThemeProvider({
+ children,
+ seed = USAFSeed,
+}: {
+ children: React.ReactNode;
+ seed?: ThemeSeed;
+}) {
+ const [themeSeed, setThemeSeed] = useState(seed);
+ const token = useMemo(() => {
+
+ const result = generateTheme(themeSeed)
+ console.log(createTailwindTheme(result))
+
+ injectThemeVariables(result)
+ return result.token;
+ }, [themeSeed]);
+
+ const contextValue = useMemo(
+ () => ({
+ token,
+ setTheme: setThemeSeed,
+ toggleDarkMode: () =>
+ setThemeSeed((prev) => ({ ...prev, isDark: !prev.isDark })),
+ }),
+ [token]
+ );
+
+ return (
+ {children}
+ );
+}
+
+export function useTheme() {
+ const context = useContext(ThemeContext);
+ if (!context) {
+ throw new Error('useTheme must be used within a ThemeProvider');
+ }
+ return context;
+}
diff --git a/packages/config/src/generator.ts b/packages/config/src/generator.ts
new file mode 100644
index 0000000..0777d7b
--- /dev/null
+++ b/packages/config/src/generator.ts
@@ -0,0 +1,99 @@
+import { Theme, ThemeColors, ThemeSemantics, ThemeSeed, ThemeToken } from './types';
+import { withAlpha, generateColorScale } from './colors';
+import { darkMode } from './utils';
+export function generateThemeColors(seed: ThemeSeed['colors']): ThemeColors {
+ const defaultColors = {
+ success: '#22c55e',
+ warning: '#f59e0b',
+ error: '#ef4444',
+ info: '#3b82f6'
+ };
+ const colors = { ...defaultColors, ...seed };
+ return Object.fromEntries(
+ Object.entries(colors).map(([key, value]) => [
+ key,
+ generateColorScale(value)
+ ])
+ ) as ThemeColors;
+}
+
+export function generateSemantics(colors: ThemeColors, isDark: boolean): ThemeSemantics {
+ const neutral = colors.neutral;
+ const primary = colors.primary;
+
+ const statusColors = {
+ success: colors.success,
+ warning: colors.warning,
+ error: colors.error,
+ info: colors.info
+ };
+ return {
+ colors,
+ textColor: {
+ DEFAULT: darkMode(isDark, neutral[100], neutral[900]),
+ primary: darkMode(isDark, neutral[100], neutral[900]),
+ secondary: darkMode(isDark, neutral[300], neutral[700]),
+ tertiary: darkMode(isDark, neutral[400], neutral[600]),
+ disabled: darkMode(isDark, neutral[500], neutral[400]),
+ inverse: darkMode(isDark, neutral[900], neutral[100]),
+ success: statusColors.success[darkMode(isDark, 400, 600)],
+ warning: statusColors.warning[darkMode(isDark, 400, 600)],
+ error: statusColors.error[darkMode(isDark, 400, 600)],
+ info: statusColors.info[darkMode(isDark, 400, 600)],
+ link: primary[darkMode(isDark, 400, 600)],
+ linkHover: primary[darkMode(isDark, 300, 700)],
+ placeholder: darkMode(isDark, neutral[500], neutral[400]),
+ highlight: primary[darkMode(isDark, 300, 700)]
+ },
+
+ backgroundColor: {
+ DEFAULT: darkMode(isDark, neutral[900], neutral[50]),
+ paper: darkMode(isDark, neutral[800], neutral[100]),
+ subtle: darkMode(isDark, neutral[700], neutral[200]),
+ inverse: darkMode(isDark, neutral[50], neutral[900]),
+ success: withAlpha(statusColors.success[darkMode(isDark, 900, 50)], 0.12),
+ warning: withAlpha(statusColors.warning[darkMode(isDark, 900, 50)], 0.12),
+ error: withAlpha(statusColors.error[darkMode(isDark, 900, 50)], 0.12),
+ info: withAlpha(statusColors.info[darkMode(isDark, 900, 50)], 0.12),
+ primaryHover: withAlpha(primary[darkMode(isDark, 800, 100)], 0.08),
+ primaryActive: withAlpha(primary[darkMode(isDark, 700, 200)], 0.12),
+ primaryDisabled: withAlpha(neutral[darkMode(isDark, 700, 200)], 0.12),
+ secondaryHover: withAlpha(neutral[darkMode(isDark, 800, 100)], 0.08),
+ secondaryActive: withAlpha(neutral[darkMode(isDark, 700, 200)], 0.12),
+ secondaryDisabled: withAlpha(neutral[darkMode(isDark, 700, 200)], 0.12),
+ selected: withAlpha(primary[darkMode(isDark, 900, 50)], 0.16),
+ hover: withAlpha(neutral[darkMode(isDark, 800, 100)], 0.08),
+ focused: withAlpha(primary[darkMode(isDark, 900, 50)], 0.12),
+ disabled: withAlpha(neutral[darkMode(isDark, 700, 200)], 0.12),
+ overlay: withAlpha(neutral[900], 0.5)
+ },
+
+ border: {
+ DEFAULT: darkMode(isDark, neutral[600], neutral[300]),
+ subtle: darkMode(isDark, neutral[700], neutral[200]),
+ strong: darkMode(isDark, neutral[500], neutral[400]),
+ focus: primary[500],
+ inverse: darkMode(isDark, neutral[300], neutral[600]),
+ success: statusColors.success[darkMode(isDark, 500, 500)],
+ warning: statusColors.warning[darkMode(isDark, 500, 500)],
+ error: statusColors.error[darkMode(isDark, 500, 500)],
+ info: statusColors.info[darkMode(isDark, 500, 500)],
+ disabled: darkMode(isDark, neutral[600], neutral[300])
+ }
+ };
+}
+
+
+export function generateTheme(seed: ThemeSeed): Theme {
+ const isDark = seed.isDark ?? false;
+ const colors = generateThemeColors(seed.colors);
+ const semantics = generateSemantics(colors, isDark);
+
+ return {
+ token: {
+ ...colors,
+ ...semantics
+ },
+ isDark
+ };
+}
diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts
new file mode 100644
index 0000000..69ce129
--- /dev/null
+++ b/packages/config/src/index.ts
@@ -0,0 +1,6 @@
+export * from "./context"
+export * from "./types"
+export * from "./utils"
+export * from "./styles"
+export * from "./constants"
+export * from "./tailwind"
\ No newline at end of file
diff --git a/packages/config/src/styles.ts b/packages/config/src/styles.ts
new file mode 100644
index 0000000..152bcea
--- /dev/null
+++ b/packages/config/src/styles.ts
@@ -0,0 +1,89 @@
+import { Theme, ThemeToken } from './types'
+import { flattenObject, toKebabCase } from './utils'
+import type { Config } from 'tailwindcss'
+
+const PREFIX = '--nice'
+
+/**
+ * 生成CSS变量键名
+ */
+function createCssVariableName(path: string[]): string {
+ return `${PREFIX}-${path.map(p => toKebabCase(p.toLowerCase())).join('-')}`
+}
+
+/**
+ * 将主题对象转换为CSS变量对象
+ */
+export function themeToCssVariables(theme: Theme): Record {
+ const flattenedToken = flattenObject(theme.token)
+ console.log(flattenedToken)
+ const cssVars: Record = {}
+
+ for (const [path, value] of Object.entries(flattenedToken)) {
+ const cssVarName = createCssVariableName(path.split('.'))
+
+ cssVars[cssVarName] = value
+ }
+
+ return cssVars
+}
+
+export function injectThemeVariables(theme: Theme) {
+ const cssVars = themeToCssVariables(theme)
+ const root = document.documentElement
+
+ Object.entries(cssVars).forEach(([key, value]) => {
+ console.log(key, value)
+ root.style.setProperty(key, value)
+ })
+}
+
+/**
+ * 将扁平化的主题对象转换为Tailwind主题配置
+ */
+function transformToTailwindConfig(flattenedToken: Record): Record {
+ const result: Record = {}
+
+ // 处理对象路径,将其转换为嵌套结构
+ for (const [path, _] of Object.entries(flattenedToken)) {
+ const parts = path.split('.')
+ let current = result
+
+ // 遍历路径的每一部分,构建嵌套结构
+ for (let i = 0; i < parts.length; i++) {
+ const part = parts[i]
+ const isLast = i === parts.length - 1
+
+ // 如果是最后一个部分,设置CSS变量引用
+ if (isLast) {
+ current[part] = `var(${createCssVariableName(parts)})`
+ } else {
+ // 如果不是最后一个部分,确保存在嵌套对象
+ current[part] = current[part] || {}
+ current = current[part]
+ }
+ }
+ }
+
+ return result
+}
+
+/**
+ * 创建引用CSS变量的Tailwind主题配置
+ */
+export function createTailwindTheme(theme: Theme): Partial {
+ const flattenedToken = flattenObject(theme.token)
+ const themeConfig = transformToTailwindConfig(flattenedToken)
+
+ // 将主题配置映射到Tailwind的结构
+ const result = {
+ extend: {
+ colors: themeConfig.colors,
+ textColor: themeConfig.textColor,
+ backgroundColor: themeConfig.backgroundColor,
+ borderColor: themeConfig.border,
+ }
+ }
+ console.log(result)
+ return result
+}
diff --git a/packages/config/src/tailwind.ts b/packages/config/src/tailwind.ts
new file mode 100644
index 0000000..8859ecd
--- /dev/null
+++ b/packages/config/src/tailwind.ts
@@ -0,0 +1,132 @@
+import type { Config } from "tailwindcss";
+
+export const NiceTailwindConfig: Config = {
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ extend: {
+ colors: {
+ // 主色调 - 优雅蓝(清新雅致)
+ primary: {
+ 50: "#f0f7ff",
+ 100: "#e0f0ff",
+ 150: "#cce7ff",
+ 200: "#b3ddff",
+ 250: "#99d3ff",
+ 300: "#66bfff",
+ 350: "#47b3ff",
+ 400: "#1ca6ff",
+ 500: "#0088E8", // 主色调
+ 600: "#0070cc",
+ 700: "#0058a3",
+ 800: "#004080",
+ 900: "#002847",
+ DEFAULT: "#0088E8",
+ },
+ // 辅助色 - 温和灰(内容展示)
+ secondary: {
+ 50: "#fafafa",
+ 100: "#f5f5f5",
+ 200: "#eeeeee",
+ 300: "#e0e0e0",
+ 350: "#d4d4d4",
+ 400: "#bdbdbd",
+ 500: "#9e9e9e",
+ 600: "#757575",
+ 700: "#616161",
+ 800: "#424242",
+ 900: "#212121",
+ DEFAULT: "#757575",
+ },
+ // 强调色 - 活力橙(重要操作、交互)
+ accent: {
+ 50: "#fff4e5",
+ 100: "#ffe8cc",
+ 200: "#ffd699",
+ 300: "#ffc466",
+ 400: "#ffb333",
+ 500: "#ffa200",
+ 600: "#cc8200",
+ 700: "#996100",
+ 800: "#664100",
+ 900: "#332000",
+ DEFAULT: "#ffa200",
+ },
+ // 功能色(协调搭配)
+ success: "#4caf50",
+ warning: "#ff9800",
+ danger: "#f44336",
+ info: "#03a9f4",
+ },
+ fontFamily: {
+ sans: [
+ "PingFang SC",
+ "Microsoft YaHei",
+ "Helvetica Neue",
+ "system-ui",
+ "sans-serif",
+ ],
+ heading: [
+ "PingFang SC",
+ "Microsoft YaHei",
+ "Helvetica Neue",
+ "sans-serif",
+ ],
+ mono: ["Source Code Pro", "monospace"],
+ },
+ spacing: {
+ "72": "18rem",
+ "84": "21rem",
+ "96": "24rem",
+ },
+ borderRadius: {
+ xl: "1rem",
+ "2xl": "2rem",
+ "3xl": "3rem",
+ },
+ boxShadow: {
+ outline: "0 0 0 3px rgba(0, 136, 232, 0.4)",
+ solid: "2px 2px 0 0 rgba(0, 0, 0, 0.1)",
+ glow: "0 0 8px rgba(0, 136, 232, 0.4)",
+ inset: "inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)",
+ "elevation-1": "0 1px 2px rgba(0, 0, 0, 0.05)",
+ "elevation-2": "0 2px 4px rgba(0, 0, 0, 0.1)",
+ "elevation-3": "0 4px 8px rgba(0, 0, 0, 0.1)",
+ "elevation-4": "0 8px 16px rgba(0, 0, 0, 0.1)",
+ "elevation-5": "0 16px 32px rgba(0, 0, 0, 0.1)",
+ panel: "0 2px 4px rgba(0, 0, 0, 0.05)",
+ button: "0 2px 4px rgba(0, 0, 0, 0.1)",
+ card: "0 2px 8px rgba(0, 0, 0, 0.08)",
+ modal: "0 4px 16px rgba(0, 0, 0, 0.1)",
+ "video-thumbnail": "0 2px 4px rgba(0, 0, 0, 0.1)",
+ "hover-card": "0 4px 8px rgba(0, 0, 0, 0.08)",
+ "player-controls": "0 -2px 8px rgba(0, 0, 0, 0.1)",
+ "floating-player": "0 4px 12px rgba(0, 0, 0, 0.1)",
+ },
+ animation: {
+ "pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite",
+ "spin-slow": "spin 3s linear infinite",
+ "fade-in": "fadeIn 0.3s ease-in-out",
+ "slide-up": "slideUp 0.4s ease-out",
+ },
+ keyframes: {
+ fadeIn: {
+ "0%": { opacity: "0" },
+ "100%": { opacity: "1" },
+ },
+ slideUp: {
+ "0%": { transform: "translateY(10px)", opacity: "0" },
+ "100%": { transform: "translateY(0)", opacity: "1" },
+ },
+ },
+ transitionDuration: {
+ "2000": "2000ms",
+ "3000": "3000ms",
+ },
+ screens: {
+ "3xl": "1920px",
+ "4xl": "2560px",
+ },
+ },
+ },
+ plugins: [],
+};
diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts
new file mode 100644
index 0000000..dafeb70
--- /dev/null
+++ b/packages/config/src/types.ts
@@ -0,0 +1,112 @@
+/**
+ * 定义颜色比例尺,从最浅(50)到最深(950)的颜色梯度
+ * 用于构建一致的颜色系统,每个数字代表颜色的深浅程度
+ */
+export type ColorScale = {
+ 50: string; // 最浅色调
+ 100: string; // 非常浅色调
+ 200: string; // 浅色调
+ 300: string; // 中浅色调
+ 400: string; // 中等偏浅色调
+ 500: string; // 基准色调
+ 600: string; // 中等偏深色调
+ 700: string; // 深色调
+ 800: string; // 很深色调
+ 900: string; // 非常深色调
+ 950: string; // 最深色调
+ DEFAULT: string; // Tailwind 默认值,通常对应 500
+}
+/**
+ * 主题的基础颜色配置
+ * 定义系统中使用的所有基础色板
+ */
+export type ThemeColors = {
+ primary: ColorScale; // 主要品牌色
+ secondary: ColorScale; // 次要品牌色
+ neutral: ColorScale; // 中性色,通常用于文本和背景
+ success: ColorScale; // 成功状态颜色
+ warning: ColorScale; // 警告状态颜色
+ error: ColorScale; // 错误状态颜色
+ info: ColorScale; // 信息状态颜色
+}
+/**
+ * 主题的语义化颜色配置
+ * 定义具体UI元素的颜色应用场景
+ */
+export type ThemeSemantics = {
+ colors: ThemeColors,
+ /** 文本颜色相关配置 */
+ textColor: {
+ DEFAULT: string; // 默认文本颜色
+ primary: string; // 主要文本
+ secondary: string; // 次要文本
+ tertiary: string; // 第三级文本
+ disabled: string; // 禁用状态
+ inverse: string; // 反色文本
+ success: string; // 成功状态
+ warning: string; // 警告状态
+ error: string; // 错误状态
+ info: string; // 信息提示
+ link: string; // 链接文本
+ linkHover: string; // 链接悬浮
+ placeholder: string; // 占位符文本
+ highlight: string; // 高亮文本
+ };
+
+ /** 背景颜色相关配置 */
+ backgroundColor: {
+ DEFAULT: string; // 默认背景色
+ paper: string; // 卡片/纸张背景
+ subtle: string; // 轻微背景
+ inverse: string; // 反色背景
+ success: string; // 成功状态背景
+ warning: string; // 警告状态背景
+ error: string; // 错误状态背景
+ info: string; // 信息提示背景
+ primaryHover: string; // 主要按钮悬浮
+ primaryActive: string; // 主要按钮激活
+ primaryDisabled: string; // 主要按钮禁用
+ secondaryHover: string; // 次要按钮悬浮
+ secondaryActive: string; // 次要按钮激活
+ secondaryDisabled: string; // 次要按钮禁用
+ selected: string; // 选中状态
+ hover: string; // 通用悬浮态
+ focused: string; // 聚焦状态
+ disabled: string; // 禁用状态
+ overlay: string; // 遮罩层
+ };
+
+ /** 边框颜色配置 */
+ border: {
+ DEFAULT: string; // 默认边框
+ subtle: string; // 轻微边框
+ strong: string; // 强调边框
+ focus: string; // 聚焦边框
+ inverse: string; // 反色边框
+ success: string; // 成功状态边框
+ warning: string; // 警告状态边框
+ error: string; // 错误状态边框
+ info: string; // 信息提示边框
+ disabled: string; // 禁用状态边框
+ };
+}
+
+
+export type ThemeToken = ThemeSemantics
+export interface Theme {
+ token: ThemeToken;
+ isDark: boolean;
+}
+export interface ThemeSeed {
+ colors: {
+ primary: string;
+ secondary: string;
+ neutral: string;
+ success?: string;
+ warning?: string;
+ error?: string;
+ info?: string;
+ }
+
+ isDark?: boolean;
+}
diff --git a/packages/config/src/utils.ts b/packages/config/src/utils.ts
new file mode 100644
index 0000000..ae10171
--- /dev/null
+++ b/packages/config/src/utils.ts
@@ -0,0 +1,26 @@
+// Helper function to generate conditional values based on dark mode
+export function darkMode(isDark: boolean, darkValue: T, lightValue: T): T {
+ return isDark ? darkValue : lightValue;
+}
+/**
+ * 将驼峰命名转换为kebab-case
+ */
+export function toKebabCase(str: string): string {
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
+}
+/**
+ * 将嵌套对象扁平化,使用点号连接键名
+ */
+export function flattenObject(obj: Record, prefix = ''): Record {
+ return Object.keys(obj).reduce((acc: Record, k: string) => {
+ const pre = prefix.length ? prefix + '.' : ''
+
+ if (typeof obj[k] === 'object' && obj[k] !== null && !Array.isArray(obj[k])) {
+ Object.assign(acc, flattenObject(obj[k], pre + k))
+ } else {
+ acc[pre + k] = obj[k].toString()
+ }
+
+ return acc
+ }, {})
+}
\ No newline at end of file
diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json
new file mode 100644
index 0000000..f2c2a6d
--- /dev/null
+++ b/packages/config/tsconfig.json
@@ -0,0 +1,43 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "module": "esnext",
+ "allowJs": true,
+ "lib": [
+ "DOM",
+ "es2022",
+ "DOM.Iterable"
+ ],
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "removeComments": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "esModuleInterop": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noImplicitReturns": false,
+ "noFallthroughCasesInSwitch": false,
+ "noUncheckedIndexedAccess": false,
+ "noImplicitOverride": false,
+ "noPropertyAccessFromIndexSignature": false,
+ "outDir": "dist",
+ "incremental": true,
+ "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo"
+ },
+ "include": [
+ "src"
+ ],
+ "exclude": [
+ "node_modules",
+ "dist",
+ "**/*.test.ts",
+ "**/*.spec.ts",
+ "**/__tests__"
+ ]
+}
\ No newline at end of file
diff --git a/packages/config/tsup.config.ts b/packages/config/tsup.config.ts
new file mode 100644
index 0000000..f21629b
--- /dev/null
+++ b/packages/config/tsup.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from 'tsup'
+
+export default defineConfig({
+ entry: ['src/index.ts'],
+ format: ['esm', 'cjs'],
+ dts: true,
+ clean: true,
+ sourcemap: true,
+ minify: true,
+ external: ['react', 'react-dom'],
+ bundle: true,
+ target: "esnext"
+})
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d020b0d..bce2ffd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -290,6 +290,9 @@ importers:
'@nice/common':
specifier: workspace:^
version: link:../../packages/common
+ '@nice/config':
+ specifier: workspace:^
+ version: link:../../packages/config
'@nice/iconer':
specifier: workspace:^
version: link:../../packages/iconer
@@ -543,6 +546,55 @@ importers:
specifier: ^5.5.4
version: 5.7.2
+ packages/config:
+ dependencies:
+ '@nice/utils':
+ specifier: workspace:^
+ version: link:../utils
+ color:
+ specifier: ^4.2.3
+ version: 4.2.3
+ nanoid:
+ specifier: ^5.0.9
+ version: 5.0.9
+ react:
+ specifier: 18.2.0
+ version: 18.2.0
+ react-dom:
+ specifier: 18.2.0
+ version: 18.2.0(react@18.2.0)
+ devDependencies:
+ '@types/color':
+ specifier: ^4.2.0
+ version: 4.2.0
+ '@types/dagre':
+ specifier: ^0.7.52
+ version: 0.7.52
+ '@types/node':
+ specifier: ^20.3.1
+ version: 20.17.12
+ '@types/react':
+ specifier: 18.2.38
+ version: 18.2.38
+ '@types/react-dom':
+ specifier: 18.2.15
+ version: 18.2.15
+ concurrently:
+ specifier: ^8.0.0
+ version: 8.2.2
+ rimraf:
+ specifier: ^6.0.1
+ version: 6.0.1
+ ts-node:
+ specifier: ^10.9.1
+ version: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)
+ tsup:
+ specifier: ^8.3.5
+ version: 8.3.5(@microsoft/api-extractor@7.49.2(@types/node@20.17.12))(@swc/core@1.10.6(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0)
+ typescript:
+ specifier: ^5.5.4
+ version: 5.7.2
+
packages/iconer:
devDependencies:
'@types/react':
@@ -3010,6 +3062,15 @@ packages:
'@types/body-parser@1.19.5':
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
+ '@types/color-convert@2.0.4':
+ resolution: {integrity: sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==}
+
+ '@types/color-name@1.1.5':
+ resolution: {integrity: sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg==}
+
+ '@types/color@4.2.0':
+ resolution: {integrity: sha512-6+xrIRImMtGAL2X3qYkd02Mgs+gFGs+WsK0b7VVMaO4mYRISwyTjcqNrO0mNSmYEoq++rSLDB2F5HDNmqfOe+A==}
+
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
@@ -10539,6 +10600,16 @@ snapshots:
'@types/connect': 3.4.38
'@types/node': 20.17.12
+ '@types/color-convert@2.0.4':
+ dependencies:
+ '@types/color-name': 1.1.5
+
+ '@types/color-name@1.1.5': {}
+
+ '@types/color@4.2.0':
+ dependencies:
+ '@types/color-convert': 2.0.4
+
'@types/connect@3.4.38':
dependencies:
'@types/node': 20.17.12