Design System 重构继续完成
This commit is contained in:
30
CLAUDE.md
30
CLAUDE.md
@@ -22,9 +22,7 @@ pnpm run start
|
||||
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` 配置。
|
||||
|
||||
### 环境变量
|
||||
|
||||
需要在 `.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 部署配置
|
||||
- **React Compiler**: 在 `next.config.ts` 中启用以自动优化
|
||||
- **HTTPS 开发**: 开发服务器使用 `--experimental-https` 标志
|
||||
@@ -147,7 +120,6 @@ DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
||||
## 开发注意事项
|
||||
|
||||
- 使用 pnpm,而不是 npm 或 yarn
|
||||
- schema 变更后,先运行 `pnpm prisma generate` 再运行 `pnpm prisma db push`
|
||||
- 应用使用 TypeScript 严格模式 - 确保类型安全
|
||||
- 所有面向用户的文本都需要国际化
|
||||
- **优先使用 Server Components**,只在需要交互时使用 Client Components
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
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 { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { PageLayout } from "@/components/ui/PageLayout";
|
||||
import { Card } from "@/components/ui/Card";
|
||||
import { Card } from "@/design-system/base/card";
|
||||
|
||||
interface AlphabetCardProps {
|
||||
alphabet: Letter[];
|
||||
@@ -103,7 +103,7 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
|
||||
{/* 右上角返回按钮 - outside the white card */}
|
||||
<div className="flex justify-end mb-4">
|
||||
<IconClick
|
||||
size={32}
|
||||
size="lg"
|
||||
alt="close"
|
||||
src={IMAGES.close}
|
||||
onClick={onBack}
|
||||
@@ -185,7 +185,7 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
|
||||
<div className="flex justify-between items-center">
|
||||
{/* 上一个按钮 */}
|
||||
<CircleButton onClick={goToPrevious} aria-label="上一个字母">
|
||||
<ChevronLeft size={24} />
|
||||
<ChevronLeft size={20} />
|
||||
</CircleButton>
|
||||
|
||||
{/* 中间区域:随机按钮 */}
|
||||
@@ -202,7 +202,7 @@ export function AlphabetCard({ alphabet, alphabetType, onBack }: AlphabetCardPro
|
||||
|
||||
{/* 下一个按钮 */}
|
||||
<CircleButton onClick={goToNext} aria-label="下一个字母">
|
||||
<ChevronRight size={24} />
|
||||
<ChevronRight size={20} />
|
||||
</CircleButton>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { IconClick } from "@/components/ui/buttons";
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
import { IconClick } from "@/design-system/base/button";
|
||||
import { IMAGES } from "@/config/images";
|
||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||
import {
|
||||
@@ -45,10 +45,10 @@ export function MemoryCard({
|
||||
className="w-full flex justify-center items-center"
|
||||
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">
|
||||
<IconClick
|
||||
size={32}
|
||||
size="lg"
|
||||
alt="close"
|
||||
src={IMAGES.close}
|
||||
onClick={() => setChosenAlphabet(null)}
|
||||
@@ -64,13 +64,13 @@ export function MemoryCard({
|
||||
</div>
|
||||
<div className="flex flex-row mt-32 items-center justify-center gap-2">
|
||||
<IconClick
|
||||
size={48}
|
||||
size="lg"
|
||||
alt="refresh"
|
||||
src={IMAGES.refresh}
|
||||
onClick={refresh}
|
||||
></IconClick>
|
||||
<IconClick
|
||||
size={48}
|
||||
size="lg"
|
||||
alt="more"
|
||||
src={IMAGES.more_horiz}
|
||||
onClick={() => setMore(!more)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useState, useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Letter, SupportedAlphabets } from "@/lib/interfaces";
|
||||
import { PageLayout } from "@/components/ui/PageLayout";
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
import { AlphabetCard } from "./AlphabetCard";
|
||||
|
||||
export default function Alphabet() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
import { Input } from "@/design-system/base/input";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
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 { actionCreatePair } from "@/modules/folder/folder-aciton";
|
||||
import { TSharedItem } from "@/shared/dictionary-type";
|
||||
|
||||
@@ -6,7 +6,7 @@ import Link from "next/link";
|
||||
import { Folder as Fd } from "lucide-react";
|
||||
import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
||||
import { PageLayout } from "@/components/ui/PageLayout";
|
||||
import { PrimaryButton } from "@/components/ui/buttons";
|
||||
import { PrimaryButton } from "@/design-system/base/button";
|
||||
|
||||
interface FolderSelectorProps {
|
||||
folders: TSharedFolderWithTotalPairs[];
|
||||
@@ -37,7 +37,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
|
||||
{t("selectFolder")}
|
||||
</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
|
||||
.toSorted((a, b) => a.id - b.id)
|
||||
.map((folder) => (
|
||||
@@ -50,7 +50,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({ folders }) => {
|
||||
>
|
||||
{/* 文件夹图标 */}
|
||||
<div className="shrink-0">
|
||||
<Fd className="text-gray-600" size={24} />
|
||||
<Fd className="text-gray-600" size="md" />
|
||||
</div>
|
||||
{/* 文件夹信息 */}
|
||||
<div className="flex-1">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
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 { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -29,7 +29,7 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
|
||||
|
||||
if (textPairs.length === 0) {
|
||||
return (
|
||||
<PageLayout maxWidth="md">
|
||||
<PageLayout>
|
||||
<p className="text-gray-700 text-center">{t("noTextPairs")}</p>
|
||||
</PageLayout>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useRef, forwardRef, useEffect, useCallback } from "react";
|
||||
import { SubtitleDisplay } from "./SubtitleDisplay";
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
import { RangeInput } from "@/components/ui/RangeInput";
|
||||
import { getIndex, parseSrt, getNearistIndex } from "../subtitle";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useRef } from "react";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Button } from "@/design-system/base/button";
|
||||
import { FileInputProps } from "../../types/controls";
|
||||
|
||||
interface FileInputComponentProps extends FileInputProps {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
import { PlayButtonProps } from "../../types/player";
|
||||
|
||||
export function PlayButton({ isPlaying, onToggle, disabled, className }: PlayButtonProps) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
import { SpeedControlProps } from "../../types/player";
|
||||
import { getPlaybackRateOptions, getPlaybackRateLabel } from "../../utils/timeUtils";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
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 { PlayButton } from "../atoms/PlayButton";
|
||||
import { SpeedControl } from "../atoms/SpeedControl";
|
||||
|
||||
@@ -4,7 +4,7 @@ import React from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
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 { useFileUpload } from "../../hooks/useFileUpload";
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { SubtitleArea } from "./components/compounds/SubtitleArea";
|
||||
import { ControlBar } from "./components/compounds/ControlBar";
|
||||
import { UploadZone } from "./components/compounds/UploadZone";
|
||||
import { SeekBar } from "./components/atoms/SeekBar";
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
|
||||
export default function SrtPlayerPage() {
|
||||
const t = useTranslations("home");
|
||||
@@ -119,7 +119,7 @@ export default function SrtPlayerPage() {
|
||||
</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) && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-75 z-10">
|
||||
<div className="text-center text-white">
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
TextSpeakerArraySchema,
|
||||
TextSpeakerItemSchema,
|
||||
} from "@/lib/interfaces";
|
||||
import { IconClick } from "@/components/ui/buttons";
|
||||
import { IconClick } from "@/design-system/base/button";
|
||||
import { IMAGES } from "@/config/images";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
||||
@@ -24,7 +24,7 @@ function TextCard({ item, handleUse, handleDel }: TextCardProps) {
|
||||
handleDel(item);
|
||||
};
|
||||
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="max-h-26 hover:cursor-pointer text-3xl overflow-y-auto">
|
||||
{item.text}
|
||||
@@ -39,7 +39,7 @@ function TextCard({ item, handleUse, handleDel }: TextCardProps) {
|
||||
alt="delete"
|
||||
onClick={onDelClick}
|
||||
className="place-self-center"
|
||||
size={42}
|
||||
size="lg"
|
||||
></IconClick>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,7 +81,7 @@ export function SaveList({ show = false, handleUse }: SaveListProps) {
|
||||
if (show)
|
||||
return (
|
||||
<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" }}
|
||||
>
|
||||
<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}
|
||||
alt="refresh"
|
||||
onClick={refresh}
|
||||
size={48}
|
||||
size="lg"
|
||||
className=""
|
||||
></IconClick>
|
||||
<IconClick
|
||||
src={IMAGES.delete}
|
||||
alt="delete"
|
||||
onClick={handleDeleteAll}
|
||||
size={48}
|
||||
size="lg"
|
||||
className=""
|
||||
></IconClick>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { IconClick } from "@/components/ui/buttons";
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
import { IconClick } from "@/design-system/base/button";
|
||||
import { IMAGES } from "@/config/images";
|
||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||
import {
|
||||
@@ -222,7 +222,7 @@ export default function TextSpeakerPage() {
|
||||
<PageLayout className="items-start py-4">
|
||||
{/* 文本输入区域 */}
|
||||
<div
|
||||
className="border border-gray-200 rounded-2xl"
|
||||
className="border border-gray-200 rounded-lg"
|
||||
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">
|
||||
{/* 速度调节面板 */}
|
||||
{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
|
||||
size={45}
|
||||
size="lg"
|
||||
onClick={letMeSetSpeed(0.5)}
|
||||
src={IMAGES.speed_0_5x}
|
||||
alt="0.5x"
|
||||
className={speed === 0.5 ? "bg-gray-200" : ""}
|
||||
></IconClick>
|
||||
<IconClick
|
||||
size={45}
|
||||
size="lg"
|
||||
onClick={letMeSetSpeed(0.7)}
|
||||
src={IMAGES.speed_0_7x}
|
||||
alt="0.7x"
|
||||
className={speed === 0.7 ? "bg-gray-200" : ""}
|
||||
></IconClick>
|
||||
<IconClick
|
||||
size={45}
|
||||
size="lg"
|
||||
onClick={letMeSetSpeed(1)}
|
||||
src={IMAGES.speed_1x}
|
||||
alt="1x"
|
||||
className={speed === 1 ? "bg-gray-200" : ""}
|
||||
></IconClick>
|
||||
<IconClick
|
||||
size={45}
|
||||
size="lg"
|
||||
onClick={letMeSetSpeed(1.2)}
|
||||
src={IMAGES.speed_1_2_x}
|
||||
alt="1.2x"
|
||||
className={speed === 1.2 ? "bg-gray-200" : ""}
|
||||
></IconClick>
|
||||
<IconClick
|
||||
size={45}
|
||||
size="lg"
|
||||
onClick={letMeSetSpeed(1.5)}
|
||||
src={IMAGES.speed_1_5x}
|
||||
alt="1.5x"
|
||||
@@ -282,7 +282,7 @@ export default function TextSpeakerPage() {
|
||||
)}
|
||||
{/* 播放/暂停按钮 */}
|
||||
<IconClick
|
||||
size={45}
|
||||
size="lg"
|
||||
onClick={speak}
|
||||
src={pause ? IMAGES.play_arrow : IMAGES.pause}
|
||||
alt="playorpause"
|
||||
@@ -290,7 +290,7 @@ export default function TextSpeakerPage() {
|
||||
></IconClick>
|
||||
{/* 自动暂停按钮 */}
|
||||
<IconClick
|
||||
size={45}
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
setAutopause(!autopause);
|
||||
if (objurlRef) {
|
||||
@@ -303,7 +303,7 @@ export default function TextSpeakerPage() {
|
||||
></IconClick>
|
||||
{/* 速度调节按钮 */}
|
||||
<IconClick
|
||||
size={45}
|
||||
size="lg"
|
||||
onClick={() => setShowSpeedAdjust(!showSpeedAdjust)}
|
||||
src={IMAGES.speed}
|
||||
alt="speed"
|
||||
@@ -311,7 +311,7 @@ export default function TextSpeakerPage() {
|
||||
></IconClick>
|
||||
{/* 保存按钮 */}
|
||||
<IconClick
|
||||
size={45}
|
||||
size="lg"
|
||||
onClick={save}
|
||||
src={IMAGES.save}
|
||||
alt="save"
|
||||
@@ -338,7 +338,7 @@ export default function TextSpeakerPage() {
|
||||
</div>
|
||||
{/* 保存列表 */}
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { LightButton, PrimaryButton } from "@/components/ui/buttons";
|
||||
import { IconClick } from "@/components/ui/buttons";
|
||||
import { LightButton, PrimaryButton, IconClick } from "@/design-system/base/button";
|
||||
import { IMAGES } from "@/config/images";
|
||||
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -101,7 +100,7 @@ export default function TranslatorPage() {
|
||||
{/* Card Component - Left Side */}
|
||||
<div className="w-full md:w-1/2 flex flex-col-reverse gap-2">
|
||||
{/* 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
|
||||
className="resize-none h-8/12 w-full focus:outline-0"
|
||||
ref={taref}
|
||||
@@ -147,7 +146,7 @@ export default function TranslatorPage() {
|
||||
{/* Card Component - Right Side */}
|
||||
<div className="w-full md:w-1/2 flex flex-col-reverse gap-2">
|
||||
{/* 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="ipa w-full h-1/6 overflow-y-auto text-gray-600">
|
||||
{translationResult?.targetIpa || ""}
|
||||
@@ -213,7 +212,8 @@ export default function TranslatorPage() {
|
||||
<PrimaryButton
|
||||
onClick={translate}
|
||||
disabled={processing}
|
||||
className="text-xl h-16 px-8"
|
||||
size="lg"
|
||||
className="text-xl"
|
||||
>
|
||||
{t("translate")}
|
||||
</PrimaryButton>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { useState, useActionState, startTransition } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { PageLayout } from "@/components/ui/PageLayout";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import { LightButton, LinkButton } from "@/components/ui/buttons";
|
||||
import { Input } from "@/design-system/base/input";
|
||||
import { LightButton, LinkButton } from "@/design-system/base/button";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { actionSignIn, actionSignUp, ActionOutputAuth } from "@/modules/auth/auth-action";
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
FolderPlus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { CircleButton, DashedButton } from "@/components/ui/buttons";
|
||||
import { CircleButton, DashedButton } from "@/design-system/base/button";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
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="shrink-0">
|
||||
<Fd className="text-gray-600" size={24} />
|
||||
<Fd className="text-gray-600" size="md" />
|
||||
</div>
|
||||
|
||||
<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="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>
|
||||
<p className="text-sm">{t("noFoldersYet")}</p>
|
||||
</div>
|
||||
) : (
|
||||
// 文件夹卡片列表
|
||||
<div className="rounded-xl border border-gray-200 overflow-hidden">
|
||||
<div className="rounded-md border border-gray-200 overflow-hidden">
|
||||
{folders
|
||||
.toSorted((a, b) => a.id - b.id)
|
||||
.map((folder) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
import { Input } from "@/design-system/base/input";
|
||||
import { LocaleSelector } from "@/components/ui/LocaleSelector";
|
||||
import { X } from "lucide-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">
|
||||
<h2 className="flex-1 text-xl font-light mb-4 text-center">
|
||||
{t("addNewTextPair")}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AddTextPairModal } from "./AddTextPairModal";
|
||||
import { TextPairCard } from "./TextPairCard";
|
||||
import { useTranslations } from "next-intl";
|
||||
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 { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder/folder-aciton";
|
||||
import { TSharedPair } from "@/shared/folder-type";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Edit, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { CircleButton } from "@/components/ui/buttons";
|
||||
import { CircleButton } from "@/design-system/base/button";
|
||||
import { UpdateTextPairModal } from "./UpdateTextPairModal";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { TSharedPair } from "@/shared/folder-type";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
import { Input } from "@/design-system/base/input";
|
||||
import { LocaleSelector } from "@/components/ui/LocaleSelector";
|
||||
import { X } from "lucide-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">
|
||||
<h2 className="flex-1 text-xl font-light mb-4 text-center">
|
||||
{t("updateTextPair")}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/**
|
||||
* Design System CSS 变量
|
||||
*
|
||||
* 定义全局 CSS 变量用于主题切换和动态样式
|
||||
* Tailwind CSS v4 主题配置
|
||||
* 使用 @theme 指令定义主题变量
|
||||
*/
|
||||
:root {
|
||||
/* 颜色系统 */
|
||||
@theme {
|
||||
/* 主色 - Teal */
|
||||
--color-primary-50: #f0f9f8;
|
||||
--color-primary-100: #e0f2f0;
|
||||
--color-primary-200: #bce6e1;
|
||||
@@ -19,12 +18,88 @@
|
||||
--color-primary-900: #122826;
|
||||
--color-primary-950: #0a1413;
|
||||
|
||||
/* 语义色 */
|
||||
--color-success-500: #22c55e;
|
||||
--color-warning-500: #f59e0b;
|
||||
--color-error-500: #ef4444;
|
||||
--color-info-500: #3b82f6;
|
||||
/* 中性色 */
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--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;
|
||||
--foreground: #111827;
|
||||
@@ -41,13 +116,14 @@
|
||||
--border-secondary: #e5e7eb;
|
||||
--border-focus: #35786f;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 0.125rem;
|
||||
/* 圆角 - 更小的圆角 */
|
||||
--radius-xs: 0.125rem;
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-xl: 0.75rem;
|
||||
--radius-2xl: 1rem;
|
||||
--radius-3xl: 1.5rem;
|
||||
--radius-xl: 0.625rem;
|
||||
--radius-2xl: 0.75rem;
|
||||
--radius-3xl: 1rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* 阴影 */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { LightButton } from "@/components/ui/buttons";
|
||||
import { LightButton } from "@/design-system/base/button";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
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 { repoGetFoldersWithTotalPairsByUserId } from "@/modules/folder/folder-repository";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { GhostButton } from "../ui/buttons";
|
||||
import { GhostLightButton } from "@/design-system/base/button";
|
||||
import { useState } from "react";
|
||||
import { Languages } from "lucide-react";
|
||||
|
||||
@@ -15,59 +15,59 @@ export function LanguageSettings() {
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Languages onClick={handleLanguageClick} size={28} className="navbar-btn" />
|
||||
<Languages onClick={handleLanguageClick} size={28} className="text-white hover:text-white/80" />
|
||||
<div className="relative">
|
||||
{showLanguageMenu && (
|
||||
<div>
|
||||
<div className="absolute top-10 right-0 rounded-md shadow-md flex flex-col gap-2">
|
||||
<GhostButton
|
||||
<GhostLightButton
|
||||
className="w-full bg-[#35786f]"
|
||||
onClick={() => setLocale("en-US")}
|
||||
>
|
||||
English
|
||||
</GhostButton>
|
||||
<GhostButton
|
||||
</GhostLightButton>
|
||||
<GhostLightButton
|
||||
className="w-full bg-[#35786f]"
|
||||
onClick={() => setLocale("zh-CN")}
|
||||
>
|
||||
中文
|
||||
</GhostButton>
|
||||
<GhostButton
|
||||
</GhostLightButton>
|
||||
<GhostLightButton
|
||||
className="w-full bg-[#35786f]"
|
||||
onClick={() => setLocale("ja-JP")}
|
||||
>
|
||||
日本語
|
||||
</GhostButton>
|
||||
<GhostButton
|
||||
</GhostLightButton>
|
||||
<GhostLightButton
|
||||
className="w-full bg-[#35786f]"
|
||||
onClick={() => setLocale("ko-KR")}
|
||||
>
|
||||
한국어
|
||||
</GhostButton>
|
||||
<GhostButton
|
||||
</GhostLightButton>
|
||||
<GhostLightButton
|
||||
className="w-full bg-[#35786f]"
|
||||
onClick={() => setLocale("de-DE")}
|
||||
>
|
||||
Deutsch
|
||||
</GhostButton>
|
||||
<GhostButton
|
||||
</GhostLightButton>
|
||||
<GhostLightButton
|
||||
className="w-full bg-[#35786f]"
|
||||
onClick={() => setLocale("fr-FR")}
|
||||
>
|
||||
Français
|
||||
</GhostButton>
|
||||
<GhostButton
|
||||
</GhostLightButton>
|
||||
<GhostLightButton
|
||||
className="w-full bg-[#35786f]"
|
||||
onClick={() => setLocale("it-IT")}
|
||||
>
|
||||
Italiano
|
||||
</GhostButton>
|
||||
<GhostButton
|
||||
</GhostLightButton>
|
||||
<GhostLightButton
|
||||
className="w-full bg-[#35786f]"
|
||||
onClick={() => setLocale("ug-CN")}
|
||||
>
|
||||
ئۇيغۇرچە
|
||||
</GhostButton>
|
||||
</GhostLightButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { LanguageSettings } from "./LanguageSettings";
|
||||
import { auth } from "@/auth";
|
||||
import { headers } from "next/headers";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { GhostButton } from "../ui/buttons";
|
||||
import { GhostLightButton } from "@/design-system/base/button";
|
||||
|
||||
export async function Navbar() {
|
||||
const t = await getTranslations("navbar");
|
||||
@@ -15,16 +15,17 @@ export async function Navbar() {
|
||||
|
||||
return (
|
||||
<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")}
|
||||
</GhostButton>
|
||||
<GhostButton className="block! md:hidden!" href={"/"}>
|
||||
</GhostLightButton>
|
||||
<GhostLightButton className="block! md:hidden!" size="md" href={"/"}>
|
||||
<Home size={20} />
|
||||
</GhostButton>
|
||||
<div className="flex text-base md:text-xl gap-0.5 justify-center items-center flex-wrap">
|
||||
</GhostLightButton>
|
||||
<div className="flex gap-0.5 justify-center items-center flex-wrap">
|
||||
<LanguageSettings />
|
||||
<GhostButton
|
||||
className="md:hidden! block! navbar-btn p-2"
|
||||
<GhostLightButton
|
||||
className="md:hidden! block!"
|
||||
size="md"
|
||||
href="https://github.com/GoddoNebianU/learn-languages"
|
||||
>
|
||||
<Image
|
||||
@@ -33,33 +34,34 @@ export async function Navbar() {
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
</GhostButton>
|
||||
<GhostButton href="/folders" className="md:block! hidden! navbar-btn">
|
||||
</GhostLightButton>
|
||||
<GhostLightButton href="/folders" className="md:block! hidden!" size="md">
|
||||
{t("folders")}
|
||||
</GhostButton>
|
||||
<GhostButton href="/folders" className="md:hidden! block! navbar-btn p-2">
|
||||
</GhostLightButton>
|
||||
<GhostLightButton href="/folders" className="md:hidden! block!" size="md">
|
||||
<Folder size={20} />
|
||||
</GhostButton>
|
||||
<GhostButton
|
||||
className="hidden! md:block! navbar-btn"
|
||||
</GhostLightButton>
|
||||
<GhostLightButton
|
||||
className="hidden! md:block!"
|
||||
size="md"
|
||||
href="https://github.com/GoddoNebianU/learn-languages"
|
||||
>
|
||||
{t("sourceCode")}
|
||||
</GhostButton>
|
||||
</GhostLightButton>
|
||||
{
|
||||
(() => {
|
||||
return session &&
|
||||
<>
|
||||
<GhostButton href="/profile" className="hidden! md:block! text-sm md:text-base navbar-btn px-2 py-1">{t("profile")}</GhostButton>
|
||||
<GhostButton href="/profile" className="md:hidden! block! navbar-btn p-2">
|
||||
<GhostLightButton href="/profile" className="hidden! md:block!" size="md">{t("profile")}</GhostLightButton>
|
||||
<GhostLightButton href="/profile" className="md:hidden! block!" size="md">
|
||||
<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>
|
||||
<GhostButton href="/auth" className="md:hidden! block! navbar-btn p-2">
|
||||
<GhostLightButton href="/auth" className="hidden! md:block!" size="md">{t("sign_in")}</GhostLightButton>
|
||||
<GhostLightButton href="/auth" className="md:hidden! block!" size="md">
|
||||
<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 - 可滚动的卡片列表容器
|
||||
*
|
||||
* 用于显示可滚动的列表内容,如文件夹列表、文本对列表等
|
||||
* - 最大高度 96 (24rem)
|
||||
* - 垂直滚动
|
||||
* - 圆角边框
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <CardList>
|
||||
* {items.map(item => (
|
||||
* <div key={item.id}>{item.name}</div>
|
||||
* ))}
|
||||
* </CardList>
|
||||
* ```
|
||||
* 使用 Design System 重写的卡片列表组件
|
||||
*/
|
||||
import { VStack } from "@/design-system/layout/stack";
|
||||
|
||||
interface CardListProps {
|
||||
children: React.ReactNode;
|
||||
/** 额外的 CSS 类名 */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function CardList({ children, className = "" }: CardListProps) {
|
||||
return (
|
||||
<div className={`max-h-96 overflow-y-auto rounded-xl border border-gray-200 overflow-hidden ${className}`}>
|
||||
{children}
|
||||
<div className={`max-h-96 overflow-y-auto rounded-lg border-2 border-gray-200 ${className}`}>
|
||||
<VStack gap={0}>
|
||||
{children}
|
||||
</VStack>
|
||||
</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 {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Container({ children, className }: ContainerProps) {
|
||||
export function Container({ children, className = "" }: ContainerProps) {
|
||||
return (
|
||||
<div
|
||||
className={`w-full max-w-2xl mx-auto bg-white border border-gray-200 rounded-2xl ${className}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<DSContainer size="2xl" className={`mx-auto ${className}`}>
|
||||
<Card variant="bordered" padding="md">
|
||||
{children}
|
||||
</Card>
|
||||
</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 { useState } from "react";
|
||||
import { Input } from "@/components/ui/Input";
|
||||
import { Select, Option } from "@/components/ui/Select";
|
||||
import { Input } from "@/design-system/base/input";
|
||||
import { Select } from "@/design-system/base/select";
|
||||
import { VStack } from "@/design-system/layout/stack";
|
||||
|
||||
const COMMON_LANGUAGES = [
|
||||
{ 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") {
|
||||
setCustomInput("");
|
||||
onChange("other");
|
||||
@@ -48,15 +55,15 @@ export function LocaleSelector({ value, onChange }: LocaleSelectorProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<VStack gap={2}>
|
||||
<Select
|
||||
value={isCommonLanguage ? value : "other"}
|
||||
onChange={handleSelectChange}
|
||||
>
|
||||
{COMMON_LANGUAGES.map((lang) => (
|
||||
<Option key={lang.value} value={lang.value}>
|
||||
<option key={lang.value} value={lang.value}>
|
||||
{t(`translator.${lang.label}`)}
|
||||
</Option>
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
{showCustomInput && (
|
||||
@@ -66,9 +73,8 @@ export function LocaleSelector({ value, onChange }: LocaleSelectorProps) {
|
||||
onChange={(e) => handleCustomInputChange(e.target.value)}
|
||||
placeholder={t("folder_id.enterLanguageName")}
|
||||
variant="bordered"
|
||||
className="mt-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
/**
|
||||
* PageHeader - 页面标题组件
|
||||
*
|
||||
* 用于 PageLayout 内的页面标题,支持主标题和可选的副标题
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <PageHeader title="我的文件夹" subtitle="管理和组织你的学习内容" />
|
||||
* ```
|
||||
* 使用 Design System 重写的页面标题组件
|
||||
*/
|
||||
import { VStack } from "@/design-system/layout/stack";
|
||||
|
||||
interface PageHeaderProps {
|
||||
/** 页面主标题 */
|
||||
title: string;
|
||||
/** 可选的副标题/描述 */
|
||||
subtitle?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PageHeader({ title, subtitle }: PageHeaderProps) {
|
||||
export function PageHeader({ title, subtitle, className = "" }: PageHeaderProps) {
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-gray-800 mb-2">
|
||||
<VStack gap={2} className={`mb-6 ${className}`}>
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-gray-800">
|
||||
{title}
|
||||
</h1>
|
||||
{subtitle && (
|
||||
<p className="text-sm text-gray-500">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,61 +1,21 @@
|
||||
/**
|
||||
* PageLayout - 统一的页面布局组件
|
||||
* PageLayout - 页面布局组件
|
||||
*
|
||||
* 提供应用统一的标准页面布局:
|
||||
* - 绿色背景 (#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>
|
||||
* ```
|
||||
* 使用 Design System 重写的页面布局组件
|
||||
*/
|
||||
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 MaxWidth = "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "full";
|
||||
type AlignItems = "center" | "start" | "end";
|
||||
|
||||
interface PageLayoutProps {
|
||||
children: React.ReactNode;
|
||||
/** 额外的 CSS 类名,用于自定义布局行为 */
|
||||
className?: string;
|
||||
/** 布局变体 */
|
||||
variant?: PageLayoutVariant;
|
||||
/** 最大宽度(仅对 full-width 变体有效) */
|
||||
maxWidth?: MaxWidth;
|
||||
/** 内容垂直对齐方式(仅对 centered-card 变体有效) */
|
||||
align?: AlignItems;
|
||||
align?: "center" | "start" | "end";
|
||||
}
|
||||
|
||||
// 最大宽度映射
|
||||
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> = {
|
||||
const alignClasses = {
|
||||
center: "items-center",
|
||||
start: "items-start",
|
||||
end: "items-end",
|
||||
@@ -65,13 +25,12 @@ export function PageLayout({
|
||||
children,
|
||||
className = "",
|
||||
variant = "centered-card",
|
||||
maxWidth = "2xl",
|
||||
align = "center",
|
||||
}: PageLayoutProps) {
|
||||
// 默认变体:居中白色卡片布局
|
||||
// 居中卡片布局
|
||||
if (variant === "centered-card") {
|
||||
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">
|
||||
<Card padding="lg" className="p-6 md:p-8">
|
||||
{children}
|
||||
@@ -81,21 +40,21 @@ export function PageLayout({
|
||||
);
|
||||
}
|
||||
|
||||
// 全宽布局:绿色背景,最大宽度容器,无白色卡片
|
||||
// 全宽布局
|
||||
if (variant === "full-width") {
|
||||
return (
|
||||
<div className={`min-h-[calc(100vh-64px)] bg-[#35786f] px-4 py-8 ${className}`}>
|
||||
<div className={`w-full ${maxWidthClasses[maxWidth]} mx-auto`}>
|
||||
<div className={`min-h-[calc(100vh-64px)] bg-primary-500 px-4 py-8 ${className}`}>
|
||||
<Container size="2xl">
|
||||
{children}
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 全屏布局:仅绿色背景,无其他限制
|
||||
// 全屏布局
|
||||
if (variant === "fullscreen") {
|
||||
return (
|
||||
<div className={`min-h-[calc(100vh-64px)] bg-[#35786f] ${className}`}>
|
||||
<div className={`min-h-[calc(100vh-64px)] bg-primary-500 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,9 @@ export function RangeInput({
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
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={{
|
||||
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 组件导出
|
||||
// 可以从 '@/components/ui' 导入所有组件
|
||||
|
||||
// 表单组件
|
||||
export { Input } from './Input';
|
||||
export { Select, Option } from './Select';
|
||||
export { Textarea } from './Textarea';
|
||||
export { RangeInput } from './RangeInput';
|
||||
export type { InputVariant } from './Input';
|
||||
export type { SelectSize } from './Select';
|
||||
export type { TextareaVariant } from './Textarea';
|
||||
|
||||
// 按钮组件
|
||||
export { Button } from './Button';
|
||||
// Design System 组件(向后兼容)
|
||||
export { Input, type InputVariant, type InputProps } from '@/design-system/base/input';
|
||||
export { Select, type SelectVariant, type SelectSize, type SelectProps } from '@/design-system/base/select';
|
||||
export { Textarea, type TextareaVariant, type TextareaProps } from '@/design-system/base/textarea';
|
||||
export { Card, type CardVariant, type CardPadding, type CardProps } from '@/design-system/base/card';
|
||||
export {
|
||||
Button,
|
||||
PrimaryButton,
|
||||
SecondaryButton,
|
||||
LightButton,
|
||||
SuccessButton,
|
||||
WarningButton,
|
||||
ErrorButton,
|
||||
GhostButton,
|
||||
GhostLightButton,
|
||||
OutlineButton,
|
||||
LinkButton,
|
||||
IconButton,
|
||||
IconClick,
|
||||
CircleButton,
|
||||
CircleToggleButton,
|
||||
GhostButton,
|
||||
LinkButton,
|
||||
DashedButton,
|
||||
} from './buttons';
|
||||
export type { ButtonVariant, ButtonSize, ButtonProps } from './Button';
|
||||
type ButtonVariant,
|
||||
type ButtonSize,
|
||||
type ButtonProps
|
||||
} from '@/design-system/base/button';
|
||||
|
||||
// 布局组件
|
||||
// 业务特定组件
|
||||
export { RangeInput } from './RangeInput';
|
||||
export { Container } from './Container';
|
||||
export { PageLayout } from './PageLayout';
|
||||
export { PageHeader } from './PageHeader';
|
||||
export { CardList } from './CardList';
|
||||
export { Card } from './Card';
|
||||
export type { CardProps, CardVariant, CardPadding } from './Card';
|
||||
|
||||
// 复合组件
|
||||
export { LocaleSelector } from './LocaleSelector';
|
||||
|
||||
@@ -41,7 +41,7 @@ import { cn } from "@/design-system/lib/utils";
|
||||
*/
|
||||
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: {
|
||||
variant: {
|
||||
@@ -51,6 +51,7 @@ const buttonVariants = cva(
|
||||
warning: "bg-warning-500 text-white hover:bg-warning-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-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",
|
||||
link: "text-primary-500 hover:text-primary-600 hover:underline shadow-none px-0",
|
||||
},
|
||||
@@ -138,15 +139,18 @@ export function Button({
|
||||
type = "button",
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
// 确保 size 有默认值
|
||||
const actualSize = size ?? "md";
|
||||
|
||||
// 计算样式
|
||||
const computedClass = cn(
|
||||
buttonVariants({ variant, size, fullWidth }),
|
||||
buttonVariants({ variant, size: actualSize, fullWidth }),
|
||||
selected && variant === "secondary" && "bg-gray-200",
|
||||
className
|
||||
);
|
||||
|
||||
// 图标尺寸映射
|
||||
const iconSize = { sm: 14, md: 16, lg: 20 }[size];
|
||||
const iconSize = { sm: 14, md: 16, lg: 20 }[actualSize];
|
||||
|
||||
// 渲染 SVG 图标
|
||||
const renderSvgIcon = (icon: React.ReactNode, position: "left" | "right") => {
|
||||
@@ -248,6 +252,9 @@ export const SecondaryButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="secondary" {...props} />
|
||||
);
|
||||
|
||||
// LightButton: 次要按钮的别名(向后兼容)
|
||||
export const LightButton = SecondaryButton;
|
||||
|
||||
export const SuccessButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="success" {...props} />
|
||||
);
|
||||
@@ -264,6 +271,11 @@ export const GhostButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="ghost" {...props} />
|
||||
);
|
||||
|
||||
// GhostLightButton: 透明按钮(白色文字,用于深色背景)
|
||||
export const GhostLightButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="ghost-light" {...props} />
|
||||
);
|
||||
|
||||
export const OutlineButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<Button variant="outline" {...props} />
|
||||
);
|
||||
@@ -271,3 +283,69 @@ export const OutlineButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
export const LinkButton = (props: Omit<ButtonProps, "variant">) => (
|
||||
<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(
|
||||
// 基础样式
|
||||
"rounded-2xl bg-white transition-all duration-250",
|
||||
"rounded-lg bg-white transition-all duration-250",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -34,11 +34,11 @@ import { cn } from "@/design-system/lib/utils";
|
||||
*/
|
||||
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: {
|
||||
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",
|
||||
filled: "border-transparent bg-gray-100",
|
||||
search: "border-gray-200 bg-white pl-10 rounded-full",
|
||||
|
||||
@@ -186,12 +186,13 @@ export function RadioGroup({
|
||||
// 为每个 Radio 注入 name 和 onChange
|
||||
const enhancedChildren = React.Children.map(children, (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,
|
||||
checked: value !== undefined ? child.props.value === value : undefined,
|
||||
checked: value !== undefined ? childProps.value === value : undefined,
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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(
|
||||
// 基础样式
|
||||
"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: {
|
||||
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",
|
||||
filled: "border-transparent bg-gray-100",
|
||||
},
|
||||
|
||||
@@ -104,19 +104,22 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
||||
onChange?.(e);
|
||||
};
|
||||
|
||||
// 确保 size 有默认值
|
||||
const actualSize = size ?? "md";
|
||||
|
||||
// 滑块大小
|
||||
const thumbSize = {
|
||||
sm: "h-3.5 w-3.5",
|
||||
md: "h-4 w-4",
|
||||
lg: "h-5 w-5",
|
||||
}[size];
|
||||
}[actualSize];
|
||||
|
||||
// 滑块位移
|
||||
const thumbTranslate = {
|
||||
sm: isChecked ? "translate-x-4" : "translate-x-0.5",
|
||||
md: isChecked ? "translate-x-5" : "translate-x-0.5",
|
||||
lg: isChecked ? "translate-x-6" : "translate-x-0.5",
|
||||
}[size];
|
||||
}[actualSize];
|
||||
|
||||
const renderSwitch = () => (
|
||||
<div className="relative inline-block">
|
||||
|
||||
@@ -27,7 +27,7 @@ import { cn } from "@/design-system/lib/utils";
|
||||
*/
|
||||
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: {
|
||||
variant: {
|
||||
|
||||
@@ -98,13 +98,16 @@ export function Badge({
|
||||
info: "bg-info-500",
|
||||
};
|
||||
|
||||
// 确保 variant 有默认值
|
||||
const actualVariant = variant ?? "default";
|
||||
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant, size, dot }), className)} {...props}>
|
||||
<div className={cn(badgeVariants({ variant: actualVariant, size, dot }), className)} {...props}>
|
||||
{dot && (
|
||||
<span
|
||||
className={cn(
|
||||
"h-2 w-2 rounded-full",
|
||||
dotColor || dotColors[variant]
|
||||
dotColor || dotColors[actualVariant]
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -95,7 +95,7 @@ export function Divider({
|
||||
<div
|
||||
className={cn(dividerVariants({ variant, orientation }), className)}
|
||||
role="separator"
|
||||
aria-orientation={orientation}
|
||||
aria-orientation={orientation as "horizontal" | "vertical"}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ import { cn } from "@/design-system/lib/utils";
|
||||
*/
|
||||
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: {
|
||||
variant: {
|
||||
@@ -127,6 +127,9 @@ export function Alert({
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
// 确保 variant 有默认值
|
||||
const actualVariant = variant ?? "info";
|
||||
|
||||
// 图标颜色
|
||||
const iconColors = {
|
||||
info: "text-info-500",
|
||||
@@ -137,14 +140,14 @@ export function Alert({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
className={cn(alertVariants({ variant: actualVariant }), className)}
|
||||
role="alert"
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* 图标 */}
|
||||
<div className={cn("shrink-0", iconColors[variant])}>
|
||||
{icon || defaultIcons[variant]}
|
||||
<div className={cn("shrink-0", iconColors[actualVariant])}>
|
||||
{icon || defaultIcons[actualVariant]}
|
||||
</div>
|
||||
|
||||
{/* 内容 */}
|
||||
|
||||
@@ -102,7 +102,8 @@ export function Progress({
|
||||
warning: "bg-warning-500",
|
||||
error: "bg-error-500",
|
||||
};
|
||||
return colors[variant];
|
||||
const actualVariant = variant ?? "default";
|
||||
return colors[actualVariant];
|
||||
};
|
||||
|
||||
// 格式化标签
|
||||
@@ -175,7 +176,7 @@ export function CircularProgress({
|
||||
warning: "#f59e0b",
|
||||
error: "#ef4444",
|
||||
};
|
||||
const strokeColor = colors[variant];
|
||||
const strokeColor = colors[variant ?? "default"];
|
||||
|
||||
return (
|
||||
<div className={cn("inline-flex items-center justify-center", className)}>
|
||||
|
||||
@@ -125,6 +125,7 @@ export interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonE
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
variant?: "line" | "enclosed" | "soft";
|
||||
"data-state"?: string;
|
||||
}
|
||||
|
||||
const triggerVariants = cva(
|
||||
@@ -148,14 +149,15 @@ export function TabsTrigger({
|
||||
children,
|
||||
variant = "line",
|
||||
className,
|
||||
"data-state": dataState,
|
||||
...props
|
||||
}: TabsTriggerProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={props["data-state"] === "active"}
|
||||
data-state={props["data-state"]}
|
||||
aria-selected={dataState === "active"}
|
||||
data-state={dataState}
|
||||
className={cn(triggerVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
@@ -170,21 +172,24 @@ export function TabsTrigger({
|
||||
export interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
"data-state"?: string;
|
||||
}
|
||||
|
||||
export function TabsContent({
|
||||
value,
|
||||
children,
|
||||
className,
|
||||
"data-state": dataState,
|
||||
...props
|
||||
}: TabsContentProps) {
|
||||
if (value !== props["data-state"]) return null;
|
||||
if (value !== dataState) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
className={cn("mt-4 focus:outline-none", className)}
|
||||
tabIndex={0}
|
||||
data-state={dataState}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -105,7 +105,7 @@ export function Modal({
|
||||
{/* 模态框内容 */}
|
||||
<div
|
||||
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],
|
||||
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