From 4fb0fa49e7987fb6232983c1f35afa503fcf687b Mon Sep 17 00:00:00 2001 From: longdayi <13477510+longdayilongdayi@user.noreply.gitee.com> Date: Thu, 6 Feb 2025 16:32:31 +0800 Subject: [PATCH] 02061632 --- apps/web/package.json | 1 + apps/web/src/App.tsx | 2 +- .../main/courses/components/CourseCard.tsx | 48 ++++ .../main/courses/components/CourseList.tsx | 44 ++++ .../main/courses/components/FilterSection.tsx | 54 +++++ apps/web/src/app/main/courses/mockData.ts | 44 ++++ apps/web/src/app/main/courses/page.tsx | 73 ++++++ .../main/home/components/CategorySection.tsx | 162 +++++++++++++ .../main/home/components/CoursesSection.tsx | 206 ++++++++++++++++ .../components/FeaturedTeachersSection.tsx | 222 ++++++++++++++++++ .../app/main/home/components/HeroSection.tsx | 157 +++++++++++++ apps/web/src/app/main/home/page.tsx | 144 ++++++++++-- apps/web/src/app/main/layout/MainFooter.tsx | 68 ++++++ apps/web/src/app/main/layout/MainHeader.tsx | 65 +++++ apps/web/src/app/main/layout/MainLayout.tsx | 18 ++ .../src/app/main/layout/NavigationMenu.tsx | 31 +++ apps/web/src/app/main/layout/UserMenu.tsx | 49 ++++ apps/web/src/app/main/paths/page.tsx | 3 + apps/web/src/app/main/self/courses/page.tsx | 7 + apps/web/src/app/main/self/profiles/page.tsx | 3 + .../src/components/layout/main/MainLayout.tsx | 40 ---- .../src/components/layout/main/nav-data.tsx | 43 ---- .../layout/main/notifications-dropdown.tsx | 40 ---- .../layout/main/notifications-panel.tsx | 64 ----- .../src/components/layout/main/search-bar.tsx | 55 ----- .../layout/main/search-dropdown.tsx | 58 ----- .../src/components/layout/main/side-bar.tsx | 44 ---- .../components/layout/main/top-nav-bar.tsx | 47 ---- .../layout/main/usermenu-dropdown.tsx | 68 ------ apps/web/src/index.css | 6 +- apps/web/src/routes/index.tsx | 13 +- apps/web/tailwind.config.js | 53 ----- apps/web/tailwind.config.ts | 2 + packages/common/src/utils.ts | 40 ++-- packages/config/package.json | 35 +++ packages/config/src/colors.ts | 24 ++ packages/config/src/constants.ts | 19 ++ packages/config/src/context.tsx | 50 ++++ packages/config/src/generator.ts | 99 ++++++++ packages/config/src/index.ts | 6 + packages/config/src/styles.ts | 89 +++++++ packages/config/src/tailwind.ts | 132 +++++++++++ packages/config/src/types.ts | 112 +++++++++ packages/config/src/utils.ts | 26 ++ packages/config/tsconfig.json | 43 ++++ packages/config/tsup.config.ts | 13 + pnpm-lock.yaml | 71 ++++++ 47 files changed, 2132 insertions(+), 561 deletions(-) create mode 100644 apps/web/src/app/main/courses/components/CourseCard.tsx create mode 100644 apps/web/src/app/main/courses/components/CourseList.tsx create mode 100644 apps/web/src/app/main/courses/components/FilterSection.tsx create mode 100644 apps/web/src/app/main/courses/mockData.ts create mode 100644 apps/web/src/app/main/courses/page.tsx create mode 100644 apps/web/src/app/main/home/components/CategorySection.tsx create mode 100644 apps/web/src/app/main/home/components/CoursesSection.tsx create mode 100644 apps/web/src/app/main/home/components/FeaturedTeachersSection.tsx create mode 100644 apps/web/src/app/main/home/components/HeroSection.tsx create mode 100644 apps/web/src/app/main/layout/MainFooter.tsx create mode 100644 apps/web/src/app/main/layout/MainHeader.tsx create mode 100644 apps/web/src/app/main/layout/MainLayout.tsx create mode 100644 apps/web/src/app/main/layout/NavigationMenu.tsx create mode 100644 apps/web/src/app/main/layout/UserMenu.tsx create mode 100644 apps/web/src/app/main/paths/page.tsx create mode 100644 apps/web/src/app/main/self/courses/page.tsx create mode 100644 apps/web/src/app/main/self/profiles/page.tsx delete mode 100644 apps/web/src/components/layout/main/MainLayout.tsx delete mode 100644 apps/web/src/components/layout/main/nav-data.tsx delete mode 100644 apps/web/src/components/layout/main/notifications-dropdown.tsx delete mode 100644 apps/web/src/components/layout/main/notifications-panel.tsx delete mode 100644 apps/web/src/components/layout/main/search-bar.tsx delete mode 100644 apps/web/src/components/layout/main/search-dropdown.tsx delete mode 100644 apps/web/src/components/layout/main/side-bar.tsx delete mode 100644 apps/web/src/components/layout/main/top-nav-bar.tsx delete mode 100644 apps/web/src/components/layout/main/usermenu-dropdown.tsx delete mode 100755 apps/web/tailwind.config.js create mode 100755 apps/web/tailwind.config.ts create mode 100644 packages/config/package.json create mode 100644 packages/config/src/colors.ts create mode 100644 packages/config/src/constants.ts create mode 100644 packages/config/src/context.tsx create mode 100644 packages/config/src/generator.ts create mode 100644 packages/config/src/index.ts create mode 100644 packages/config/src/styles.ts create mode 100644 packages/config/src/tailwind.ts create mode 100644 packages/config/src/types.ts create mode 100644 packages/config/src/utils.ts create mode 100644 packages/config/tsconfig.json create mode 100644 packages/config/tsup.config.ts 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.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 ( +
+
+
+ {/* 开发组织信息 */} +
+

+ 创新高地 软件小组 +

+

+ 提供技术支持 +

+
+ + {/* 联系方式 */} +
+
+ + 628118 +
+
+ + gcsjs6@tx3l.nb.kj +
+
+ + {/* 系统链接 */} +
+ +
+
+ + {/* 版权信息 */} +
+

+ © {new Date().getFullYear()} 南天烽火. All rights reserved. +

+
+
+
+ ); +} 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 ( + { + const selectedItem = menuItems.find(item => item.key === key); + if (selectedItem) navigate(selectedItem.path); + }} + > + {menuItems.map(({ key, label }) => ( + + {label} + + ))} + + ); +}; \ 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 ( + + {isAuthenticated ? ( + <> + +
+ + {(user?.showname || user?.username || '')[0]?.toUpperCase()} + +
+ {user?.showname || user?.username} + {user?.department?.name || user?.officerId} +
+
+
+ + } className="px-4"> + 个人设置 + + } + onClick={async () => await logout()} + className="px-4 text-red-500 hover:text-red-600 hover:bg-red-50" + > + 退出登录 + + + ) : ( + navigate("/login")} + className="px-4 text-blue-500 hover:text-blue-600 hover:bg-blue-50" + > + 登录/注册 + + )} +
+ ); +}; \ 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