Design System 重构继续完成
This commit is contained in:
30
CLAUDE.md
30
CLAUDE.md
@@ -22,9 +22,7 @@ pnpm run start
|
|||||||
pnpm run lint
|
pnpm run lint
|
||||||
|
|
||||||
# 数据库操作
|
# 数据库操作
|
||||||
pnpm prisma generate # 生成 Prisma client 到 src/generated/prisma
|
# 不要进行数据库操作,让用户操作数据库
|
||||||
pnpm prisma db push # 推送 schema 变更到数据库
|
|
||||||
pnpm prisma studio # 打开 Prisma Studio 查看数据库
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
@@ -102,31 +100,6 @@ src/modules/{module}/
|
|||||||
|
|
||||||
**LLM 集成**: 使用智谱 AI API 进行翻译和 IPA 生成。通过环境变量 `ZHIPU_API_KEY` 和 `ZHIPU_MODEL_NAME` 配置。
|
**LLM 集成**: 使用智谱 AI API 进行翻译和 IPA 生成。通过环境变量 `ZHIPU_API_KEY` 和 `ZHIPU_MODEL_NAME` 配置。
|
||||||
|
|
||||||
### 环境变量
|
|
||||||
|
|
||||||
需要在 `.env.local` 中配置:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# LLM 集成(智谱 AI 用于翻译和 IPA 生成)
|
|
||||||
ZHIPU_API_KEY=your-api-key
|
|
||||||
ZHIPU_MODEL_NAME=your-model-name
|
|
||||||
|
|
||||||
# 阿里云千问 TTS(文本转语音)
|
|
||||||
DASHSCORE_API_KEY=your-dashscore-api-key
|
|
||||||
|
|
||||||
# 认证
|
|
||||||
BETTER_AUTH_SECRET=your-secret
|
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
|
||||||
GITHUB_CLIENT_ID=your-client-id
|
|
||||||
GITHUB_CLIENT_SECRET=your-client-secret
|
|
||||||
|
|
||||||
# 数据库
|
|
||||||
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
|
||||||
```
|
|
||||||
|
|
||||||
## 重要配置细节
|
|
||||||
|
|
||||||
- **Prisma client 输出**: 自定义目录 `src/generated/prisma`(不是默认的 `node_modules/.prisma`)
|
|
||||||
- **Standalone 输出**: 为 Docker 部署配置
|
- **Standalone 输出**: 为 Docker 部署配置
|
||||||
- **React Compiler**: 在 `next.config.ts` 中启用以自动优化
|
- **React Compiler**: 在 `next.config.ts` 中启用以自动优化
|
||||||
- **HTTPS 开发**: 开发服务器使用 `--experimental-https` 标志
|
- **HTTPS 开发**: 开发服务器使用 `--experimental-https` 标志
|
||||||
@@ -147,7 +120,6 @@ DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
|||||||
## 开发注意事项
|
## 开发注意事项
|
||||||
|
|
||||||
- 使用 pnpm,而不是 npm 或 yarn
|
- 使用 pnpm,而不是 npm 或 yarn
|
||||||
- schema 变更后,先运行 `pnpm prisma generate` 再运行 `pnpm prisma db push`
|
|
||||||
- 应用使用 TypeScript 严格模式 - 确保类型安全
|
- 应用使用 TypeScript 严格模式 - 确保类型安全
|
||||||
- 所有面向用户的文本都需要国际化
|
- 所有面向用户的文本都需要国际化
|
||||||
- **优先使用 Server Components**,只在需要交互时使用 Client Components
|
- **优先使用 Server Components**,只在需要交互时使用 Client Components
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||||
import { IconClick, CircleToggleButton, CircleButton, PrimaryButton } from "@/components/ui/buttons";
|
import { IconClick, CircleToggleButton, CircleButton, PrimaryButton } from "@/design-system/base/button";
|
||||||
import { IMAGES } from "@/config/images";
|
import { IMAGES } from "@/config/images";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { Card } from "@/components/ui/Card";
|
import { Card } from "@/design-system/base/card";
|
||||||
|
|
||||||
interface AlphabetCardProps {
|
interface AlphabetCardProps {
|
||||||
alphabet: Letter[];
|
alphabet: Letter[];
|
||||||
@@ -103,7 +103,7 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
|
|||||||
{/* 右上角返回按钮 - outside the white card */}
|
{/* 右上角返回按钮 - outside the white card */}
|
||||||
<div className="flex justify-end mb-4">
|
<div className="flex justify-end mb-4">
|
||||||
<IconClick
|
<IconClick
|
||||||
size={32}
|
size="lg"
|
||||||
alt="close"
|
alt="close"
|
||||||
src={IMAGES.close}
|
src={IMAGES.close}
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
@@ -185,7 +185,7 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
|
|||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
{/* 上一个按钮 */}
|
{/* 上一个按钮 */}
|
||||||
<CircleButton onClick={goToPrevious} aria-label="上一个字母">
|
<CircleButton onClick={goToPrevious} aria-label="上一个字母">
|
||||||
<ChevronLeft size={24} />
|
<ChevronLeft size={20} />
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
|
|
||||||
{/* 中间区域:随机按钮 */}
|
{/* 中间区域:随机按钮 */}
|
||||||
@@ -202,7 +202,7 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
|
|||||||
|
|
||||||
{/* 下一个按钮 */}
|
{/* 下一个按钮 */}
|
||||||
<CircleButton onClick={goToNext} aria-label="下一个字母">
|
<CircleButton onClick={goToNext} aria-label="下一个字母">
|
||||||
<ChevronRight size={24} />
|
<ChevronRight size={20} />
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { IconClick } from "@/components/ui/buttons";
|
import { IconClick } from "@/design-system/base/button";
|
||||||
import { IMAGES } from "@/config/images";
|
import { IMAGES } from "@/config/images";
|
||||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||||
import {
|
import {
|
||||||
@@ -45,10 +45,10 @@ export function MemoryCard({
|
|||||||
className="w-full flex justify-center items-center"
|
className="w-full flex justify-center items-center"
|
||||||
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => e.preventDefault()}
|
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<div className="m-4 p-4 w-full md:w-[60dvw] flex-col rounded-2xl shadow border-gray-200 border flex justify-center items-center">
|
<div className="m-4 p-4 w-full md:w-[60dvw] flex-col rounded-lg shadow border-gray-200 border flex justify-center items-center">
|
||||||
<div className="w-full flex justify-end items-center">
|
<div className="w-full flex justify-end items-center">
|
||||||
<IconClick
|
<IconClick
|
||||||
size={32}
|
size="lg"
|
||||||
alt="close"
|
alt="close"
|
||||||
src={IMAGES.close}
|
src={IMAGES.close}
|
||||||
onClick={() => setChosenAlphabet(null)}
|
onClick={() => setChosenAlphabet(null)}
|
||||||
@@ -64,13 +64,13 @@ export function MemoryCard({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row mt-32 items-center justify-center gap-2">
|
<div className="flex flex-row mt-32 items-center justify-center gap-2">
|
||||||
<IconClick
|
<IconClick
|
||||||
size={48}
|
size="lg"
|
||||||
alt="refresh"
|
alt="refresh"
|
||||||
src={IMAGES.refresh}
|
src={IMAGES.refresh}
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
></IconClick>
|
></IconClick>
|
||||||
<IconClick
|
<IconClick
|
||||||
size={48}
|
size="lg"
|
||||||
alt="more"
|
alt="more"
|
||||||
src={IMAGES.more_horiz}
|
src={IMAGES.more_horiz}
|
||||||
onClick={() => setMore(!more)}
|
onClick={() => setMore(!more)}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { AlphabetCard } from "./AlphabetCard";
|
import { AlphabetCard } from "./AlphabetCard";
|
||||||
|
|
||||||
export default function Alphabet() {
|
export default function Alphabet() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { Input } from "@/components/ui/Input";
|
import { Input } from "@/design-system/base/input";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Plus, RefreshCw } from "lucide-react";
|
import { Plus, RefreshCw } from "lucide-react";
|
||||||
import { CircleButton, LightButton } from "@/components/ui/buttons";
|
import { CircleButton, LightButton } from "@/design-system/base/button";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { actionCreatePair } from "@/modules/folder/folder-aciton";
|
import { actionCreatePair } from "@/modules/folder/folder-aciton";
|
||||||
import { TSharedItem } from "@/shared/dictionary-type";
|
import { TSharedItem } from "@/shared/dictionary-type";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Link from "next/link";
|
|||||||
import { Folder as Fd } from "lucide-react";
|
import { Folder as Fd } from "lucide-react";
|
||||||
import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { PrimaryButton } from "@/components/ui/buttons";
|
import { PrimaryButton } from "@/design-system/base/button";
|
||||||
|
|
||||||
interface FolderSelectorProps {
|
interface FolderSelectorProps {
|
||||||
folders: TSharedFolderWithTotalPairs[];
|
folders: TSharedFolderWithTotalPairs[];
|
||||||
@@ -37,7 +37,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
|
|||||||
{t("selectFolder")}
|
{t("selectFolder")}
|
||||||
</h1>
|
</h1>
|
||||||
{/* 文件夹列表 */}
|
{/* 文件夹列表 */}
|
||||||
<div className="border border-gray-200 rounded-2xl max-h-96 overflow-y-auto">
|
<div className="border border-gray-200 rounded-lg max-h-96 overflow-y-auto">
|
||||||
{folders
|
{folders
|
||||||
.toSorted((a, b) => a.id - b.id)
|
.toSorted((a, b) => a.id - b.id)
|
||||||
.map((folder) => (
|
.map((folder) => (
|
||||||
@@ -50,7 +50,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
|
|||||||
>
|
>
|
||||||
{/* 文件夹图标 */}
|
{/* 文件夹图标 */}
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<Fd className="text-gray-600" size={24} />
|
<Fd className="text-gray-600" size="md" />
|
||||||
</div>
|
</div>
|
||||||
{/* 文件夹信息 */}
|
{/* 文件夹信息 */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { LinkButton, CircleToggleButton, LightButton } from "@/components/ui/buttons";
|
import { LinkButton, CircleToggleButton, LightButton } from "@/design-system/base/button";
|
||||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||||
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
|
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -29,7 +29,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
|||||||
|
|
||||||
if (textPairs.length === 0) {
|
if (textPairs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<PageLayout maxWidth="md">
|
<PageLayout>
|
||||||
<p className="text-gray-700 text-center">{t("noTextPairs")}</p>
|
<p className="text-gray-700 text-center">{t("noTextPairs")}</p>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useRef, forwardRef, useEffect, useCallback } from "react";
|
import { useState, useRef, forwardRef, useEffect, useCallback } from "react";
|
||||||
import { SubtitleDisplay } from "./SubtitleDisplay";
|
import { SubtitleDisplay } from "./SubtitleDisplay";
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { RangeInput } from "@/components/ui/RangeInput";
|
import { RangeInput } from "@/components/ui/RangeInput";
|
||||||
import { getIndex, parseSrt, getNearistIndex } from "../subtitle";
|
import { getIndex, parseSrt, getNearistIndex } from "../subtitle";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/design-system/base/button";
|
||||||
import { FileInputProps } from "../../types/controls";
|
import { FileInputProps } from "../../types/controls";
|
||||||
|
|
||||||
interface FileInputComponentProps extends FileInputProps {
|
interface FileInputComponentProps extends FileInputProps {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { PlayButtonProps } from "../../types/player";
|
import { PlayButtonProps } from "../../types/player";
|
||||||
|
|
||||||
export function PlayButton({ isPlaying, onToggle, disabled, className }: PlayButtonProps) {
|
export function PlayButton({ isPlaying, onToggle, disabled, className }: PlayButtonProps) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { SpeedControlProps } from "../../types/player";
|
import { SpeedControlProps } from "../../types/player";
|
||||||
import { getPlaybackRateOptions, getPlaybackRateLabel } from "../../utils/timeUtils";
|
import { getPlaybackRateOptions, getPlaybackRateLabel } from "../../utils/timeUtils";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { ChevronLeft, ChevronRight, RotateCcw, Pause } from "lucide-react";
|
import { ChevronLeft, ChevronRight, RotateCcw, Pause } from "lucide-react";
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { ControlBarProps } from "../../types/controls";
|
import { ControlBarProps } from "../../types/controls";
|
||||||
import { PlayButton } from "../atoms/PlayButton";
|
import { PlayButton } from "../atoms/PlayButton";
|
||||||
import { SpeedControl } from "../atoms/SpeedControl";
|
import { SpeedControl } from "../atoms/SpeedControl";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Video, FileText } from "lucide-react";
|
import { Video, FileText } from "lucide-react";
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { FileUploadProps } from "../../types/controls";
|
import { FileUploadProps } from "../../types/controls";
|
||||||
import { useFileUpload } from "../../hooks/useFileUpload";
|
import { useFileUpload } from "../../hooks/useFileUpload";
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { SubtitleArea } from "./components/compounds/SubtitleArea";
|
|||||||
import { ControlBar } from "./components/compounds/ControlBar";
|
import { ControlBar } from "./components/compounds/ControlBar";
|
||||||
import { UploadZone } from "./components/compounds/UploadZone";
|
import { UploadZone } from "./components/compounds/UploadZone";
|
||||||
import { SeekBar } from "./components/atoms/SeekBar";
|
import { SeekBar } from "./components/atoms/SeekBar";
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
|
|
||||||
export default function SrtPlayerPage() {
|
export default function SrtPlayerPage() {
|
||||||
const t = useTranslations("home");
|
const t = useTranslations("home");
|
||||||
@@ -119,7 +119,7 @@ export default function SrtPlayerPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 视频播放器区域 */}
|
{/* 视频播放器区域 */}
|
||||||
<div className="aspect-video bg-black relative rounded-xl overflow-hidden">
|
<div className="aspect-video bg-black relative rounded-md overflow-hidden">
|
||||||
{(!state.video.url || !state.subtitle.url || state.subtitle.data.length === 0) && (
|
{(!state.video.url || !state.subtitle.url || state.subtitle.data.length === 0) && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-75 z-10">
|
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-75 z-10">
|
||||||
<div className="text-center text-white">
|
<div className="text-center text-white">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
TextSpeakerArraySchema,
|
TextSpeakerArraySchema,
|
||||||
TextSpeakerItemSchema,
|
TextSpeakerItemSchema,
|
||||||
} from "@/lib/interfaces";
|
} from "@/lib/interfaces";
|
||||||
import { IconClick } from "@/components/ui/buttons";
|
import { IconClick } from "@/design-system/base/button";
|
||||||
import { IMAGES } from "@/config/images";
|
import { IMAGES } from "@/config/images";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
||||||
@@ -24,7 +24,7 @@ function TextCard({ item, handleUse, handleDel }: TextCardProps) {
|
|||||||
handleDel(item);
|
handleDel(item);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="p-2 border-b border-gray-200 rounded-2xl bg-gray-100 m-2 grid grid-cols-8">
|
<div className="p-2 border-b border-gray-200 rounded-lg bg-gray-100 m-2 grid grid-cols-8">
|
||||||
<div className="col-span-7" onClick={onUseClick}>
|
<div className="col-span-7" onClick={onUseClick}>
|
||||||
<div className="max-h-26 hover:cursor-pointer text-3xl overflow-y-auto">
|
<div className="max-h-26 hover:cursor-pointer text-3xl overflow-y-auto">
|
||||||
{item.text}
|
{item.text}
|
||||||
@@ -39,7 +39,7 @@ function TextCard({ item, handleUse, handleDel }: TextCardProps) {
|
|||||||
alt="delete"
|
alt="delete"
|
||||||
onClick={onDelClick}
|
onClick={onDelClick}
|
||||||
className="place-self-center"
|
className="place-self-center"
|
||||||
size={42}
|
size="lg"
|
||||||
></IconClick>
|
></IconClick>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,7 +81,7 @@ export function SaveList({ show = false, handleUse }: SaveListProps) {
|
|||||||
if (show)
|
if (show)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="my-4 p-2 mx-4 md:mx-32 border border-gray-200 rounded-2xl"
|
className="my-4 p-2 mx-4 md:mx-32 border border-gray-200 rounded-lg"
|
||||||
style={{ fontFamily: "Times New Roman, serif" }}
|
style={{ fontFamily: "Times New Roman, serif" }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row justify-center gap-8 items-center">
|
<div className="flex flex-row justify-center gap-8 items-center">
|
||||||
@@ -89,14 +89,14 @@ export function SaveList({ show = false, handleUse }: SaveListProps) {
|
|||||||
src={IMAGES.refresh}
|
src={IMAGES.refresh}
|
||||||
alt="refresh"
|
alt="refresh"
|
||||||
onClick={refresh}
|
onClick={refresh}
|
||||||
size={48}
|
size="lg"
|
||||||
className=""
|
className=""
|
||||||
></IconClick>
|
></IconClick>
|
||||||
<IconClick
|
<IconClick
|
||||||
src={IMAGES.delete}
|
src={IMAGES.delete}
|
||||||
alt="delete"
|
alt="delete"
|
||||||
onClick={handleDeleteAll}
|
onClick={handleDeleteAll}
|
||||||
size={48}
|
size="lg"
|
||||||
className=""
|
className=""
|
||||||
></IconClick>
|
></IconClick>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { IconClick } from "@/components/ui/buttons";
|
import { IconClick } from "@/design-system/base/button";
|
||||||
import { IMAGES } from "@/config/images";
|
import { IMAGES } from "@/config/images";
|
||||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||||
import {
|
import {
|
||||||
@@ -222,7 +222,7 @@ export default function TextSpeakerPage() {
|
|||||||
<PageLayout className="items-start py-4">
|
<PageLayout className="items-start py-4">
|
||||||
{/* 文本输入区域 */}
|
{/* 文本输入区域 */}
|
||||||
<div
|
<div
|
||||||
className="border border-gray-200 rounded-2xl"
|
className="border border-gray-200 rounded-lg"
|
||||||
style={{ fontFamily: "Times New Roman, serif" }}
|
style={{ fontFamily: "Times New Roman, serif" }}
|
||||||
>
|
>
|
||||||
{/* 文本输入框 */}
|
{/* 文本输入框 */}
|
||||||
@@ -242,37 +242,37 @@ export default function TextSpeakerPage() {
|
|||||||
<div className="p-4 relative w-full flex flex-row flex-wrap gap-2 justify-center items-center">
|
<div className="p-4 relative w-full flex flex-row flex-wrap gap-2 justify-center items-center">
|
||||||
{/* 速度调节面板 */}
|
{/* 速度调节面板 */}
|
||||||
{showSpeedAdjust && (
|
{showSpeedAdjust && (
|
||||||
<div className="bg-white p-6 rounded-2xl border-gray-200 border-2 shadow-2xl absolute left-1/2 -translate-x-1/2 -translate-y-full -top-4 flex flex-row flex-wrap gap-2 justify-center items-center z-10">
|
<div className="bg-white p-6 rounded-lg border-gray-200 border-2 shadow-2xl absolute left-1/2 -translate-x-1/2 -translate-y-full -top-4 flex flex-row flex-wrap gap-2 justify-center items-center z-10">
|
||||||
<IconClick
|
<IconClick
|
||||||
size={45}
|
size="lg"
|
||||||
onClick={letMeSetSpeed(0.5)}
|
onClick={letMeSetSpeed(0.5)}
|
||||||
src={IMAGES.speed_0_5x}
|
src={IMAGES.speed_0_5x}
|
||||||
alt="0.5x"
|
alt="0.5x"
|
||||||
className={speed === 0.5 ? "bg-gray-200" : ""}
|
className={speed === 0.5 ? "bg-gray-200" : ""}
|
||||||
></IconClick>
|
></IconClick>
|
||||||
<IconClick
|
<IconClick
|
||||||
size={45}
|
size="lg"
|
||||||
onClick={letMeSetSpeed(0.7)}
|
onClick={letMeSetSpeed(0.7)}
|
||||||
src={IMAGES.speed_0_7x}
|
src={IMAGES.speed_0_7x}
|
||||||
alt="0.7x"
|
alt="0.7x"
|
||||||
className={speed === 0.7 ? "bg-gray-200" : ""}
|
className={speed === 0.7 ? "bg-gray-200" : ""}
|
||||||
></IconClick>
|
></IconClick>
|
||||||
<IconClick
|
<IconClick
|
||||||
size={45}
|
size="lg"
|
||||||
onClick={letMeSetSpeed(1)}
|
onClick={letMeSetSpeed(1)}
|
||||||
src={IMAGES.speed_1x}
|
src={IMAGES.speed_1x}
|
||||||
alt="1x"
|
alt="1x"
|
||||||
className={speed === 1 ? "bg-gray-200" : ""}
|
className={speed === 1 ? "bg-gray-200" : ""}
|
||||||
></IconClick>
|
></IconClick>
|
||||||
<IconClick
|
<IconClick
|
||||||
size={45}
|
size="lg"
|
||||||
onClick={letMeSetSpeed(1.2)}
|
onClick={letMeSetSpeed(1.2)}
|
||||||
src={IMAGES.speed_1_2_x}
|
src={IMAGES.speed_1_2_x}
|
||||||
alt="1.2x"
|
alt="1.2x"
|
||||||
className={speed === 1.2 ? "bg-gray-200" : ""}
|
className={speed === 1.2 ? "bg-gray-200" : ""}
|
||||||
></IconClick>
|
></IconClick>
|
||||||
<IconClick
|
<IconClick
|
||||||
size={45}
|
size="lg"
|
||||||
onClick={letMeSetSpeed(1.5)}
|
onClick={letMeSetSpeed(1.5)}
|
||||||
src={IMAGES.speed_1_5x}
|
src={IMAGES.speed_1_5x}
|
||||||
alt="1.5x"
|
alt="1.5x"
|
||||||
@@ -282,7 +282,7 @@ export default function TextSpeakerPage() {
|
|||||||
)}
|
)}
|
||||||
{/* 播放/暂停按钮 */}
|
{/* 播放/暂停按钮 */}
|
||||||
<IconClick
|
<IconClick
|
||||||
size={45}
|
size="lg"
|
||||||
onClick={speak}
|
onClick={speak}
|
||||||
src={pause ? IMAGES.play_arrow : IMAGES.pause}
|
src={pause ? IMAGES.play_arrow : IMAGES.pause}
|
||||||
alt="playorpause"
|
alt="playorpause"
|
||||||
@@ -290,7 +290,7 @@ export default function TextSpeakerPage() {
|
|||||||
></IconClick>
|
></IconClick>
|
||||||
{/* 自动暂停按钮 */}
|
{/* 自动暂停按钮 */}
|
||||||
<IconClick
|
<IconClick
|
||||||
size={45}
|
size="lg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAutopause(!autopause);
|
setAutopause(!autopause);
|
||||||
if (objurlRef) {
|
if (objurlRef) {
|
||||||
@@ -303,7 +303,7 @@ export default function TextSpeakerPage() {
|
|||||||
></IconClick>
|
></IconClick>
|
||||||
{/* 速度调节按钮 */}
|
{/* 速度调节按钮 */}
|
||||||
<IconClick
|
<IconClick
|
||||||
size={45}
|
size="lg"
|
||||||
onClick={() => setShowSpeedAdjust(!showSpeedAdjust)}
|
onClick={() => setShowSpeedAdjust(!showSpeedAdjust)}
|
||||||
src={IMAGES.speed}
|
src={IMAGES.speed}
|
||||||
alt="speed"
|
alt="speed"
|
||||||
@@ -311,7 +311,7 @@ export default function TextSpeakerPage() {
|
|||||||
></IconClick>
|
></IconClick>
|
||||||
{/* 保存按钮 */}
|
{/* 保存按钮 */}
|
||||||
<IconClick
|
<IconClick
|
||||||
size={45}
|
size="lg"
|
||||||
onClick={save}
|
onClick={save}
|
||||||
src={IMAGES.save}
|
src={IMAGES.save}
|
||||||
alt="save"
|
alt="save"
|
||||||
@@ -338,7 +338,7 @@ export default function TextSpeakerPage() {
|
|||||||
</div>
|
</div>
|
||||||
{/* 保存列表 */}
|
{/* 保存列表 */}
|
||||||
{showSaveList && (
|
{showSaveList && (
|
||||||
<div className="mt-4 border border-gray-200 rounded-2xl overflow-hidden">
|
<div className="mt-4 border border-gray-200 rounded-lg overflow-hidden">
|
||||||
<SaveList show={showSaveList} handleUse={handleUseItem}></SaveList>
|
<SaveList show={showSaveList} handleUse={handleUseItem}></SaveList>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { LightButton, PrimaryButton } from "@/components/ui/buttons";
|
import { LightButton, PrimaryButton, IconClick } from "@/design-system/base/button";
|
||||||
import { IconClick } from "@/components/ui/buttons";
|
|
||||||
import { IMAGES } from "@/config/images";
|
import { IMAGES } from "@/config/images";
|
||||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -101,7 +100,7 @@ export default function TranslatorPage() {
|
|||||||
{/* Card Component - Left Side */}
|
{/* Card Component - Left Side */}
|
||||||
<div className="w-full md:w-1/2 flex flex-col-reverse gap-2">
|
<div className="w-full md:w-1/2 flex flex-col-reverse gap-2">
|
||||||
{/* ICard1 Component */}
|
{/* ICard1 Component */}
|
||||||
<div className="border border-gray-200 rounded-2xl w-full h-64 p-2">
|
<div className="border border-gray-200 rounded-lg w-full h-64 p-2">
|
||||||
<textarea
|
<textarea
|
||||||
className="resize-none h-8/12 w-full focus:outline-0"
|
className="resize-none h-8/12 w-full focus:outline-0"
|
||||||
ref={taref}
|
ref={taref}
|
||||||
@@ -147,7 +146,7 @@ export default function TranslatorPage() {
|
|||||||
{/* Card Component - Right Side */}
|
{/* Card Component - Right Side */}
|
||||||
<div className="w-full md:w-1/2 flex flex-col-reverse gap-2">
|
<div className="w-full md:w-1/2 flex flex-col-reverse gap-2">
|
||||||
{/* ICard2 Component */}
|
{/* ICard2 Component */}
|
||||||
<div className="bg-gray-100 rounded-2xl w-full h-64 p-2">
|
<div className="bg-gray-100 rounded-lg w-full h-64 p-2">
|
||||||
<div className="h-2/3 w-full overflow-y-auto">{translationResult?.translatedText || ""}</div>
|
<div className="h-2/3 w-full overflow-y-auto">{translationResult?.translatedText || ""}</div>
|
||||||
<div className="ipa w-full h-1/6 overflow-y-auto text-gray-600">
|
<div className="ipa w-full h-1/6 overflow-y-auto text-gray-600">
|
||||||
{translationResult?.targetIpa || ""}
|
{translationResult?.targetIpa || ""}
|
||||||
@@ -213,7 +212,8 @@ export default function TranslatorPage() {
|
|||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={translate}
|
onClick={translate}
|
||||||
disabled={processing}
|
disabled={processing}
|
||||||
className="text-xl h-16 px-8"
|
size="lg"
|
||||||
|
className="text-xl"
|
||||||
>
|
>
|
||||||
{t("translate")}
|
{t("translate")}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { useState, useActionState, startTransition } from "react";
|
import { useState, useActionState, startTransition } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { Input } from "@/components/ui/Input";
|
import { Input } from "@/design-system/base/input";
|
||||||
import { LightButton, LinkButton } from "@/components/ui/buttons";
|
import { LightButton, LinkButton } from "@/design-system/base/button";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { actionSignIn, actionSignUp, ActionOutputAuth } from "@/modules/auth/auth-action";
|
import { actionSignIn, actionSignUp, ActionOutputAuth } from "@/modules/auth/auth-action";
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
FolderPlus,
|
FolderPlus,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CircleButton, DashedButton } from "@/components/ui/buttons";
|
import { CircleButton, DashedButton } from "@/design-system/base/button";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -36,7 +36,7 @@ const FolderCard = ({ folder, refresh }: FolderProps) => {
|
|||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 flex-1">
|
<div className="flex items-center gap-3 flex-1">
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<Fd className="text-gray-600" size={24} />
|
<Fd className="text-gray-600" size="md" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -167,13 +167,13 @@ export function FoldersClient({ userId }: { userId: string; }) {
|
|||||||
// 空状态
|
// 空状态
|
||||||
<div className="text-center py-12 text-gray-400">
|
<div className="text-center py-12 text-gray-400">
|
||||||
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
<div className="w-16 h-16 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
|
||||||
<FolderPlus size={24} className="text-gray-400" />
|
<FolderPlus size="md" className="text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm">{t("noFoldersYet")}</p>
|
<p className="text-sm">{t("noFoldersYet")}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 文件夹卡片列表
|
// 文件夹卡片列表
|
||||||
<div className="rounded-xl border border-gray-200 overflow-hidden">
|
<div className="rounded-md border border-gray-200 overflow-hidden">
|
||||||
{folders
|
{folders
|
||||||
.toSorted((a, b) => a.id - b.id)
|
.toSorted((a, b) => a.id - b.id)
|
||||||
.map((folder) => (
|
.map((folder) => (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { Input } from "@/components/ui/Input";
|
import { Input } from "@/design-system/base/input";
|
||||||
import { LocaleSelector } from "@/components/ui/LocaleSelector";
|
import { LocaleSelector } from "@/components/ui/LocaleSelector";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
@@ -67,7 +67,7 @@ export function AddTextPairModal({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
|
<div className="bg-white rounded-md p-6 w-full max-w-md mx-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<h2 className="flex-1 text-xl font-light mb-4 text-center">
|
<h2 className="flex-1 text-xl font-light mb-4 text-center">
|
||||||
{t("addNewTextPair")}
|
{t("addNewTextPair")}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AddTextPairModal } from "./AddTextPairModal";
|
|||||||
import { TextPairCard } from "./TextPairCard";
|
import { TextPairCard } from "./TextPairCard";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { PrimaryButton, IconButton, LinkButton } from "@/components/ui/buttons";
|
import { PrimaryButton, IconButton, LinkButton } from "@/design-system/base/button";
|
||||||
import { CardList } from "@/components/ui/CardList";
|
import { CardList } from "@/components/ui/CardList";
|
||||||
import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder/folder-aciton";
|
import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder/folder-aciton";
|
||||||
import { TSharedPair } from "@/shared/folder-type";
|
import { TSharedPair } from "@/shared/folder-type";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Edit, Trash2 } from "lucide-react";
|
import { Edit, Trash2 } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { CircleButton } from "@/components/ui/buttons";
|
import { CircleButton } from "@/design-system/base/button";
|
||||||
import { UpdateTextPairModal } from "./UpdateTextPairModal";
|
import { UpdateTextPairModal } from "./UpdateTextPairModal";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { TSharedPair } from "@/shared/folder-type";
|
import { TSharedPair } from "@/shared/folder-type";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { Input } from "@/components/ui/Input";
|
import { Input } from "@/design-system/base/input";
|
||||||
import { LocaleSelector } from "@/components/ui/LocaleSelector";
|
import { LocaleSelector } from "@/components/ui/LocaleSelector";
|
||||||
import { X } from "lucide-react";
|
import { X } from "lucide-react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
@@ -63,7 +63,7 @@ export function UpdateTextPairModal({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
|
<div className="bg-white rounded-md p-6 w-full max-w-md mx-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<h2 className="flex-1 text-xl font-light mb-4 text-center">
|
<h2 className="flex-1 text-xl font-light mb-4 text-center">
|
||||||
{t("updateTextPair")}
|
{t("updateTextPair")}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Design System CSS 变量
|
* Tailwind CSS v4 主题配置
|
||||||
*
|
* 使用 @theme 指令定义主题变量
|
||||||
* 定义全局 CSS 变量用于主题切换和动态样式
|
|
||||||
*/
|
*/
|
||||||
:root {
|
@theme {
|
||||||
/* 颜色系统 */
|
/* 主色 - Teal */
|
||||||
--color-primary-50: #f0f9f8;
|
--color-primary-50: #f0f9f8;
|
||||||
--color-primary-100: #e0f2f0;
|
--color-primary-100: #e0f2f0;
|
||||||
--color-primary-200: #bce6e1;
|
--color-primary-200: #bce6e1;
|
||||||
@@ -19,12 +18,88 @@
|
|||||||
--color-primary-900: #122826;
|
--color-primary-900: #122826;
|
||||||
--color-primary-950: #0a1413;
|
--color-primary-950: #0a1413;
|
||||||
|
|
||||||
/* 语义色 */
|
/* 中性色 */
|
||||||
--color-success-500: #22c55e;
|
--color-gray-50: #f9fafb;
|
||||||
--color-warning-500: #f59e0b;
|
--color-gray-100: #f3f4f6;
|
||||||
--color-error-500: #ef4444;
|
--color-gray-200: #e5e7eb;
|
||||||
--color-info-500: #3b82f6;
|
--color-gray-300: #d1d5db;
|
||||||
|
--color-gray-400: #9ca3af;
|
||||||
|
--color-gray-500: #6b7280;
|
||||||
|
--color-gray-600: #4b5563;
|
||||||
|
--color-gray-700: #374151;
|
||||||
|
--color-gray-800: #1f2937;
|
||||||
|
--color-gray-900: #111827;
|
||||||
|
--color-gray-950: #030712;
|
||||||
|
|
||||||
|
/* 语义色 - Success */
|
||||||
|
--color-success-50: #f0fdf4;
|
||||||
|
--color-success-100: #dcfce7;
|
||||||
|
--color-success-200: #bbf7d0;
|
||||||
|
--color-success-300: #86efac;
|
||||||
|
--color-success-400: #4ade80;
|
||||||
|
--color-success-500: #22c55e;
|
||||||
|
--color-success-600: #16a34a;
|
||||||
|
--color-success-700: #15803d;
|
||||||
|
--color-success-800: #166534;
|
||||||
|
--color-success-900: #14532d;
|
||||||
|
--color-success-950: #052e16;
|
||||||
|
|
||||||
|
/* 语义色 - Warning */
|
||||||
|
--color-warning-50: #fffbeb;
|
||||||
|
--color-warning-100: #fef3c7;
|
||||||
|
--color-warning-200: #fde68a;
|
||||||
|
--color-warning-300: #fcd34d;
|
||||||
|
--color-warning-400: #fbbf24;
|
||||||
|
--color-warning-500: #f59e0b;
|
||||||
|
--color-warning-600: #d97706;
|
||||||
|
--color-warning-700: #b45309;
|
||||||
|
--color-warning-800: #92400e;
|
||||||
|
--color-warning-900: #78350f;
|
||||||
|
--color-warning-950: #451a03;
|
||||||
|
|
||||||
|
/* 语义色 - Error */
|
||||||
|
--color-error-50: #fef2f2;
|
||||||
|
--color-error-100: #fee2e2;
|
||||||
|
--color-error-200: #fecaca;
|
||||||
|
--color-error-300: #fca5a5;
|
||||||
|
--color-error-400: #f87171;
|
||||||
|
--color-error-500: #ef4444;
|
||||||
|
--color-error-600: #dc2626;
|
||||||
|
--color-error-700: #b91c1c;
|
||||||
|
--color-error-800: #991b1b;
|
||||||
|
--color-error-900: #7f1d1d;
|
||||||
|
--color-error-950: #450a0a;
|
||||||
|
|
||||||
|
/* 语义色 - Info */
|
||||||
|
--color-info-50: #eff6ff;
|
||||||
|
--color-info-100: #dbeafe;
|
||||||
|
--color-info-200: #bfdbfe;
|
||||||
|
--color-info-300: #93c5fd;
|
||||||
|
--color-info-400: #60a5fa;
|
||||||
|
--color-info-500: #3b82f6;
|
||||||
|
--color-info-600: #2563eb;
|
||||||
|
--color-info-700: #1d4ed8;
|
||||||
|
--color-info-800: #1e40af;
|
||||||
|
--color-info-900: #1e3a8a;
|
||||||
|
--color-info-950: #172554;
|
||||||
|
|
||||||
|
/* 圆角 - 更小的圆角 */
|
||||||
|
--radius-xs: 0.125rem;
|
||||||
|
--radius-sm: 0.25rem;
|
||||||
|
--radius-md: 0.375rem;
|
||||||
|
--radius-lg: 0.5rem;
|
||||||
|
--radius-xl: 0.625rem;
|
||||||
|
--radius-2xl: 0.75rem;
|
||||||
|
--radius-3xl: 1rem;
|
||||||
|
--radius-full: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design System CSS 变量
|
||||||
|
*
|
||||||
|
* 定义全局 CSS 变量用于主题切换和动态样式
|
||||||
|
*/
|
||||||
|
:root {
|
||||||
/* 基础颜色 */
|
/* 基础颜色 */
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #111827;
|
--foreground: #111827;
|
||||||
@@ -41,13 +116,14 @@
|
|||||||
--border-secondary: #e5e7eb;
|
--border-secondary: #e5e7eb;
|
||||||
--border-focus: #35786f;
|
--border-focus: #35786f;
|
||||||
|
|
||||||
/* 圆角 */
|
/* 圆角 - 更小的圆角 */
|
||||||
--radius-sm: 0.125rem;
|
--radius-xs: 0.125rem;
|
||||||
|
--radius-sm: 0.25rem;
|
||||||
--radius-md: 0.375rem;
|
--radius-md: 0.375rem;
|
||||||
--radius-lg: 0.5rem;
|
--radius-lg: 0.5rem;
|
||||||
--radius-xl: 0.75rem;
|
--radius-xl: 0.625rem;
|
||||||
--radius-2xl: 1rem;
|
--radius-2xl: 0.75rem;
|
||||||
--radius-3xl: 1.5rem;
|
--radius-3xl: 1rem;
|
||||||
--radius-full: 9999px;
|
--radius-full: 9999px;
|
||||||
|
|
||||||
/* 阴影 */
|
/* 阴影 */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { LightButton } from "@/components/ui/buttons";
|
import { LightButton } from "@/design-system/base/button";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { LinkButton } from "@/components/ui/buttons";
|
import { LinkButton } from "@/design-system/base/button";
|
||||||
import { actionGetUserProfileByUsername } from "@/modules/auth/auth-action";
|
import { actionGetUserProfileByUsername } from "@/modules/auth/auth-action";
|
||||||
import { repoGetFoldersWithTotalPairsByUserId } from "@/modules/folder/folder-repository";
|
import { repoGetFoldersWithTotalPairsByUserId } from "@/modules/folder/folder-repository";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { GhostButton } from "../ui/buttons";
|
import { GhostLightButton } from "@/design-system/base/button";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Languages } from "lucide-react";
|
import { Languages } from "lucide-react";
|
||||||
|
|
||||||
@@ -15,59 +15,59 @@ export function LanguageSettings() {
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Languages onClick={handleLanguageClick} size={28} className="navbar-btn" />
|
<Languages onClick={handleLanguageClick} size={28} className="text-white hover:text-white/80" />
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{showLanguageMenu && (
|
{showLanguageMenu && (
|
||||||
<div>
|
<div>
|
||||||
<div className="absolute top-10 right-0 rounded-md shadow-md flex flex-col gap-2">
|
<div className="absolute top-10 right-0 rounded-md shadow-md flex flex-col gap-2">
|
||||||
<GhostButton
|
<GhostLightButton
|
||||||
className="w-full bg-[#35786f]"
|
className="w-full bg-[#35786f]"
|
||||||
onClick={() => setLocale("en-US")}
|
onClick={() => setLocale("en-US")}
|
||||||
>
|
>
|
||||||
English
|
English
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<GhostButton
|
<GhostLightButton
|
||||||
className="w-full bg-[#35786f]"
|
className="w-full bg-[#35786f]"
|
||||||
onClick={() => setLocale("zh-CN")}
|
onClick={() => setLocale("zh-CN")}
|
||||||
>
|
>
|
||||||
中文
|
中文
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<GhostButton
|
<GhostLightButton
|
||||||
className="w-full bg-[#35786f]"
|
className="w-full bg-[#35786f]"
|
||||||
onClick={() => setLocale("ja-JP")}
|
onClick={() => setLocale("ja-JP")}
|
||||||
>
|
>
|
||||||
日本語
|
日本語
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<GhostButton
|
<GhostLightButton
|
||||||
className="w-full bg-[#35786f]"
|
className="w-full bg-[#35786f]"
|
||||||
onClick={() => setLocale("ko-KR")}
|
onClick={() => setLocale("ko-KR")}
|
||||||
>
|
>
|
||||||
한국어
|
한국어
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<GhostButton
|
<GhostLightButton
|
||||||
className="w-full bg-[#35786f]"
|
className="w-full bg-[#35786f]"
|
||||||
onClick={() => setLocale("de-DE")}
|
onClick={() => setLocale("de-DE")}
|
||||||
>
|
>
|
||||||
Deutsch
|
Deutsch
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<GhostButton
|
<GhostLightButton
|
||||||
className="w-full bg-[#35786f]"
|
className="w-full bg-[#35786f]"
|
||||||
onClick={() => setLocale("fr-FR")}
|
onClick={() => setLocale("fr-FR")}
|
||||||
>
|
>
|
||||||
Français
|
Français
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<GhostButton
|
<GhostLightButton
|
||||||
className="w-full bg-[#35786f]"
|
className="w-full bg-[#35786f]"
|
||||||
onClick={() => setLocale("it-IT")}
|
onClick={() => setLocale("it-IT")}
|
||||||
>
|
>
|
||||||
Italiano
|
Italiano
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<GhostButton
|
<GhostLightButton
|
||||||
className="w-full bg-[#35786f]"
|
className="w-full bg-[#35786f]"
|
||||||
onClick={() => setLocale("ug-CN")}
|
onClick={() => setLocale("ug-CN")}
|
||||||
>
|
>
|
||||||
ئۇيغۇرچە
|
ئۇيغۇرچە
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { LanguageSettings } from "./LanguageSettings";
|
|||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { GhostButton } from "../ui/buttons";
|
import { GhostLightButton } from "@/design-system/base/button";
|
||||||
|
|
||||||
export async function Navbar() {
|
export async function Navbar() {
|
||||||
const t = await getTranslations("navbar");
|
const t = await getTranslations("navbar");
|
||||||
@@ -15,16 +15,17 @@ export async function Navbar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center w-full h-16 px-4 md:px-8 bg-[#35786f] text-white">
|
<div className="flex justify-between items-center w-full h-16 px-4 md:px-8 bg-[#35786f] text-white">
|
||||||
<GhostButton href="/" className="text-lg md:text-xl border-b hidden! md:block!">
|
<GhostLightButton href="/" className="border-b hidden! md:block!" size="md">
|
||||||
{t("title")}
|
{t("title")}
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<GhostButton className="block! md:hidden!" href={"/"}>
|
<GhostLightButton className="block! md:hidden!" size="md" href={"/"}>
|
||||||
<Home size={20} />
|
<Home size={20} />
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<div className="flex text-base md:text-xl gap-0.5 justify-center items-center flex-wrap">
|
<div className="flex gap-0.5 justify-center items-center flex-wrap">
|
||||||
<LanguageSettings />
|
<LanguageSettings />
|
||||||
<GhostButton
|
<GhostLightButton
|
||||||
className="md:hidden! block! navbar-btn p-2"
|
className="md:hidden! block!"
|
||||||
|
size="md"
|
||||||
href="https://github.com/GoddoNebianU/learn-languages"
|
href="https://github.com/GoddoNebianU/learn-languages"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -33,33 +34,34 @@ export async function Navbar() {
|
|||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<GhostButton href="/folders" className="md:block! hidden! navbar-btn">
|
<GhostLightButton href="/folders" className="md:block! hidden!" size="md">
|
||||||
{t("folders")}
|
{t("folders")}
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<GhostButton href="/folders" className="md:hidden! block! navbar-btn p-2">
|
<GhostLightButton href="/folders" className="md:hidden! block!" size="md">
|
||||||
<Folder size={20} />
|
<Folder size={20} />
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
<GhostButton
|
<GhostLightButton
|
||||||
className="hidden! md:block! navbar-btn"
|
className="hidden! md:block!"
|
||||||
|
size="md"
|
||||||
href="https://github.com/GoddoNebianU/learn-languages"
|
href="https://github.com/GoddoNebianU/learn-languages"
|
||||||
>
|
>
|
||||||
{t("sourceCode")}
|
{t("sourceCode")}
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
{
|
{
|
||||||
(() => {
|
(() => {
|
||||||
return session &&
|
return session &&
|
||||||
<>
|
<>
|
||||||
<GhostButton href="/profile" className="hidden! md:block! text-sm md:text-base navbar-btn px-2 py-1">{t("profile")}</GhostButton>
|
<GhostLightButton href="/profile" className="hidden! md:block!" size="md">{t("profile")}</GhostLightButton>
|
||||||
<GhostButton href="/profile" className="md:hidden! block! navbar-btn p-2">
|
<GhostLightButton href="/profile" className="md:hidden! block!" size="md">
|
||||||
<User size={20} />
|
<User size={20} />
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
</>
|
</>
|
||||||
|| <>
|
|| <>
|
||||||
<GhostButton href="/auth" className="hidden! md:block! text-sm md:text-base navbar-btn px-2 py-1">{t("sign_in")}</GhostButton>
|
<GhostLightButton href="/auth" className="hidden! md:block!" size="md">{t("sign_in")}</GhostLightButton>
|
||||||
<GhostButton href="/auth" className="md:hidden! block! navbar-btn p-2">
|
<GhostLightButton href="/auth" className="md:hidden! block!" size="md">
|
||||||
<User size={20} />
|
<User size={20} />
|
||||||
</GhostButton>
|
</GhostLightButton>
|
||||||
</>;
|
</>;
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,180 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export type ButtonVariant = "primary" | "secondary" | "ghost" | "icon" | "circle" | "dashed" | "link";
|
|
||||||
export type ButtonSize = "sm" | "md" | "lg";
|
|
||||||
|
|
||||||
export interface ButtonProps {
|
|
||||||
// Content
|
|
||||||
children?: React.ReactNode;
|
|
||||||
|
|
||||||
// Behavior
|
|
||||||
onClick?: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
type?: "button" | "submit" | "reset";
|
|
||||||
|
|
||||||
// Styling
|
|
||||||
variant?: ButtonVariant;
|
|
||||||
size?: ButtonSize;
|
|
||||||
className?: string;
|
|
||||||
selected?: boolean;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
|
|
||||||
// Icons
|
|
||||||
leftIcon?: React.ReactNode;
|
|
||||||
rightIcon?: React.ReactNode;
|
|
||||||
iconSrc?: string; // For Next.js Image icons
|
|
||||||
iconAlt?: string;
|
|
||||||
|
|
||||||
// Navigation
|
|
||||||
href?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Button({
|
|
||||||
variant = "secondary",
|
|
||||||
size = "md",
|
|
||||||
selected = false,
|
|
||||||
href,
|
|
||||||
iconSrc,
|
|
||||||
iconAlt,
|
|
||||||
leftIcon,
|
|
||||||
rightIcon,
|
|
||||||
children,
|
|
||||||
className = "",
|
|
||||||
style,
|
|
||||||
type = "button",
|
|
||||||
disabled = false,
|
|
||||||
...props
|
|
||||||
}: ButtonProps) {
|
|
||||||
// Base classes
|
|
||||||
const baseClasses = "inline-flex items-center justify-center gap-2 rounded font-bold shadow hover:cursor-pointer transition-colors";
|
|
||||||
|
|
||||||
// Variant-specific classes
|
|
||||||
const variantStyles: Record<ButtonVariant, string> = {
|
|
||||||
primary: `
|
|
||||||
text-white
|
|
||||||
hover:opacity-90
|
|
||||||
`,
|
|
||||||
secondary: `
|
|
||||||
text-black
|
|
||||||
hover:bg-gray-100
|
|
||||||
`,
|
|
||||||
ghost: `
|
|
||||||
hover:bg-black/30
|
|
||||||
p-2
|
|
||||||
`,
|
|
||||||
icon: `
|
|
||||||
p-2 bg-gray-200 rounded-full
|
|
||||||
hover:bg-gray-300
|
|
||||||
`,
|
|
||||||
circle: `
|
|
||||||
p-2 rounded-full
|
|
||||||
hover:bg-gray-200
|
|
||||||
`,
|
|
||||||
dashed: `
|
|
||||||
border-2 border-dashed border-gray-300
|
|
||||||
text-gray-500
|
|
||||||
hover:border-gray-400
|
|
||||||
hover:text-gray-600
|
|
||||||
bg-transparent
|
|
||||||
shadow-none
|
|
||||||
`,
|
|
||||||
link: `
|
|
||||||
text-[#35786f]
|
|
||||||
hover:underline
|
|
||||||
p-0
|
|
||||||
shadow-none
|
|
||||||
`
|
|
||||||
};
|
|
||||||
|
|
||||||
// Size-specific classes
|
|
||||||
const sizeStyles: Record<ButtonSize, string> = {
|
|
||||||
sm: "px-3 py-1 text-sm",
|
|
||||||
md: "px-4 py-2",
|
|
||||||
lg: "px-6 py-3 text-lg"
|
|
||||||
};
|
|
||||||
|
|
||||||
const variantClass = variantStyles[variant];
|
|
||||||
const sizeClass = sizeStyles[size];
|
|
||||||
|
|
||||||
// Selected state for secondary variant
|
|
||||||
const selectedClass = variant === "secondary" && selected ? "bg-gray-100" : "";
|
|
||||||
|
|
||||||
// Background color for primary variant
|
|
||||||
const backgroundColor = variant === "primary" ? '#35786f' : undefined;
|
|
||||||
|
|
||||||
// Combine all classes
|
|
||||||
const combinedClasses = `
|
|
||||||
${baseClasses}
|
|
||||||
${variantClass}
|
|
||||||
${sizeClass}
|
|
||||||
${selectedClass}
|
|
||||||
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
||||||
${className}
|
|
||||||
`.trim().replace(/\s+/g, " ");
|
|
||||||
|
|
||||||
// Icon rendering helper for SVG icons
|
|
||||||
const renderSvgIcon = (icon: React.ReactNode, position: "left" | "right") => {
|
|
||||||
if (!icon) return null;
|
|
||||||
return (
|
|
||||||
<span className={`flex items-center ${position === "left" ? "-ml-1 mr-2" : "-mr-1 ml-2"}`}>
|
|
||||||
{icon}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Image icon rendering for Next.js Image
|
|
||||||
const renderImageIcon = () => {
|
|
||||||
if (!iconSrc) return null;
|
|
||||||
const sizeMap = { sm: 16, md: 20, lg: 24 };
|
|
||||||
const imgSize = sizeMap[size] || 20;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
src={iconSrc}
|
|
||||||
width={imgSize}
|
|
||||||
height={imgSize}
|
|
||||||
alt={iconAlt || "icon"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Content assembly
|
|
||||||
const content = (
|
|
||||||
<>
|
|
||||||
{renderImageIcon()}
|
|
||||||
{renderSvgIcon(leftIcon, "left")}
|
|
||||||
{children}
|
|
||||||
{renderSvgIcon(rightIcon, "right")}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
// If href is provided, render as Link
|
|
||||||
if (href) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={href}
|
|
||||||
className={combinedClasses}
|
|
||||||
style={{ ...style, backgroundColor }}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise render as button
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type={type}
|
|
||||||
disabled={disabled}
|
|
||||||
className={combinedClasses}
|
|
||||||
style={{ ...style, backgroundColor }}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
/**
|
|
||||||
* Card - 可复用的卡片组件
|
|
||||||
*
|
|
||||||
* 提供应用统一的标准白色卡片样式:
|
|
||||||
* - 白色背景
|
|
||||||
* - 圆角 (rounded-2xl)
|
|
||||||
* - 阴影 (shadow-xl)
|
|
||||||
* - 可配置内边距
|
|
||||||
* - 多种样式变体
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* // 默认卡片
|
|
||||||
* <Card>
|
|
||||||
* <p>卡片内容</p>
|
|
||||||
* </Card>
|
|
||||||
*
|
|
||||||
* // 带边框的卡片
|
|
||||||
* <Card variant="bordered" padding="lg">
|
|
||||||
* <p>带边框的内容</p>
|
|
||||||
* </Card>
|
|
||||||
*
|
|
||||||
* // 无内边距卡片
|
|
||||||
* <Card padding="none">
|
|
||||||
* <img src="image.jpg" alt="完全填充的图片" />
|
|
||||||
* </Card>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export type CardVariant = "default" | "bordered" | "elevated";
|
|
||||||
export type CardPadding = "none" | "sm" | "md" | "lg" | "xl";
|
|
||||||
|
|
||||||
export interface CardProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
/** 额外的 CSS 类名,用于自定义样式 */
|
|
||||||
className?: string;
|
|
||||||
/** 卡片样式变体 */
|
|
||||||
variant?: CardVariant;
|
|
||||||
/** 内边距大小 */
|
|
||||||
padding?: CardPadding;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 变体样式映射
|
|
||||||
const variantClasses: Record<CardVariant, string> = {
|
|
||||||
default: "bg-white shadow-xl",
|
|
||||||
bordered: "bg-white border-2 border-gray-200",
|
|
||||||
elevated: "bg-white shadow-2xl",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 内边距映射
|
|
||||||
const paddingClasses: Record<CardPadding, string> = {
|
|
||||||
none: "",
|
|
||||||
sm: "p-4",
|
|
||||||
md: "p-6",
|
|
||||||
lg: "p-8",
|
|
||||||
xl: "p-8 md:p-12",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Card({
|
|
||||||
children,
|
|
||||||
className = "",
|
|
||||||
variant = "default",
|
|
||||||
padding = "md",
|
|
||||||
}: CardProps) {
|
|
||||||
const baseClasses = "rounded-2xl";
|
|
||||||
const variantClass = variantClasses[variant];
|
|
||||||
const paddingClass = paddingClasses[padding];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${baseClasses} ${variantClass} ${paddingClass} ${className}`}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* CardList - 可滚动的卡片列表容器
|
* CardList - 可滚动的卡片列表容器
|
||||||
*
|
*
|
||||||
* 用于显示可滚动的列表内容,如文件夹列表、文本对列表等
|
* 使用 Design System 重写的卡片列表组件
|
||||||
* - 最大高度 96 (24rem)
|
|
||||||
* - 垂直滚动
|
|
||||||
* - 圆角边框
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* <CardList>
|
|
||||||
* {items.map(item => (
|
|
||||||
* <div key={item.id}>{item.name}</div>
|
|
||||||
* ))}
|
|
||||||
* </CardList>
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
|
import { VStack } from "@/design-system/layout/stack";
|
||||||
|
|
||||||
interface CardListProps {
|
interface CardListProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
/** 额外的 CSS 类名 */
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CardList({ children, className = "" }: CardListProps) {
|
export function CardList({ children, className = "" }: CardListProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`max-h-96 overflow-y-auto rounded-xl border border-gray-200 overflow-hidden ${className}`}>
|
<div className={`max-h-96 overflow-y-auto rounded-lg border-2 border-gray-200 ${className}`}>
|
||||||
{children}
|
<VStack gap={0}>
|
||||||
|
{children}
|
||||||
|
</VStack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Container - 容器组件
|
||||||
|
*
|
||||||
|
* 使用 Design System 重写的容器组件
|
||||||
|
*/
|
||||||
|
import { Container as DSContainer } from "@/design-system/layout/container";
|
||||||
|
import { Card } from "@/design-system/base/card";
|
||||||
|
|
||||||
interface ContainerProps {
|
interface ContainerProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Container({ children, className }: ContainerProps) {
|
export function Container({ children, className = "" }: ContainerProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<DSContainer size="2xl" className={`mx-auto ${className}`}>
|
||||||
className={`w-full max-w-2xl mx-auto bg-white border border-gray-200 rounded-2xl ${className}`}
|
<Card variant="bordered" padding="md">
|
||||||
>
|
{children}
|
||||||
{children}
|
</Card>
|
||||||
</div>
|
</DSContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
export type InputVariant = "default" | "search" | "bordered" | "filled";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
ref?: React.Ref<HTMLInputElement>;
|
|
||||||
placeholder?: string;
|
|
||||||
type?: string;
|
|
||||||
className?: string;
|
|
||||||
name?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
value?: string;
|
|
||||||
variant?: InputVariant;
|
|
||||||
required?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Input({
|
|
||||||
ref,
|
|
||||||
placeholder = "",
|
|
||||||
type = "text",
|
|
||||||
className = "",
|
|
||||||
name = "",
|
|
||||||
defaultValue = "",
|
|
||||||
value,
|
|
||||||
variant = "default",
|
|
||||||
required = false,
|
|
||||||
disabled = false,
|
|
||||||
onChange,
|
|
||||||
}: Props) {
|
|
||||||
// Variant-specific classes
|
|
||||||
const variantStyles: Record<InputVariant, string> = {
|
|
||||||
default: "block focus:outline-none border-b-2 border-gray-600",
|
|
||||||
search: "flex-1 min-w-0 px-4 py-3 text-lg text-gray-800 focus:outline-none border-b-2 border-gray-600 bg-white/90 rounded",
|
|
||||||
bordered: "w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f]",
|
|
||||||
filled: "w-full px-3 py-2 bg-gray-100 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f]"
|
|
||||||
};
|
|
||||||
|
|
||||||
const variantClass = variantStyles[variant];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
placeholder={placeholder}
|
|
||||||
type={type}
|
|
||||||
className={`${variantClass} ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
|
||||||
name={name}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
value={value}
|
|
||||||
required={required}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={onChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* LocaleSelector - 语言选择器组件
|
||||||
|
*
|
||||||
|
* 使用 Design System 重写的语言选择器组件
|
||||||
|
*/
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Input } from "@/components/ui/Input";
|
import { Input } from "@/design-system/base/input";
|
||||||
import { Select, Option } from "@/components/ui/Select";
|
import { Select } from "@/design-system/base/select";
|
||||||
|
import { VStack } from "@/design-system/layout/stack";
|
||||||
|
|
||||||
const COMMON_LANGUAGES = [
|
const COMMON_LANGUAGES = [
|
||||||
{ label: "chinese", value: "chinese" },
|
{ label: "chinese", value: "chinese" },
|
||||||
@@ -38,7 +44,8 @@ export function LocaleSelector({ value, onChange }: LocaleSelectorProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 当选择常见语言或"其他"时
|
// 当选择常见语言或"其他"时
|
||||||
const handleSelectChange = (selectedValue: string) => {
|
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const selectedValue = e.target.value;
|
||||||
if (selectedValue === "other") {
|
if (selectedValue === "other") {
|
||||||
setCustomInput("");
|
setCustomInput("");
|
||||||
onChange("other");
|
onChange("other");
|
||||||
@@ -48,15 +55,15 @@ export function LocaleSelector({ value, onChange }: LocaleSelectorProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<VStack gap={2}>
|
||||||
<Select
|
<Select
|
||||||
value={isCommonLanguage ? value : "other"}
|
value={isCommonLanguage ? value : "other"}
|
||||||
onChange={handleSelectChange}
|
onChange={handleSelectChange}
|
||||||
>
|
>
|
||||||
{COMMON_LANGUAGES.map((lang) => (
|
{COMMON_LANGUAGES.map((lang) => (
|
||||||
<Option key={lang.value} value={lang.value}>
|
<option key={lang.value} value={lang.value}>
|
||||||
{t(`translator.${lang.label}`)}
|
{t(`translator.${lang.label}`)}
|
||||||
</Option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
{showCustomInput && (
|
{showCustomInput && (
|
||||||
@@ -66,9 +73,8 @@ export function LocaleSelector({ value, onChange }: LocaleSelectorProps) {
|
|||||||
onChange={(e) => handleCustomInputChange(e.target.value)}
|
onChange={(e) => handleCustomInputChange(e.target.value)}
|
||||||
placeholder={t("folder_id.enterLanguageName")}
|
placeholder={t("folder_id.enterLanguageName")}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
className="mt-2"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* PageHeader - 页面标题组件
|
* PageHeader - 页面标题组件
|
||||||
*
|
*
|
||||||
* 用于 PageLayout 内的页面标题,支持主标题和可选的副标题
|
* 使用 Design System 重写的页面标题组件
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* <PageHeader title="我的文件夹" subtitle="管理和组织你的学习内容" />
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
|
import { VStack } from "@/design-system/layout/stack";
|
||||||
|
|
||||||
interface PageHeaderProps {
|
interface PageHeaderProps {
|
||||||
/** 页面主标题 */
|
|
||||||
title: string;
|
title: string;
|
||||||
/** 可选的副标题/描述 */
|
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PageHeader({ title, subtitle }: PageHeaderProps) {
|
export function PageHeader({ title, subtitle, className = "" }: PageHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-6">
|
<VStack gap={2} className={`mb-6 ${className}`}>
|
||||||
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-2">
|
<h1 className="text-2xl md:text-3xl font-bold text-gray-800">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
{subtitle && (
|
{subtitle && (
|
||||||
<p className="text-sm text-gray-500">{subtitle}</p>
|
<p className="text-sm text-gray-500">{subtitle}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* PageLayout - 统一的页面布局组件
|
* PageLayout - 页面布局组件
|
||||||
*
|
*
|
||||||
* 提供应用统一的标准页面布局:
|
* 使用 Design System 重写的页面布局组件
|
||||||
* - 绿色背景 (#35786f)
|
|
||||||
* - 最小高度 min-h-[calc(100vh-64px)]
|
|
||||||
* - 支持多种布局变体
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* // 默认:居中白色卡片布局
|
|
||||||
* <PageLayout>
|
|
||||||
* <PageHeader title="标题" subtitle="副标题" />
|
|
||||||
* <div>页面内容</div>
|
|
||||||
* </PageLayout>
|
|
||||||
*
|
|
||||||
* // 全宽布局(无白色卡片)
|
|
||||||
* <PageLayout variant="full-width" maxWidth="3xl">
|
|
||||||
* <div>页面内容</div>
|
|
||||||
* </PageLayout>
|
|
||||||
*
|
|
||||||
* // 全屏布局(用于 translator 等)
|
|
||||||
* <PageLayout variant="fullscreen">
|
|
||||||
* <div>全屏内容</div>
|
|
||||||
* </PageLayout>
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
import { Card } from "./Card";
|
import { Card } from "@/design-system/base/card";
|
||||||
|
import { Container } from "@/design-system/layout/container";
|
||||||
|
|
||||||
type PageLayoutVariant = "centered-card" | "full-width" | "fullscreen";
|
type PageLayoutVariant = "centered-card" | "full-width" | "fullscreen";
|
||||||
type MaxWidth = "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "full";
|
|
||||||
type AlignItems = "center" | "start" | "end";
|
|
||||||
|
|
||||||
interface PageLayoutProps {
|
interface PageLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
/** 额外的 CSS 类名,用于自定义布局行为 */
|
|
||||||
className?: string;
|
className?: string;
|
||||||
/** 布局变体 */
|
|
||||||
variant?: PageLayoutVariant;
|
variant?: PageLayoutVariant;
|
||||||
/** 最大宽度(仅对 full-width 变体有效) */
|
align?: "center" | "start" | "end";
|
||||||
maxWidth?: MaxWidth;
|
|
||||||
/** 内容垂直对齐方式(仅对 centered-card 变体有效) */
|
|
||||||
align?: AlignItems;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最大宽度映射
|
const alignClasses = {
|
||||||
const maxWidthClasses: Record<MaxWidth, string> = {
|
|
||||||
sm: "max-w-sm",
|
|
||||||
md: "max-w-md",
|
|
||||||
lg: "max-w-lg",
|
|
||||||
xl: "max-w-xl",
|
|
||||||
"2xl": "max-w-2xl",
|
|
||||||
"3xl": "max-w-3xl",
|
|
||||||
full: "max-w-full",
|
|
||||||
};
|
|
||||||
|
|
||||||
// 对齐方式映射
|
|
||||||
const alignClasses: Record<AlignItems, string> = {
|
|
||||||
center: "items-center",
|
center: "items-center",
|
||||||
start: "items-start",
|
start: "items-start",
|
||||||
end: "items-end",
|
end: "items-end",
|
||||||
@@ -65,13 +25,12 @@ export function PageLayout({
|
|||||||
children,
|
children,
|
||||||
className = "",
|
className = "",
|
||||||
variant = "centered-card",
|
variant = "centered-card",
|
||||||
maxWidth = "2xl",
|
|
||||||
align = "center",
|
align = "center",
|
||||||
}: PageLayoutProps) {
|
}: PageLayoutProps) {
|
||||||
// 默认变体:居中白色卡片布局
|
// 居中卡片布局
|
||||||
if (variant === "centered-card") {
|
if (variant === "centered-card") {
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-[calc(100vh-64px)] bg-[#35786f] flex ${alignClasses[align]} justify-center px-4 py-8 ${className}`}>
|
<div className={`min-h-[calc(100vh-64px)] bg-primary-500 flex ${alignClasses[align]} justify-center px-4 py-8 ${className}`}>
|
||||||
<div className="w-full max-w-2xl">
|
<div className="w-full max-w-2xl">
|
||||||
<Card padding="lg" className="p-6 md:p-8">
|
<Card padding="lg" className="p-6 md:p-8">
|
||||||
{children}
|
{children}
|
||||||
@@ -81,21 +40,21 @@ export function PageLayout({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全宽布局:绿色背景,最大宽度容器,无白色卡片
|
// 全宽布局
|
||||||
if (variant === "full-width") {
|
if (variant === "full-width") {
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-[calc(100vh-64px)] bg-[#35786f] px-4 py-8 ${className}`}>
|
<div className={`min-h-[calc(100vh-64px)] bg-primary-500 px-4 py-8 ${className}`}>
|
||||||
<div className={`w-full ${maxWidthClasses[maxWidth]} mx-auto`}>
|
<Container size="2xl">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全屏布局:仅绿色背景,无其他限制
|
// 全屏布局
|
||||||
if (variant === "fullscreen") {
|
if (variant === "fullscreen") {
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-[calc(100vh-64px)] bg-[#35786f] ${className}`}>
|
<div className={`min-h-[calc(100vh-64px)] bg-primary-500 ${className}`}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ export function RangeInput({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={`w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
className={`w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-primary-500 ${
|
||||||
|
disabled ? "opacity-50 cursor-not-allowed" : ""
|
||||||
|
} ${className}`}
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(to right, #374151 0%, #374151 ${progressPercentage}%, #e5e7eb ${progressPercentage}%, #e5e7eb 100%)`
|
background: `linear-gradient(to right, #374151 0%, #374151 ${progressPercentage}%, #e5e7eb ${progressPercentage}%, #e5e7eb 100%)`
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
export type SelectSize = "sm" | "md" | "lg";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
value?: string;
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
size?: SelectSize;
|
|
||||||
disabled?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Select({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
children,
|
|
||||||
className = "",
|
|
||||||
size = "md",
|
|
||||||
disabled = false,
|
|
||||||
required = false,
|
|
||||||
}: Props) {
|
|
||||||
// Size-specific classes
|
|
||||||
const sizeStyles: Record<SelectSize, string> = {
|
|
||||||
sm: "px-2 py-1 text-sm",
|
|
||||||
md: "px-3 py-2 text-base",
|
|
||||||
lg: "px-4 py-3 text-lg"
|
|
||||||
};
|
|
||||||
|
|
||||||
const sizeClass = sizeStyles[size];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<select
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => onChange?.(e.target.value)}
|
|
||||||
className={`w-full border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f] ${sizeClass} ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
|
||||||
disabled={disabled}
|
|
||||||
required={required}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OptionProps {
|
|
||||||
value: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Option({ value, children, disabled = false }: OptionProps) {
|
|
||||||
return (
|
|
||||||
<option value={value} disabled={disabled}>
|
|
||||||
{children}
|
|
||||||
</option>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
export type TextareaVariant = "default" | "bordered" | "filled";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
value?: string;
|
|
||||||
defaultValue?: string;
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
className?: string;
|
|
||||||
variant?: TextareaVariant;
|
|
||||||
disabled?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
rows?: number;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Textarea({
|
|
||||||
value,
|
|
||||||
defaultValue,
|
|
||||||
onChange,
|
|
||||||
placeholder = "",
|
|
||||||
className = "",
|
|
||||||
variant = "default",
|
|
||||||
disabled = false,
|
|
||||||
required = false,
|
|
||||||
rows = 3,
|
|
||||||
name = "",
|
|
||||||
}: Props) {
|
|
||||||
// Variant-specific classes
|
|
||||||
const variantStyles: Record<TextareaVariant, string> = {
|
|
||||||
default: "block focus:outline-none border-b-2 border-gray-600 resize-none",
|
|
||||||
bordered: "w-full border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f] resize-none",
|
|
||||||
filled: "w-full bg-gray-100 rounded focus:outline-none focus:ring-2 focus:ring-[#35786f] resize-none"
|
|
||||||
};
|
|
||||||
|
|
||||||
const variantClass = variantStyles[variant];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<textarea
|
|
||||||
value={value}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
onChange={(e) => onChange?.(e.target.value)}
|
|
||||||
placeholder={placeholder}
|
|
||||||
className={`${variantClass} ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
|
||||||
disabled={disabled}
|
|
||||||
required={required}
|
|
||||||
rows={rows}
|
|
||||||
name={name}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
// 统一的按钮组件导出
|
|
||||||
// 基于 Button 组件的便捷包装器,提供语义化的按钮类型
|
|
||||||
|
|
||||||
import { Button } from "../Button";
|
|
||||||
|
|
||||||
// ========== 基础按钮 ==========
|
|
||||||
|
|
||||||
// PrimaryButton: 主要操作按钮(主题色)
|
|
||||||
export const PrimaryButton = (props: any) => <Button variant="primary" {...props} />;
|
|
||||||
|
|
||||||
// SecondaryButton: 次要按钮,支持 selected 状态
|
|
||||||
export const SecondaryButton = (props: any) => <Button variant="secondary" {...props} />;
|
|
||||||
|
|
||||||
// LightButton: 次要按钮的别名(向后兼容)
|
|
||||||
export const LightButton = SecondaryButton;
|
|
||||||
|
|
||||||
// ========== 图标按钮 ==========
|
|
||||||
|
|
||||||
// IconButton: SVG 图标按钮(方形背景)
|
|
||||||
export const IconButton = (props: any) => {
|
|
||||||
const { icon, ...rest } = props;
|
|
||||||
return <Button variant="icon" leftIcon={icon} {...rest} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
// IconClick: 图片图标按钮(支持 Next.js Image)
|
|
||||||
export const IconClick = (props: any) => {
|
|
||||||
const { src, alt, size, disableOnHoverBgChange, className, ...rest } = props;
|
|
||||||
let buttonSize: "sm" | "md" | "lg" = "md";
|
|
||||||
if (typeof size === "number") {
|
|
||||||
if (size <= 20) buttonSize = "sm";
|
|
||||||
else if (size >= 32) buttonSize = "lg";
|
|
||||||
} else if (typeof size === "string") {
|
|
||||||
buttonSize = (size === "sm" || size === "md" || size === "lg") ? size : "md";
|
|
||||||
}
|
|
||||||
|
|
||||||
const hoverClass = disableOnHoverBgChange ? "hover:bg-black/30 hover:cursor-pointer border-0 bg-transparent shadow-none" : "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
iconSrc={src}
|
|
||||||
iconAlt={alt}
|
|
||||||
size={buttonSize}
|
|
||||||
className={`${hoverClass} ${className || ""}`}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// CircleButton: 圆形图标按钮
|
|
||||||
export const CircleButton = (props: any) => {
|
|
||||||
const { icon, className, ...rest } = props;
|
|
||||||
return <Button variant="circle" leftIcon={icon} className={className} {...rest} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
// CircleToggleButton: 带选中状态的圆形切换按钮
|
|
||||||
export const CircleToggleButton = (props: any) => {
|
|
||||||
const { selected, className, children, ...rest } = props;
|
|
||||||
const selectedClass = selected
|
|
||||||
? "bg-[#35786f] text-white"
|
|
||||||
: "bg-gray-200 text-gray-600 hover:bg-gray-300";
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="circle"
|
|
||||||
className={`rounded-full px-3 py-1 text-sm transition-colors ${selectedClass} ${className || ""}`}
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ========== 特殊样式按钮 ==========
|
|
||||||
|
|
||||||
// GhostButton: 透明导航按钮
|
|
||||||
export const GhostButton = (props: any) => {
|
|
||||||
const { className, children, ...rest } = props;
|
|
||||||
return (
|
|
||||||
<Button variant="ghost" className={className} {...rest}>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// LinkButton: 链接样式按钮
|
|
||||||
export const LinkButton = (props: any) => <Button variant="link" {...props} />;
|
|
||||||
|
|
||||||
// DashedButton: 虚线边框按钮
|
|
||||||
export const DashedButton = (props: any) => <Button variant="dashed" {...props} />;
|
|
||||||
@@ -1,38 +1,37 @@
|
|||||||
// 统一的 UI 组件导出
|
// 统一的 UI 组件导出
|
||||||
// 可以从 '@/components/ui' 导入所有组件
|
// 可以从 '@/components/ui' 导入所有组件
|
||||||
|
|
||||||
// 表单组件
|
// Design System 组件(向后兼容)
|
||||||
export { Input } from './Input';
|
export { Input, type InputVariant, type InputProps } from '@/design-system/base/input';
|
||||||
export { Select, Option } from './Select';
|
export { Select, type SelectVariant, type SelectSize, type SelectProps } from '@/design-system/base/select';
|
||||||
export { Textarea } from './Textarea';
|
export { Textarea, type TextareaVariant, type TextareaProps } from '@/design-system/base/textarea';
|
||||||
export { RangeInput } from './RangeInput';
|
export { Card, type CardVariant, type CardPadding, type CardProps } from '@/design-system/base/card';
|
||||||
export type { InputVariant } from './Input';
|
|
||||||
export type { SelectSize } from './Select';
|
|
||||||
export type { TextareaVariant } from './Textarea';
|
|
||||||
|
|
||||||
// 按钮组件
|
|
||||||
export { Button } from './Button';
|
|
||||||
export {
|
export {
|
||||||
|
Button,
|
||||||
PrimaryButton,
|
PrimaryButton,
|
||||||
SecondaryButton,
|
SecondaryButton,
|
||||||
LightButton,
|
LightButton,
|
||||||
|
SuccessButton,
|
||||||
|
WarningButton,
|
||||||
|
ErrorButton,
|
||||||
|
GhostButton,
|
||||||
|
GhostLightButton,
|
||||||
|
OutlineButton,
|
||||||
|
LinkButton,
|
||||||
IconButton,
|
IconButton,
|
||||||
IconClick,
|
IconClick,
|
||||||
CircleButton,
|
CircleButton,
|
||||||
CircleToggleButton,
|
CircleToggleButton,
|
||||||
GhostButton,
|
|
||||||
LinkButton,
|
|
||||||
DashedButton,
|
DashedButton,
|
||||||
} from './buttons';
|
type ButtonVariant,
|
||||||
export type { ButtonVariant, ButtonSize, ButtonProps } from './Button';
|
type ButtonSize,
|
||||||
|
type ButtonProps
|
||||||
|
} from '@/design-system/base/button';
|
||||||
|
|
||||||
// 布局组件
|
// 业务特定组件
|
||||||
|
export { RangeInput } from './RangeInput';
|
||||||
export { Container } from './Container';
|
export { Container } from './Container';
|
||||||
export { PageLayout } from './PageLayout';
|
export { PageLayout } from './PageLayout';
|
||||||
export { PageHeader } from './PageHeader';
|
export { PageHeader } from './PageHeader';
|
||||||
export { CardList } from './CardList';
|
export { CardList } from './CardList';
|
||||||
export { Card } from './Card';
|
|
||||||
export type { CardProps, CardVariant, CardPadding } from './Card';
|
|
||||||
|
|
||||||
// 复合组件
|
|
||||||
export { LocaleSelector } from './LocaleSelector';
|
export { LocaleSelector } from './LocaleSelector';
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import { cn } from "@/design-system/lib/utils";
|
|||||||
*/
|
*/
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
// 基础样式
|
// 基础样式
|
||||||
"inline-flex items-center justify-center gap-2 rounded-xl font-semibold shadow transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
"inline-flex items-center justify-center gap-2 rounded-md font-semibold shadow transition-all duration-250 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -51,6 +51,7 @@ const buttonVariants = cva(
|
|||||||
warning: "bg-warning-500 text-white hover:bg-warning-600 shadow-md",
|
warning: "bg-warning-500 text-white hover:bg-warning-600 shadow-md",
|
||||||
error: "bg-error-500 text-white hover:bg-error-600 shadow-md",
|
error: "bg-error-500 text-white hover:bg-error-600 shadow-md",
|
||||||
ghost: "bg-transparent text-gray-700 hover:bg-gray-100 shadow-none",
|
ghost: "bg-transparent text-gray-700 hover:bg-gray-100 shadow-none",
|
||||||
|
"ghost-light": "bg-transparent text-white hover:bg-white/10 shadow-none",
|
||||||
outline: "border-2 border-gray-300 text-gray-700 hover:bg-gray-50 shadow-none",
|
outline: "border-2 border-gray-300 text-gray-700 hover:bg-gray-50 shadow-none",
|
||||||
link: "text-primary-500 hover:text-primary-600 hover:underline shadow-none px-0",
|
link: "text-primary-500 hover:text-primary-600 hover:underline shadow-none px-0",
|
||||||
},
|
},
|
||||||
@@ -138,15 +139,18 @@ export function Button({
|
|||||||
type = "button",
|
type = "button",
|
||||||
...props
|
...props
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
|
// 确保 size 有默认值
|
||||||
|
const actualSize = size ?? "md";
|
||||||
|
|
||||||
// 计算样式
|
// 计算样式
|
||||||
const computedClass = cn(
|
const computedClass = cn(
|
||||||
buttonVariants({ variant, size, fullWidth }),
|
buttonVariants({ variant, size: actualSize, fullWidth }),
|
||||||
selected && variant === "secondary" && "bg-gray-200",
|
selected && variant === "secondary" && "bg-gray-200",
|
||||||
className
|
className
|
||||||
);
|
);
|
||||||
|
|
||||||
// 图标尺寸映射
|
// 图标尺寸映射
|
||||||
const iconSize = { sm: 14, md: 16, lg: 20 }[size];
|
const iconSize = { sm: 14, md: 16, lg: 20 }[actualSize];
|
||||||
|
|
||||||
// 渲染 SVG 图标
|
// 渲染 SVG 图标
|
||||||
const renderSvgIcon = (icon: React.ReactNode, position: "left" | "right") => {
|
const renderSvgIcon = (icon: React.ReactNode, position: "left" | "right") => {
|
||||||
@@ -248,6 +252,9 @@ export const SecondaryButton = (props: Omit<ButtonProps, "variant">) => (
|
|||||||
<Button variant="secondary" {...props} />
|
<Button variant="secondary" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// LightButton: 次要按钮的别名(向后兼容)
|
||||||
|
export const LightButton = SecondaryButton;
|
||||||
|
|
||||||
export const SuccessButton = (props: Omit<ButtonProps, "variant">) => (
|
export const SuccessButton = (props: Omit<ButtonProps, "variant">) => (
|
||||||
<Button variant="success" {...props} />
|
<Button variant="success" {...props} />
|
||||||
);
|
);
|
||||||
@@ -264,6 +271,11 @@ export const GhostButton = (props: Omit<ButtonProps, "variant">) => (
|
|||||||
<Button variant="ghost" {...props} />
|
<Button variant="ghost" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// GhostLightButton: 透明按钮(白色文字,用于深色背景)
|
||||||
|
export const GhostLightButton = (props: Omit<ButtonProps, "variant">) => (
|
||||||
|
<Button variant="ghost-light" {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
export const OutlineButton = (props: Omit<ButtonProps, "variant">) => (
|
export const OutlineButton = (props: Omit<ButtonProps, "variant">) => (
|
||||||
<Button variant="outline" {...props} />
|
<Button variant="outline" {...props} />
|
||||||
);
|
);
|
||||||
@@ -271,3 +283,69 @@ export const OutlineButton = (props: Omit<ButtonProps, "variant">) => (
|
|||||||
export const LinkButton = (props: Omit<ButtonProps, "variant">) => (
|
export const LinkButton = (props: Omit<ButtonProps, "variant">) => (
|
||||||
<Button variant="link" {...props} />
|
<Button variant="link" {...props} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ========== 其他便捷组件 ==========
|
||||||
|
|
||||||
|
// IconButton: SVG 图标按钮(使用 ghost 变体)
|
||||||
|
export const IconButton = (props: Omit<ButtonProps, "variant"> & { icon?: React.ReactNode }) => {
|
||||||
|
const { icon, ...rest } = props;
|
||||||
|
return <Button variant="ghost" leftIcon={icon} {...rest} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// IconClick: 图片图标按钮(支持 Next.js Image)
|
||||||
|
export const IconClick = (props: Omit<ButtonProps, "variant"> & {
|
||||||
|
src?: string;
|
||||||
|
alt?: string;
|
||||||
|
size?: number | "sm" | "md" | "lg";
|
||||||
|
disableOnHoverBgChange?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { src, alt, size, disableOnHoverBgChange, className, ...rest } = props;
|
||||||
|
let buttonSize: "sm" | "md" | "lg" = "md";
|
||||||
|
if (typeof size === "number") {
|
||||||
|
if (size <= 20) buttonSize = "sm";
|
||||||
|
else if (size >= 32) buttonSize = "lg";
|
||||||
|
} else if (typeof size === "string") {
|
||||||
|
buttonSize = (size === "sm" || size === "md" || size === "lg") ? size : "md";
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoverClass = disableOnHoverBgChange ? "hover:bg-black/30" : "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
iconSrc={src}
|
||||||
|
iconAlt={alt}
|
||||||
|
size={buttonSize}
|
||||||
|
className={`${hoverClass} ${className || ""}`}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// CircleButton: 圆形图标按钮
|
||||||
|
export const CircleButton = (props: Omit<ButtonProps, "variant"> & { icon?: React.ReactNode }) => {
|
||||||
|
const { icon, className, ...rest } = props;
|
||||||
|
return <Button variant="ghost" leftIcon={icon} className={`rounded-full ${className || ""}`} {...rest} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// CircleToggleButton: 带选中状态的圆形切换按钮
|
||||||
|
export const CircleToggleButton = (props: Omit<ButtonProps, "variant"> & { selected?: boolean }) => {
|
||||||
|
const { selected, className, children, ...rest } = props;
|
||||||
|
const selectedClass = selected
|
||||||
|
? "bg-primary-500 text-white"
|
||||||
|
: "bg-gray-200 text-gray-600 hover:bg-gray-300";
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className={`rounded-full px-3 py-1 text-sm transition-colors ${selectedClass} ${className || ""}`}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// DashedButton: 虚线边框按钮(使用 outline 变体近似)
|
||||||
|
export const DashedButton = (props: Omit<ButtonProps, "variant">) => (
|
||||||
|
<Button variant="outline" className="border-dashed" {...props} />
|
||||||
|
);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { cn } from "@/design-system/lib/utils";
|
|||||||
*/
|
*/
|
||||||
const cardVariants = cva(
|
const cardVariants = cva(
|
||||||
// 基础样式
|
// 基础样式
|
||||||
"rounded-2xl bg-white transition-all duration-250",
|
"rounded-lg bg-white transition-all duration-250",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ import { cn } from "@/design-system/lib/utils";
|
|||||||
*/
|
*/
|
||||||
const inputVariants = cva(
|
const inputVariants = cva(
|
||||||
// 基础样式
|
// 基础样式
|
||||||
"flex w-full rounded-xl border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
"flex w-full rounded-md border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border-b-2 border-gray-300 bg-transparent rounded-t-xl",
|
default: "border-b-2 border-gray-300 bg-transparent rounded-t-md",
|
||||||
bordered: "border-gray-300 bg-white",
|
bordered: "border-gray-300 bg-white",
|
||||||
filled: "border-transparent bg-gray-100",
|
filled: "border-transparent bg-gray-100",
|
||||||
search: "border-gray-200 bg-white pl-10 rounded-full",
|
search: "border-gray-200 bg-white pl-10 rounded-full",
|
||||||
|
|||||||
@@ -186,12 +186,13 @@ export function RadioGroup({
|
|||||||
// 为每个 Radio 注入 name 和 onChange
|
// 为每个 Radio 注入 name 和 onChange
|
||||||
const enhancedChildren = React.Children.map(children, (child) => {
|
const enhancedChildren = React.Children.map(children, (child) => {
|
||||||
if (React.isValidElement(child)) {
|
if (React.isValidElement(child)) {
|
||||||
return React.cloneElement(child, {
|
const childProps = child.props as { value?: string; onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void };
|
||||||
|
return React.cloneElement(child as React.ReactElement<any>, {
|
||||||
name,
|
name,
|
||||||
checked: value !== undefined ? child.props.value === value : undefined,
|
checked: value !== undefined ? childProps.value === value : undefined,
|
||||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange?.(e.target.value);
|
onChange?.(e.target.value);
|
||||||
child.props.onChange?.(e);
|
childProps.onChange?.(e);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ import { cn } from "@/design-system/lib/utils";
|
|||||||
*/
|
*/
|
||||||
const selectVariants = cva(
|
const selectVariants = cva(
|
||||||
// 基础样式
|
// 基础样式
|
||||||
"flex w-full appearance-none items-center justify-between rounded-xl border px-3 py-2 pr-8 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
"flex w-full appearance-none items-center justify-between rounded-md border px-3 py-2 pr-8 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "border-b-2 border-gray-300 bg-transparent rounded-t-xl",
|
default: "border-b-2 border-gray-300 bg-transparent rounded-t-md",
|
||||||
bordered: "border-gray-300 bg-white",
|
bordered: "border-gray-300 bg-white",
|
||||||
filled: "border-transparent bg-gray-100",
|
filled: "border-transparent bg-gray-100",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -104,19 +104,22 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
|||||||
onChange?.(e);
|
onChange?.(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确保 size 有默认值
|
||||||
|
const actualSize = size ?? "md";
|
||||||
|
|
||||||
// 滑块大小
|
// 滑块大小
|
||||||
const thumbSize = {
|
const thumbSize = {
|
||||||
sm: "h-3.5 w-3.5",
|
sm: "h-3.5 w-3.5",
|
||||||
md: "h-4 w-4",
|
md: "h-4 w-4",
|
||||||
lg: "h-5 w-5",
|
lg: "h-5 w-5",
|
||||||
}[size];
|
}[actualSize];
|
||||||
|
|
||||||
// 滑块位移
|
// 滑块位移
|
||||||
const thumbTranslate = {
|
const thumbTranslate = {
|
||||||
sm: isChecked ? "translate-x-4" : "translate-x-0.5",
|
sm: isChecked ? "translate-x-4" : "translate-x-0.5",
|
||||||
md: isChecked ? "translate-x-5" : "translate-x-0.5",
|
md: isChecked ? "translate-x-5" : "translate-x-0.5",
|
||||||
lg: isChecked ? "translate-x-6" : "translate-x-0.5",
|
lg: isChecked ? "translate-x-6" : "translate-x-0.5",
|
||||||
}[size];
|
}[actualSize];
|
||||||
|
|
||||||
const renderSwitch = () => (
|
const renderSwitch = () => (
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { cn } from "@/design-system/lib/utils";
|
|||||||
*/
|
*/
|
||||||
const textareaVariants = cva(
|
const textareaVariants = cva(
|
||||||
// 基础样式
|
// 基础样式
|
||||||
"flex w-full rounded-xl border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none",
|
"flex w-full rounded-md border px-3 py-2 text-base transition-all duration-250 placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -98,13 +98,16 @@ export function Badge({
|
|||||||
info: "bg-info-500",
|
info: "bg-info-500",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确保 variant 有默认值
|
||||||
|
const actualVariant = variant ?? "default";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(badgeVariants({ variant, size, dot }), className)} {...props}>
|
<div className={cn(badgeVariants({ variant: actualVariant, size, dot }), className)} {...props}>
|
||||||
{dot && (
|
{dot && (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-2 w-2 rounded-full",
|
"h-2 w-2 rounded-full",
|
||||||
dotColor || dotColors[variant]
|
dotColor || dotColors[actualVariant]
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export function Divider({
|
|||||||
<div
|
<div
|
||||||
className={cn(dividerVariants({ variant, orientation }), className)}
|
className={cn(dividerVariants({ variant, orientation }), className)}
|
||||||
role="separator"
|
role="separator"
|
||||||
aria-orientation={orientation}
|
aria-orientation={orientation as "horizontal" | "vertical"}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { cn } from "@/design-system/lib/utils";
|
|||||||
*/
|
*/
|
||||||
const alertVariants = cva(
|
const alertVariants = cva(
|
||||||
// 基础样式
|
// 基础样式
|
||||||
"rounded-xl border-2 px-4 py-3 shadow-sm transition-all duration-250",
|
"rounded-lg border-2 px-4 py-3 shadow-sm transition-all duration-250",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
@@ -127,6 +127,9 @@ export function Alert({
|
|||||||
|
|
||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
|
// 确保 variant 有默认值
|
||||||
|
const actualVariant = variant ?? "info";
|
||||||
|
|
||||||
// 图标颜色
|
// 图标颜色
|
||||||
const iconColors = {
|
const iconColors = {
|
||||||
info: "text-info-500",
|
info: "text-info-500",
|
||||||
@@ -137,14 +140,14 @@ export function Alert({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(alertVariants({ variant }), className)}
|
className={cn(alertVariants({ variant: actualVariant }), className)}
|
||||||
role="alert"
|
role="alert"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
{/* 图标 */}
|
{/* 图标 */}
|
||||||
<div className={cn("shrink-0", iconColors[variant])}>
|
<div className={cn("shrink-0", iconColors[actualVariant])}>
|
||||||
{icon || defaultIcons[variant]}
|
{icon || defaultIcons[actualVariant]}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 内容 */}
|
{/* 内容 */}
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ export function Progress({
|
|||||||
warning: "bg-warning-500",
|
warning: "bg-warning-500",
|
||||||
error: "bg-error-500",
|
error: "bg-error-500",
|
||||||
};
|
};
|
||||||
return colors[variant];
|
const actualVariant = variant ?? "default";
|
||||||
|
return colors[actualVariant];
|
||||||
};
|
};
|
||||||
|
|
||||||
// 格式化标签
|
// 格式化标签
|
||||||
@@ -175,7 +176,7 @@ export function CircularProgress({
|
|||||||
warning: "#f59e0b",
|
warning: "#f59e0b",
|
||||||
error: "#ef4444",
|
error: "#ef4444",
|
||||||
};
|
};
|
||||||
const strokeColor = colors[variant];
|
const strokeColor = colors[variant ?? "default"];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("inline-flex items-center justify-center", className)}>
|
<div className={cn("inline-flex items-center justify-center", className)}>
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ export interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonE
|
|||||||
value: string;
|
value: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
variant?: "line" | "enclosed" | "soft";
|
variant?: "line" | "enclosed" | "soft";
|
||||||
|
"data-state"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerVariants = cva(
|
const triggerVariants = cva(
|
||||||
@@ -148,14 +149,15 @@ export function TabsTrigger({
|
|||||||
children,
|
children,
|
||||||
variant = "line",
|
variant = "line",
|
||||||
className,
|
className,
|
||||||
|
"data-state": dataState,
|
||||||
...props
|
...props
|
||||||
}: TabsTriggerProps) {
|
}: TabsTriggerProps) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={props["data-state"] === "active"}
|
aria-selected={dataState === "active"}
|
||||||
data-state={props["data-state"]}
|
data-state={dataState}
|
||||||
className={cn(triggerVariants({ variant }), className)}
|
className={cn(triggerVariants({ variant }), className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -170,21 +172,24 @@ export function TabsTrigger({
|
|||||||
export interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
export interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
value: string;
|
value: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
"data-state"?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TabsContent({
|
export function TabsContent({
|
||||||
value,
|
value,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
"data-state": dataState,
|
||||||
...props
|
...props
|
||||||
}: TabsContentProps) {
|
}: TabsContentProps) {
|
||||||
if (value !== props["data-state"]) return null;
|
if (value !== dataState) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
className={cn("mt-4 focus:outline-none", className)}
|
className={cn("mt-4 focus:outline-none", className)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
data-state={dataState}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export function Modal({
|
|||||||
{/* 模态框内容 */}
|
{/* 模态框内容 */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-10 w-full bg-white rounded-2xl shadow-2xl max-h-[90vh] overflow-hidden flex flex-col",
|
"relative z-10 w-full bg-white rounded-lg shadow-2xl max-h-[90vh] overflow-hidden flex flex-col",
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,233 +0,0 @@
|
|||||||
import type { Config } from "tailwindcss";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tailwind CSS 配置
|
|
||||||
*
|
|
||||||
* 集成 Design System 设计令牌到 Tailwind 工具类
|
|
||||||
*/
|
|
||||||
const config: Config = {
|
|
||||||
content: [
|
|
||||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
||||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
// 颜色系统
|
|
||||||
colors: {
|
|
||||||
// 主色 - Teal
|
|
||||||
primary: {
|
|
||||||
50: '#f0f9f8',
|
|
||||||
100: '#e0f2f0',
|
|
||||||
200: '#bce6e1',
|
|
||||||
300: '#8dd4cc',
|
|
||||||
400: '#5ec2b7',
|
|
||||||
500: '#35786f',
|
|
||||||
600: '#2a605b',
|
|
||||||
700: '#1f4844',
|
|
||||||
800: '#183835',
|
|
||||||
900: '#122826',
|
|
||||||
950: '#0a1413',
|
|
||||||
},
|
|
||||||
// 中性色
|
|
||||||
gray: {
|
|
||||||
50: '#f9fafb',
|
|
||||||
100: '#f3f4f6',
|
|
||||||
200: '#e5e7eb',
|
|
||||||
300: '#d1d5db',
|
|
||||||
400: '#9ca3af',
|
|
||||||
500: '#6b7280',
|
|
||||||
600: '#4b5563',
|
|
||||||
700: '#374151',
|
|
||||||
800: '#1f2937',
|
|
||||||
900: '#111827',
|
|
||||||
950: '#030712',
|
|
||||||
},
|
|
||||||
// 语义色 - Success
|
|
||||||
success: {
|
|
||||||
50: '#f0fdf4',
|
|
||||||
100: '#dcfce7',
|
|
||||||
200: '#bbf7d0',
|
|
||||||
300: '#86efac',
|
|
||||||
400: '#4ade80',
|
|
||||||
500: '#22c55e',
|
|
||||||
600: '#16a34a',
|
|
||||||
700: '#15803d',
|
|
||||||
800: '#166534',
|
|
||||||
900: '#14532d',
|
|
||||||
950: '#052e16',
|
|
||||||
},
|
|
||||||
// 语义色 - Warning
|
|
||||||
warning: {
|
|
||||||
50: '#fffbeb',
|
|
||||||
100: '#fef3c7',
|
|
||||||
200: '#fde68a',
|
|
||||||
300: '#fcd34d',
|
|
||||||
400: '#fbbf24',
|
|
||||||
500: '#f59e0b',
|
|
||||||
600: '#d97706',
|
|
||||||
700: '#b45309',
|
|
||||||
800: '#92400e',
|
|
||||||
900: '#78350f',
|
|
||||||
950: '#451a03',
|
|
||||||
},
|
|
||||||
// 语义色 - Error
|
|
||||||
error: {
|
|
||||||
50: '#fef2f2',
|
|
||||||
100: '#fee2e2',
|
|
||||||
200: '#fecaca',
|
|
||||||
300: '#fca5a5',
|
|
||||||
400: '#f87171',
|
|
||||||
500: '#ef4444',
|
|
||||||
600: '#dc2626',
|
|
||||||
700: '#b91c1c',
|
|
||||||
800: '#991b1b',
|
|
||||||
900: '#7f1d1d',
|
|
||||||
950: '#450a0a',
|
|
||||||
},
|
|
||||||
// 语义色 - Info
|
|
||||||
info: {
|
|
||||||
50: '#eff6ff',
|
|
||||||
100: '#dbeafe',
|
|
||||||
200: '#bfdbfe',
|
|
||||||
300: '#93c5fd',
|
|
||||||
400: '#60a5fa',
|
|
||||||
500: '#3b82f6',
|
|
||||||
600: '#2563eb',
|
|
||||||
700: '#1d4ed8',
|
|
||||||
800: '#1e40af',
|
|
||||||
900: '#1e3a8a',
|
|
||||||
950: '#172554',
|
|
||||||
},
|
|
||||||
// 语义别名
|
|
||||||
background: 'var(--background)',
|
|
||||||
foreground: 'var(--foreground)',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 间距系统(基于 8pt 网格)
|
|
||||||
spacing: {
|
|
||||||
18: '4.5rem',
|
|
||||||
88: '22rem',
|
|
||||||
128: '32rem',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 字体家族
|
|
||||||
fontFamily: {
|
|
||||||
sans: [
|
|
||||||
'var(--font-geist-sans)',
|
|
||||||
'-apple-system',
|
|
||||||
'BlinkMacSystemFont',
|
|
||||||
'system-ui',
|
|
||||||
'sans-serif',
|
|
||||||
],
|
|
||||||
mono: [
|
|
||||||
'var(--font-geist-mono)',
|
|
||||||
'ui-monospace',
|
|
||||||
'SFMono-Regular',
|
|
||||||
'Monaco',
|
|
||||||
'Consolas',
|
|
||||||
'monospace',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// 字体大小和行高
|
|
||||||
fontSize: {
|
|
||||||
xs: ['0.75rem', { lineHeight: '1rem' }],
|
|
||||||
sm: ['0.875rem', { lineHeight: '1.25rem' }],
|
|
||||||
base: ['1rem', { lineHeight: '1.5rem' }],
|
|
||||||
lg: ['1.125rem', { lineHeight: '1.75rem' }],
|
|
||||||
xl: ['1.25rem', { lineHeight: '1.75rem' }],
|
|
||||||
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
|
||||||
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
|
|
||||||
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
|
|
||||||
'5xl': ['3rem', { lineHeight: '1' }],
|
|
||||||
'6xl': ['3.75rem', { lineHeight: '1' }],
|
|
||||||
'7xl': ['4.5rem', { lineHeight: '1' }],
|
|
||||||
'8xl': ['6rem', { lineHeight: '1' }],
|
|
||||||
'9xl': ['8rem', { lineHeight: '1' }],
|
|
||||||
},
|
|
||||||
|
|
||||||
// 字重
|
|
||||||
fontWeight: {
|
|
||||||
thin: '100',
|
|
||||||
extralight: '200',
|
|
||||||
light: '300',
|
|
||||||
normal: '400',
|
|
||||||
medium: '500',
|
|
||||||
semibold: '600',
|
|
||||||
bold: '700',
|
|
||||||
extrabold: '800',
|
|
||||||
black: '900',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 圆角
|
|
||||||
borderRadius: {
|
|
||||||
sm: '0.125rem',
|
|
||||||
DEFAULT: '0.25rem',
|
|
||||||
md: '0.375rem',
|
|
||||||
lg: '0.5rem',
|
|
||||||
xl: '0.75rem',
|
|
||||||
'2xl': '1rem',
|
|
||||||
'3xl': '1.5rem',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 阴影
|
|
||||||
boxShadow: {
|
|
||||||
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
|
||||||
DEFAULT: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)',
|
|
||||||
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
|
||||||
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
|
|
||||||
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)',
|
|
||||||
'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
|
|
||||||
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)',
|
|
||||||
// 语义阴影
|
|
||||||
'primary': '0 4px 14px 0 rgba(53, 120, 111, 0.39)',
|
|
||||||
'success': '0 4px 14px 0 rgba(34, 197, 94, 0.39)',
|
|
||||||
'warning': '0 4px 14px 0 rgba(245, 158, 11, 0.39)',
|
|
||||||
'error': '0 4px 14px 0 rgba(239, 68, 68, 0.39)',
|
|
||||||
'info': '0 4px 14px 0 rgba(59, 130, 246, 0.39)',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 容器最大宽度
|
|
||||||
maxWidth: {
|
|
||||||
'xs': '20rem',
|
|
||||||
'sm': '24rem',
|
|
||||||
'md': '28rem',
|
|
||||||
'lg': '32rem',
|
|
||||||
'xl': '36rem',
|
|
||||||
'2xl': '42rem',
|
|
||||||
'3xl': '48rem',
|
|
||||||
'4xl': '56rem',
|
|
||||||
'5xl': '64rem',
|
|
||||||
'6xl': '72rem',
|
|
||||||
'7xl': '80rem',
|
|
||||||
'8xl': '88rem',
|
|
||||||
},
|
|
||||||
|
|
||||||
// Z-index 层级
|
|
||||||
zIndex: {
|
|
||||||
dropdown: 1000,
|
|
||||||
sticky: 1020,
|
|
||||||
fixed: 1030,
|
|
||||||
modalBackdrop: 1040,
|
|
||||||
modal: 1050,
|
|
||||||
popover: 1060,
|
|
||||||
tooltip: 1070,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 动画时长
|
|
||||||
transitionDuration: {
|
|
||||||
'250': '250ms',
|
|
||||||
'350': '350ms',
|
|
||||||
},
|
|
||||||
|
|
||||||
// 断点
|
|
||||||
screens: {
|
|
||||||
'xs': '475px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
Reference in New Issue
Block a user