Add PDF translation API, utilities, docs, and config
Introduces core backend and frontend infrastructure for a PDF translation interface. Adds API endpoints for translation, PDF testing, and AI provider testing; implements PDF text extraction, cost tracking, and pricing logic in the lib directory; adds reusable UI components; and provides comprehensive documentation (SDD, environment setup, Claude instructions). Updates Tailwind and global styles, and includes a sample test PDF and configuration files.
This commit is contained in:
67
app/api/test-api/route.ts
Normal file
67
app/api/test-api/route.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
import { generateText } from "ai"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { provider, apiKey } = await request.json()
|
||||
|
||||
if (!provider || !apiKey) {
|
||||
return NextResponse.json({ error: "缺少必要參數" }, { status: 400 })
|
||||
}
|
||||
|
||||
let model
|
||||
let modelName: string
|
||||
|
||||
if (provider === "openai") {
|
||||
// Test OpenAI
|
||||
modelName = "gpt-4o-mini"
|
||||
model = openai(modelName, { apiKey })
|
||||
} else {
|
||||
// Test DeepSeek
|
||||
modelName = "deepseek-chat"
|
||||
const deepseek = createOpenAI({
|
||||
apiKey: apiKey,
|
||||
baseURL: "https://api.deepseek.com",
|
||||
})
|
||||
model = deepseek(modelName)
|
||||
}
|
||||
|
||||
// Test with a simple prompt
|
||||
const { text } = await generateText({
|
||||
model: model,
|
||||
prompt: "Hello, this is a test. Please respond with 'API connection successful!'",
|
||||
maxTokens: 50,
|
||||
temperature: 0,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "API 連接成功!",
|
||||
provider,
|
||||
model: modelName,
|
||||
testResponse: text
|
||||
})
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("API Test Error:", error)
|
||||
|
||||
let errorMessage = "API 測試失敗"
|
||||
|
||||
if (error.message?.includes("API key")) {
|
||||
errorMessage = "API 金鑰無效或已過期"
|
||||
} else if (error.message?.includes("rate limit")) {
|
||||
errorMessage = "API 請求次數超過限制"
|
||||
} else if (error.message?.includes("quota")) {
|
||||
errorMessage = "API 配額已用完"
|
||||
} else if (error.message?.includes("Not Found")) {
|
||||
errorMessage = "API 端點錯誤或模型不存在"
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
error: errorMessage,
|
||||
details: error.message
|
||||
}, { status: 400 })
|
||||
}
|
||||
}
|
42
app/api/test-pdf/route.ts
Normal file
42
app/api/test-pdf/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { extractTextFromPDF } from "@/lib/pdf-processor"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get("file") as File
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "No file provided" }, { status: 400 })
|
||||
}
|
||||
|
||||
if (file.type !== "application/pdf") {
|
||||
return NextResponse.json({ error: "File must be PDF" }, { status: 400 })
|
||||
}
|
||||
|
||||
console.log(`Testing PDF: ${file.name}, size: ${file.size} bytes`)
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
|
||||
const result = await extractTextFromPDF(buffer)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
result: {
|
||||
text: result.text,
|
||||
textLength: result.text.length,
|
||||
pageCount: result.pageCount,
|
||||
isScanned: result.isScanned,
|
||||
metadata: result.metadata
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error("PDF test error:", error)
|
||||
return NextResponse.json({
|
||||
error: `PDF test failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
details: error instanceof Error ? error.stack : undefined
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
@@ -1,30 +1,133 @@
|
||||
import { type NextRequest, NextResponse } from "next/server"
|
||||
import { createOpenAI } from "@ai-sdk/openai"
|
||||
import { openai } from "@ai-sdk/openai"
|
||||
import { generateText } from "ai"
|
||||
import { extractTextFromPDF, generateTranslatedPDF, processImageFile, processPDFWithOCR, ocrLanguageMap, isImageFile, isPDFFile } from "@/lib/pdf-processor"
|
||||
import { calculateCost, estimateTokens, formatTokenCount, MODEL_PRICING } from "@/lib/pricing"
|
||||
import { costTracker } from "@/lib/cost-tracker"
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Select AI provider based on environment variable
|
||||
const aiProvider = process.env.AI_PROVIDER || "deepseek"
|
||||
|
||||
let model
|
||||
let modelName: string
|
||||
|
||||
if (aiProvider === "openai") {
|
||||
// Use OpenAI
|
||||
modelName = process.env.OPENAI_MODEL || "gpt-4o-mini"
|
||||
model = openai(modelName)
|
||||
} else {
|
||||
// Use DeepSeek (default)
|
||||
modelName = process.env.DEEPSEEK_MODEL || "deepseek-chat"
|
||||
const deepseek = createOpenAI({
|
||||
apiKey: process.env.DEEPSEEK_API_KEY,
|
||||
baseURL: process.env.DEEPSEEK_BASE_URL || "https://api.deepseek.com",
|
||||
})
|
||||
model = deepseek(modelName)
|
||||
}
|
||||
|
||||
const formData = await request.formData()
|
||||
const file = formData.get("file") as File
|
||||
const targetLanguage = formData.get("targetLanguage") as string
|
||||
const sourceLanguage = formData.get("sourceLanguage") as string
|
||||
const returnPDF = formData.get("returnPDF") === "true"
|
||||
|
||||
if (!file || !targetLanguage) {
|
||||
return NextResponse.json({ error: "缺少必要參數" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Extract text from PDF
|
||||
// Validate file type
|
||||
if (!isPDFFile(file.type) && !isImageFile(file.type)) {
|
||||
return NextResponse.json({ error: "請上傳 PDF 檔案或圖片檔案" }, { status: 400 })
|
||||
}
|
||||
|
||||
// Check file size (10MB limit)
|
||||
const maxSize = parseInt(process.env.MAX_FILE_SIZE || "10485760")
|
||||
if (file.size > maxSize) {
|
||||
return NextResponse.json({ error: "檔案太大,請上傳小於 10MB 的檔案" }, { status: 413 })
|
||||
}
|
||||
|
||||
// Process file based on type
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
|
||||
let extractedText = ""
|
||||
let metadata: any = {}
|
||||
|
||||
try {
|
||||
if (isImageFile(file.type)) {
|
||||
// Image file - OCR功能已停用
|
||||
console.log("Image file detected - OCR功能已停用")
|
||||
extractedText = "目前僅支援包含文字的 PDF 文件,不支援圖片檔案。"
|
||||
metadata = { title: file.name, type: 'image' }
|
||||
} else if (isPDFFile(file.type)) {
|
||||
// Process PDF file
|
||||
console.log("Processing PDF file...")
|
||||
const result = await extractTextFromPDF(buffer)
|
||||
metadata = result.metadata
|
||||
|
||||
if (result.isScanned) {
|
||||
// PDF is scanned or text extraction failed
|
||||
console.log("Detected scanned PDF or text extraction failed")
|
||||
const message = result.metadata?.message || "此 PDF 為掃描檔案,目前僅支援包含文字的 PDF 文件。"
|
||||
|
||||
return NextResponse.json({
|
||||
error: `${message}
|
||||
|
||||
// For demo purposes, we'll simulate PDF text extraction
|
||||
// In production, you'd use a library like pdf-parse
|
||||
const pdfText = `這是從PDF提取的示例文本。在實際應用中,這裡會是真實的PDF內容。
|
||||
📋 可能的原因:
|
||||
• PDF 是掃描的圖片檔案
|
||||
• PDF 文件已加密或受保護
|
||||
• PDF 內容格式特殊,無法提取文字
|
||||
• PDF 文件損壞
|
||||
|
||||
這個應用展示了如何使用AI來翻譯文檔內容。您可以上傳任何PDF文件,選擇目標語言,然後獲得翻譯結果。
|
||||
💡 建議:
|
||||
• 嘗試其他包含純文字的 PDF 文件
|
||||
• 確認 PDF 可以在其他軟體中複製文字
|
||||
• 如果是掃描檔案,建議轉換為圖片格式`,
|
||||
details: {
|
||||
pageCount: result.pageCount,
|
||||
textLength: result.metadata?.extractedTextLength || 0,
|
||||
hasTextContent: result.metadata?.hasTextContent || false
|
||||
}
|
||||
}, { status: 400 })
|
||||
} else {
|
||||
// PDF has extractable text, use it directly
|
||||
console.log("PDF contains extractable text, using direct extraction")
|
||||
extractedText = result.text
|
||||
}
|
||||
}
|
||||
|
||||
if (!extractedText || extractedText.trim().length === 0) {
|
||||
extractedText = "無法從檔案擷取文字內容。請確認檔案包含可讀取的文字或清晰的圖像。"
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("File processing error:", error)
|
||||
|
||||
// Provide helpful error message for PDF conversion issues
|
||||
if (error instanceof Error && error.message.includes('PDF 轉圖片失敗')) {
|
||||
return NextResponse.json({
|
||||
error: `📄 掃描 PDF 需要額外工具支援
|
||||
|
||||
主要功能包括:
|
||||
- 支持多種語言翻譯
|
||||
- 清爽的用戶介面
|
||||
- 簡單易用的操作流程`
|
||||
🎯 建議解決方案:
|
||||
1. 💡 立即可用:將 PDF 轉換為圖片格式(JPG/PNG)後上傳
|
||||
- 使用 PDF 閱讀器截圖
|
||||
- 或使用線上 PDF 轉圖片工具
|
||||
|
||||
2. 🔧 安裝系統工具:
|
||||
• Windows: 下載安裝 ImageMagick (https://imagemagick.org/script/download.php#windows)
|
||||
• Mac: brew install imagemagick
|
||||
• Linux: apt-get install imagemagick
|
||||
|
||||
📸 提示:圖片格式的 OCR 識別效果通常比掃描 PDF 更好!`,
|
||||
suggestion: "convert_to_image",
|
||||
downloadLink: "https://imagemagick.org/script/download.php#windows"
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
extractedText = `檔案處理過程中發生錯誤:${error instanceof Error ? error.message : '未知錯誤'}`
|
||||
}
|
||||
|
||||
// Get language name for better translation context
|
||||
const languageNames: Record<string, string> = {
|
||||
@@ -40,23 +143,137 @@ export async function POST(request: NextRequest) {
|
||||
pt: "Português",
|
||||
ru: "Русский",
|
||||
ar: "العربية",
|
||||
hi: "हिन्दी",
|
||||
th: "ไทย",
|
||||
vi: "Tiếng Việt",
|
||||
}
|
||||
|
||||
const targetLanguageName = languageNames[targetLanguage] || targetLanguage
|
||||
const sourceLanguageName = languageNames[sourceLanguage] || "自動偵測"
|
||||
|
||||
// Translate using AI SDK
|
||||
const { text: translatedText } = await generateText({
|
||||
model: "openai/gpt-4o-mini",
|
||||
prompt: `請將以下文本翻譯成${targetLanguageName}。保持原文的格式和結構,只翻譯內容:
|
||||
// Prepare translation prompt
|
||||
const prompt = `You are a professional translator. Translate the following text from ${sourceLanguageName} to ${targetLanguageName}.
|
||||
Keep the original format and structure. Only translate the content, preserving line breaks and paragraphs.
|
||||
If the text appears to be an error message or system message, translate it appropriately.
|
||||
|
||||
${pdfText}`,
|
||||
Text to translate:
|
||||
|
||||
${extractedText}`
|
||||
|
||||
// Estimate input tokens
|
||||
const estimatedInputTokens = estimateTokens(prompt)
|
||||
|
||||
let translatedText: string
|
||||
let usage: any = null
|
||||
|
||||
try {
|
||||
// Try to translate using selected AI provider
|
||||
const result = await generateText({
|
||||
model: model,
|
||||
prompt: prompt,
|
||||
temperature: 0.3, // Lower temperature for more accurate translation
|
||||
maxTokens: 4000,
|
||||
})
|
||||
translatedText = result.text
|
||||
usage = result.usage
|
||||
} catch (error) {
|
||||
console.error("AI API Error:", error)
|
||||
// Fallback to a simple mock translation for demo purposes
|
||||
translatedText = `[模擬翻譯結果]\n\n原文內容: ${extractedText}\n\n注意: 這是模擬翻譯結果,因為 AI API 連接失敗。請檢查 API 金鑰配置。\n\n目標語言: ${targetLanguageName}\n來源語言: ${sourceLanguageName}\n\n實際應用中,這裡會顯示真正的 AI 翻譯結果。`
|
||||
}
|
||||
|
||||
// Calculate token usage and cost
|
||||
const tokenUsage = {
|
||||
promptTokens: usage?.promptTokens || estimatedInputTokens,
|
||||
completionTokens: usage?.completionTokens || estimateTokens(translatedText),
|
||||
totalTokens: (usage?.promptTokens || estimatedInputTokens) + (usage?.completionTokens || estimateTokens(translatedText))
|
||||
}
|
||||
|
||||
const costCalculation = calculateCost(modelName, tokenUsage)
|
||||
const modelDisplayName = MODEL_PRICING[modelName as keyof typeof MODEL_PRICING]?.displayName || modelName
|
||||
|
||||
// Track cost in accumulator (client-side will handle storage)
|
||||
const costSession = {
|
||||
model: modelName,
|
||||
provider: aiProvider,
|
||||
tokenUsage: {
|
||||
promptTokens: tokenUsage.promptTokens,
|
||||
completionTokens: tokenUsage.completionTokens,
|
||||
totalTokens: tokenUsage.totalTokens
|
||||
},
|
||||
cost: {
|
||||
inputCost: costCalculation.inputCost,
|
||||
outputCost: costCalculation.outputCost,
|
||||
totalCost: costCalculation.totalCost,
|
||||
currency: costCalculation.currency
|
||||
}
|
||||
}
|
||||
|
||||
// Generate translated PDF if requested
|
||||
let pdfBase64 = ""
|
||||
if (returnPDF) {
|
||||
try {
|
||||
const pdfBytes = await generateTranslatedPDF(translatedText, metadata, targetLanguage)
|
||||
pdfBase64 = Buffer.from(pdfBytes).toString('base64')
|
||||
} catch (error) {
|
||||
console.error("PDF generation error:", error)
|
||||
// Continue without PDF generation
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
translatedText,
|
||||
pdfBase64: pdfBase64,
|
||||
hasPDF: pdfBase64.length > 0,
|
||||
tokenUsage: {
|
||||
promptTokens: tokenUsage.promptTokens,
|
||||
completionTokens: tokenUsage.completionTokens,
|
||||
totalTokens: tokenUsage.totalTokens,
|
||||
formattedCounts: {
|
||||
prompt: formatTokenCount(tokenUsage.promptTokens),
|
||||
completion: formatTokenCount(tokenUsage.completionTokens),
|
||||
total: formatTokenCount(tokenUsage.totalTokens)
|
||||
}
|
||||
},
|
||||
cost: {
|
||||
inputCost: costCalculation.inputCost,
|
||||
outputCost: costCalculation.outputCost,
|
||||
totalCost: costCalculation.totalCost,
|
||||
formattedCost: costCalculation.formattedCost,
|
||||
currency: costCalculation.currency
|
||||
},
|
||||
model: {
|
||||
name: modelName,
|
||||
displayName: modelDisplayName,
|
||||
provider: aiProvider
|
||||
},
|
||||
costSession: costSession
|
||||
})
|
||||
|
||||
return NextResponse.json({ translatedText })
|
||||
} catch (error) {
|
||||
console.error("翻譯錯誤:", error)
|
||||
return NextResponse.json({ error: "翻譯過程中發生錯誤" }, { status: 500 })
|
||||
|
||||
// Check for specific error types
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("API key")) {
|
||||
return NextResponse.json({ error: "API 金鑰配置錯誤,請聯繫管理員" }, { status: 500 })
|
||||
}
|
||||
if (error.message.includes("rate limit")) {
|
||||
return NextResponse.json({ error: "API 請求過於頻繁,請稍後再試" }, { status: 429 })
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "翻譯過程中發生錯誤,請稍後再試" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// OPTIONS method for CORS
|
||||
export async function OPTIONS(request: NextRequest) {
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
},
|
||||
})
|
||||
}
|
168
app/globals.css
168
app/globals.css
@@ -1,122 +1,74 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(0.95 0.02 85);
|
||||
--foreground: oklch(0.25 0.05 250);
|
||||
--card: oklch(0.98 0.01 85);
|
||||
--card-foreground: oklch(0.25 0.05 250);
|
||||
--popover: oklch(0.98 0.01 85);
|
||||
--popover-foreground: oklch(0.25 0.05 250);
|
||||
--primary: oklch(0.3 0.08 250);
|
||||
--primary-foreground: oklch(0.98 0.01 85);
|
||||
--secondary: oklch(0.92 0.02 85);
|
||||
--secondary-foreground: oklch(0.25 0.05 250);
|
||||
--muted: oklch(0.92 0.02 85);
|
||||
--muted-foreground: oklch(0.5 0.03 250);
|
||||
--accent: oklch(0.65 0.18 35);
|
||||
--accent-foreground: oklch(0.98 0.01 85);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.98 0.01 85);
|
||||
--border: oklch(0.3 0.08 250);
|
||||
--input: oklch(0.98 0.01 85);
|
||||
--ring: oklch(0.65 0.18 35);
|
||||
--chart-1: oklch(0.65 0.18 35);
|
||||
--chart-2: oklch(0.3 0.08 250);
|
||||
--chart-3: oklch(0.5 0.15 200);
|
||||
--chart-4: oklch(0.7 0.12 150);
|
||||
--chart-5: oklch(0.55 0.2 60);
|
||||
--radius: 0.25rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.2 0.03 250);
|
||||
--foreground: oklch(0.95 0.02 85);
|
||||
--card: oklch(0.25 0.03 250);
|
||||
--card-foreground: oklch(0.95 0.02 85);
|
||||
--popover: oklch(0.25 0.03 250);
|
||||
--popover-foreground: oklch(0.95 0.02 85);
|
||||
--primary: oklch(0.95 0.02 85);
|
||||
--primary-foreground: oklch(0.25 0.05 250);
|
||||
--secondary: oklch(0.3 0.04 250);
|
||||
--secondary-foreground: oklch(0.95 0.02 85);
|
||||
--muted: oklch(0.3 0.04 250);
|
||||
--muted-foreground: oklch(0.65 0.03 250);
|
||||
--accent: oklch(0.65 0.18 35);
|
||||
--accent-foreground: oklch(0.98 0.01 85);
|
||||
--destructive: oklch(0.5 0.2 27);
|
||||
--destructive-foreground: oklch(0.95 0.02 85);
|
||||
--border: oklch(0.35 0.05 250);
|
||||
--input: oklch(0.3 0.04 250);
|
||||
--ring: oklch(0.65 0.18 35);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
@theme {
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
--radius: var(--radius);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
Reference in New Issue
Block a user