diff --git a/.codebuddy/plans/texture-atlas-browser-upgrade_c5a9c6ec.md b/.codebuddy/plans/texture-atlas-browser-upgrade_c5a9c6ec.md new file mode 100644 index 0000000..d956a12 --- /dev/null +++ b/.codebuddy/plans/texture-atlas-browser-upgrade_c5a9c6ec.md @@ -0,0 +1,310 @@ +--- +name: texture-atlas-browser-upgrade +overview: 重构合图工具页面,实现三栏布局(左侧文件列表、中间Canvas实时预览、右侧参数配置),支持文件夹拖拽上传,使用 Web Worker 在浏览器端完成合图处理,并新增精灵动画预览弹窗功能。 +design: + architecture: + framework: react + styleKeywords: + - Professional Tool + - Dark Theme + - Three-Column Layout + - Glassmorphism Panels + - Minimal Borders + fontSystem: + fontFamily: PingFang SC + heading: + size: 18px + weight: 600 + subheading: + size: 14px + weight: 500 + body: + size: 13px + weight: 400 + colorSystem: + primary: + - "#3B82F6" + - "#2563EB" + - "#1D4ED8" + background: + - "#0A0A0B" + - "#18181B" + - "#27272A" + text: + - "#FAFAFA" + - "#A1A1AA" + - "#71717A" + functional: + - "#22C55E" + - "#EF4444" + - "#F59E0B" +todos: + - id: atlas-store + content: 创建atlasStore状态管理,定义精灵列表、配置、结果等状态及actions + status: completed + - id: browser-packer + content: 使用 [skill:frontend-patterns] 迁移打包算法到浏览器端,创建atlas-packer.ts实现MaxRects和Shelf算法 + status: completed + - id: web-worker + content: 创建atlas-worker.ts,实现Web Worker打包处理和消息通信协议 + status: completed + dependencies: + - browser-packer + - id: file-list-panel + content: 使用 [skill:frontend-design] 开发左侧FileListPanel组件,支持文件夹拖拽上传和文件列表展示 + status: completed + dependencies: + - atlas-store + - id: canvas-preview + content: 使用 [skill:frontend-design] 开发中间CanvasPreview组件,实现实时渲染、缩放平移功能 + status: completed + dependencies: + - atlas-store + - web-worker + - id: animation-dialog + content: 使用 [skill:frontend-design] 开发AnimationPreviewDialog组件,实现精灵动画帧播放和帧率控制 + status: completed + dependencies: + - canvas-preview + - id: page-integration + content: 使用 [skill:vercel-react-best-practices] 重构texture-atlas/page.tsx,整合三栏布局和所有功能组件 + status: completed + dependencies: + - file-list-panel + - canvas-preview + - animation-dialog +--- + +## 产品概述 + +重构合图工具页面,从传统的两栏布局升级为三栏专业工作台布局。左侧文件列表面板显示上传的精灵图文件,中间Canvas实时预览合图结果,右侧参数配置面板调整合图参数。核心目标是将服务端处理迁移到浏览器端,使用 Web Worker 实现无阻塞的合图算法处理。 + +## 核心功能 + +1. **三栏工作台布局**:左侧文件树/列表、中间Canvas预览、右侧参数配置,支持面板宽度调整 +2. **文件夹拖拽上传**:支持 webkitdirectory 单层文件夹上传,图片平铺显示并按文件名排序 +3. **实时Canvas预览**:参数变更时自动触发合图计算,Canvas实时渲染结果,支持缩放和平移 +4. **Web Worker浏览器端处理**:将 MaxRects 和 Shelf 打包算法迁移到 Web Worker,实现非阻塞处理 +5. **精灵动画预览弹窗**:支持选择精灵帧按顺序播放动画,可调节帧率(FPS),循环播放 +6. **导出下载**:支持导出合图图片和元数据文件(Cocos2d plist、Cocos Creator JSON、通用JSON) + +## 技术栈 + +- 框架:Next.js 15 + React 19 + TypeScript +- 样式:Tailwind CSS + CSS Variables +- UI组件:Radix UI (Dialog已有) + Framer Motion +- 状态管理:Zustand (扩展现有 uploadStore) +- 国际化:现有 i18n 系统 (en.json / zh.json) +- 图像处理:Canvas API + Web Worker + OffscreenCanvas + +## 技术架构 + +### 系统架构 + +```mermaid +graph TB + subgraph UI Layer + A[FileListPanel] --> B[AtlasStore] + C[CanvasPreview] --> B + D[ConfigPanel] --> B + E[AnimationDialog] --> B + end + + subgraph Processing Layer + B --> F[Web Worker] + F --> G[MaxRects Algorithm] + F --> H[Shelf Algorithm] + F --> I[Canvas Composite] + end + + subgraph Export Layer + B --> J[Plist Exporter] + B --> K[JSON Exporter] + B --> L[Image Download] + end +``` + +### 模块划分 + +- **UI模块**:三栏布局组件、文件列表面板、Canvas预览组件、动画预览弹窗 +- **状态模块**:扩展现有Zustand store,管理文件列表、合图配置、处理状态 +- **Worker模块**:浏览器端打包算法实现,处理图片合成 +- **导出模块**:复用现有导出格式逻辑,适配浏览器端 + +### 数据流 + +```mermaid +flowchart LR + A[文件夹拖拽上传] --> B[解析图片文件] + B --> C[加载为ImageBitmap] + C --> D[存入Store] + D --> E[触发Worker处理] + E --> F[执行打包算法] + F --> G[返回布局数据] + G --> H[Canvas渲染预览] + H --> I[用户调整参数] + I --> E +``` + +## 实现细节 + +### 核心目录结构 + +``` +src/ +├── app/(dashboard)/tools/texture-atlas/ +│ └── page.tsx # 重构:三栏布局主页面 +├── components/tools/ +│ ├── atlas/ +│ │ ├── FileListPanel.tsx # 新增:左侧文件列表面板 +│ │ ├── CanvasPreview.tsx # 新增:中间Canvas预览组件 +│ │ ├── AtlasConfigPanel.tsx # 新增:右侧配置面板(基于ConfigPanel) +│ │ └── AnimationPreviewDialog.tsx # 新增:动画预览弹窗 +│ └── FileUploader.tsx # 修改:支持文件夹上传 +├── lib/ +│ ├── atlas-worker.ts # 新增:Web Worker主文件 +│ ├── atlas-packer.ts # 新增:浏览器端打包算法(从texture-atlas.ts迁移) +│ └── atlas-exporter.ts # 新增:导出格式生成器 +├── store/ +│ └── atlasStore.ts # 新增:合图专用状态管理 +└── types/ + └── index.ts # 修改:添加浏览器端类型定义 +``` + +### 关键代码结构 + +**浏览器端精灵类型定义**:用于存储加载的图片及其元信息,使用 ImageBitmap 代替 Buffer 以适配浏览器环境。 + +```typescript +// 浏览器端精灵类型 +interface BrowserSprite { + id: string; + name: string; + width: number; + height: number; + image: ImageBitmap; +} + +// 合图Store状态 +interface AtlasState { + sprites: BrowserSprite[]; + config: TextureAtlasConfig; + result: AtlasResult | null; + isProcessing: boolean; + previewScale: number; +} +``` + +**Web Worker消息协议**:定义Worker与主线程之间的通信格式,支持打包计算和进度回调。 + +```typescript +// Worker消息协议 +interface WorkerMessage { + type: 'pack' | 'cancel'; + sprites: { id: string; width: number; height: number }[]; + config: TextureAtlasConfig; +} + +interface WorkerResult { + type: 'result' | 'progress' | 'error'; + placements?: Map; + progress?: number; + error?: string; +} +``` + +### 技术实现要点 + +1. **文件夹上传** + +- 使用 `webkitdirectory` 属性支持文件夹选择 +- 过滤非图片文件,按文件名自然排序 +- 使用 `createImageBitmap` 异步加载图片 + +2. **Web Worker处理** + +- 将 MaxRects 和 Shelf 算法迁移到 Worker +- 仅传递尺寸数据到 Worker,图片保留在主线程 +- Worker 返回布局坐标,主线程执行 Canvas 合成 + +3. **Canvas实时预览** + +- 使用 Canvas 2D Context 渲染合图结果 +- 支持鼠标滚轮缩放和拖拽平移 +- 配置变更触发防抖重新计算 + +4. **动画预览** + +- 基于现有 Dialog 组件实现弹窗 +- 使用 requestAnimationFrame 控制帧率 +- 从合图结果中裁剪精灵帧序列播放 + +### 性能优化 + +- 使用防抖处理配置变更,避免频繁重计算 +- Worker 处理打包算法,不阻塞UI线程 +- ImageBitmap 高效图片加载和渲染 +- Canvas缓存合图结果,缩放时仅变换视图 + +## 设计风格 + +采用专业工具型界面设计,三栏工作台布局参考主流图形编辑软件(如TexturePacker、Figma)。整体风格延续项目现有的暗色主题设计,使用玻璃态面板分隔各功能区域。 + +## 页面规划 + +### 合图工作台页面 (单页面三栏布局) + +**整体布局** + +- 三栏横向布局,左中右比例约 240px : auto : 320px +- 面板之间使用微妙的边框分隔,支持视觉层次区分 +- 中间预览区域占据主要空间,深色背景突出Canvas + +**左侧文件列表面板** + +- 顶部:文件夹名称显示、文件数量统计、清空按钮 +- 中部:文件列表区域,每项显示缩略图、文件名、尺寸信息 +- 底部:拖拽上传提示区域,支持点击选择文件夹 +- 支持列表项hover高亮,点击选中状态 + +**中间Canvas预览区域** + +- 顶部工具栏:缩放比例显示、适应窗口按钮、动画预览按钮 +- 主体:深色棋盘格背景,Canvas居中显示合图结果 +- 支持鼠标滚轮缩放(10%-400%)、拖拽平移 +- 空状态:显示拖拽上传引导图标和文字 + +**右侧参数配置面板** + +- 复用现有ConfigPanel组件样式 +- 分组展示:尺寸设置、布局设置、输出设置 +- 底部:生成按钮、下载按钮组 + +**动画预览弹窗** + +- 居中弹窗,尺寸 640x480 +- 顶部:标题、关闭按钮 +- 中部:Canvas播放区域,深色背景 +- 底部:播放/暂停按钮、帧率滑块(1-60fps)、当前帧/总帧数显示 + +## Agent Extensions + +### Skill + +- **frontend-design** +- 用途:设计三栏工作台布局、Canvas预览区域、动画预览弹窗等UI界面 +- 预期结果:生成专业、美观的合图工具界面,符合现有项目设计风格 + +- **frontend-patterns** +- 用途:实现Web Worker通信、Canvas渲染、状态管理等前端模式 +- 预期结果:代码结构清晰,性能优化到位,符合React最佳实践 + +- **vercel-react-best-practices** +- 用途:优化React组件性能,处理大量图片文件时的渲染优化 +- 预期结果:组件渲染高效,避免不必要的重渲染 + +### SubAgent + +- **code-explorer** +- 用途:在实现过程中探索现有组件复用、类型定义、国际化键值等 +- 预期结果:充分复用现有代码,保持项目一致性 \ No newline at end of file diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 314a8f6..392755d 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -6,10 +6,10 @@ export default function DashboardLayout({ children: React.ReactNode; }) { return ( -
+
-
{children}
+ {children}
); diff --git a/src/app/(dashboard)/tools/audio-compress/page.tsx b/src/app/(dashboard)/tools/audio-compress/page.tsx index 3b104af..7974f94 100644 --- a/src/app/(dashboard)/tools/audio-compress/page.tsx +++ b/src/app/(dashboard)/tools/audio-compress/page.tsx @@ -110,7 +110,7 @@ export default function AudioCompressPage() { [addFile] ); - const handleConfigChange = (id: string, value: any) => { + const handleConfigChange = (id: string, value: string | number | boolean | undefined) => { setConfig((prev) => ({ ...prev, [id]: value })); }; diff --git a/src/app/(dashboard)/tools/image-compress/page.tsx b/src/app/(dashboard)/tools/image-compress/page.tsx index ee99138..dc5fe11 100644 --- a/src/app/(dashboard)/tools/image-compress/page.tsx +++ b/src/app/(dashboard)/tools/image-compress/page.tsx @@ -75,13 +75,31 @@ async function uploadFile(file: File): Promise<{ fileId: string } | null> { return { fileId: data.fileId }; } +interface ProcessResult { + success: boolean; + data?: { + fileUrl: string; + filename: string; + metadata: { + originalSize: number; + compressedSize: number; + compressionRatio: number; + format: string; + quality?: number; + width?: number; + height?: number; + }; + }; + error?: string; +} + /** * Process image compression */ async function processImageCompression( fileId: string, config: ImageCompressConfig -): Promise<{ success: boolean; data?: any; error?: string }> { +): Promise { const response = await fetch("/api/process/image-compress", { method: "POST", headers: { @@ -131,7 +149,7 @@ export default function ImageCompressPage() { [addFile] ); - const handleConfigChange = (id: string, value: any) => { + const handleConfigChange = (id: string, value: string | number | boolean | undefined) => { setConfig((prev) => ({ ...prev, [id]: value })); }; @@ -299,10 +317,7 @@ export default function ImageCompressPage() { ({ - ...opt, - value: config[opt.id as keyof ImageCompressConfig], - }))} + options={configOptions} onChange={handleConfigChange} onReset={handleResetConfig} /> diff --git a/src/app/(dashboard)/tools/texture-atlas/page.tsx b/src/app/(dashboard)/tools/texture-atlas/page.tsx index 1054e14..cb55a11 100644 --- a/src/app/(dashboard)/tools/texture-atlas/page.tsx +++ b/src/app/(dashboard)/tools/texture-atlas/page.tsx @@ -1,487 +1,103 @@ "use client"; -import { useState, useCallback, useEffect } from "react"; +import { useEffect } from "react"; import { motion } from "framer-motion"; -import { Layers as LayersIcon, Box, Download, Archive } from "lucide-react"; -import { FileUploader } from "@/components/tools/FileUploader"; -import { ProgressBar } from "@/components/tools/ProgressBar"; -import { ConfigPanel, type ConfigOption } from "@/components/tools/ConfigPanel"; -import { Button } from "@/components/ui/button"; -import { useUploadStore } from "@/store/uploadStore"; -import { generateId } from "@/lib/utils"; -import { useTranslation, getServerTranslations } from "@/lib/i18n"; -import type { UploadedFile, TextureAtlasConfig } from "@/types"; - -const imageAccept = { - "image/*": [".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"], -}; - -const defaultConfig: TextureAtlasConfig = { - maxWidth: 1024, - maxHeight: 1024, - padding: 2, - allowRotation: false, - pot: true, - format: "png", - quality: 80, - outputFormat: "cocos2d", - algorithm: "MaxRects", -}; - -function useConfigOptions(config: TextureAtlasConfig, getT: (key: string) => string): ConfigOption[] { - return [ - { - id: "maxWidth", - type: "slider", - label: getT("config.textureAtlas.maxWidth"), - description: getT("config.textureAtlas.maxWidthDescription"), - value: config.maxWidth, - min: 256, - max: 4096, - step: 256, - suffix: "px", - icon: , - }, - { - id: "maxHeight", - type: "slider", - label: getT("config.textureAtlas.maxHeight"), - description: getT("config.textureAtlas.maxHeightDescription"), - value: config.maxHeight, - min: 256, - max: 4096, - step: 256, - suffix: "px", - icon: , - }, - { - id: "padding", - type: "slider", - label: getT("config.textureAtlas.padding"), - description: getT("config.textureAtlas.paddingDescription"), - value: config.padding, - min: 0, - max: 16, - step: 1, - suffix: "px", - }, - { - id: "allowRotation", - type: "select", - label: getT("config.textureAtlas.allowRotation"), - description: getT("config.textureAtlas.allowRotationDescription"), - value: config.allowRotation, - options: [ - { label: getT("common.no"), value: false }, - { label: getT("common.yes"), value: true }, - ], - }, - { - id: "pot", - type: "select", - label: getT("config.textureAtlas.pot"), - description: getT("config.textureAtlas.potDescription"), - value: config.pot, - options: [ - { label: getT("common.no"), value: false }, - { label: getT("common.yes"), value: true }, - ], - }, - { - id: "format", - type: "select", - label: getT("config.textureAtlas.format"), - description: getT("config.textureAtlas.formatDescription"), - value: config.format, - options: [ - { label: getT("config.textureAtlas.formatPng"), value: "png" }, - { label: getT("config.textureAtlas.formatWebp"), value: "webp" }, - ], - }, - { - id: "quality", - type: "slider", - label: getT("config.textureAtlas.quality"), - description: getT("config.textureAtlas.qualityDescription"), - value: config.quality, - min: 1, - max: 100, - step: 1, - suffix: "%", - }, - { - id: "outputFormat", - type: "select", - label: getT("config.textureAtlas.outputFormat"), - description: getT("config.textureAtlas.outputFormatDescription"), - value: config.outputFormat, - options: [ - { label: getT("config.textureAtlas.outputCocosCreator"), value: "cocos-creator" }, - { label: getT("config.textureAtlas.outputCocos2d"), value: "cocos2d" }, - { label: getT("config.textureAtlas.outputGeneric"), value: "generic-json" }, - ], - }, - { - id: "algorithm", - type: "select", - label: getT("config.textureAtlas.algorithm"), - description: getT("config.textureAtlas.algorithmDescription"), - value: config.algorithm, - options: [ - { label: getT("config.textureAtlas.algorithmMaxRects"), value: "MaxRects" }, - { label: getT("config.textureAtlas.algorithmShelf"), value: "Shelf" }, - ], - }, - ]; -} - -/** - * Upload a file to the server - */ -async function uploadFile(file: File): Promise<{ fileId: string } | null> { - const formData = new FormData(); - formData.append("file", file); - - const response = await fetch("/api/upload", { - method: "POST", - body: formData, - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.error || "Upload failed"); - } - - const data = await response.json(); - return { fileId: data.fileId }; -} - -/** - * Process texture atlas creation - */ -async function processTextureAtlas( - fileIds: string[], - filenames: string[], - config: TextureAtlasConfig -): Promise<{ success: boolean; data?: any; error?: string }> { - const response = await fetch("/api/process/texture-atlas", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ fileIds, filenames, config }), - }); - - const data = await response.json(); - - if (!response.ok) { - return { success: false, error: data.error || "Processing failed" }; - } - - return { success: true, data }; -} - -interface AtlasResult { - id: string; - imageUrl: string; - metadataUrl: string; - zipUrl: string; - imageFilename: string; - metadataFilename: string; - zipFilename: string; - metadata: { - width: number; - height: number; - format: string; - frameCount: number; - outputFormat: string; - }; - createdAt: Date; -} +import { Layers as LayersIcon } from "lucide-react"; +import { + FileListPanel, + CanvasPreview, + AtlasConfigPanel, + AnimationPreviewDialog +} from "@/components/tools/atlas"; +import { useAtlasStore } from "@/store/atlasStore"; +import { useAtlasWorker } from "@/hooks/useAtlasWorker"; +import { useSafeTranslation } from "@/lib/i18n"; export default function TextureAtlasPage() { - const [mounted, setMounted] = useState(false); - useEffect(() => setMounted(true), []); - const { t } = useTranslation(); + const { t, mounted } = useSafeTranslation(); + + const { sprites, config } = useAtlasStore(); + const { pack } = useAtlasWorker(); - const getT = (key: string, params?: Record) => { - if (!mounted) return getServerTranslations("en").t(key, params); - return t(key, params); + // Auto-pack when sprites or config changes (with debounce) + useEffect(() => { + if (sprites.length === 0) return; + + const timer = setTimeout(() => { + pack(); + }, 300); + + return () => clearTimeout(timer); + }, [sprites, config, pack]); + + // Cleanup on unmount + useEffect(() => { + return () => { + // Don't clear sprites on unmount - keep state for when user returns + }; + }, []); + + const getT = (key: string) => { + if (!mounted) return key.split(".").pop() || key; + return t(key); }; - const { files, addFile, removeFile, clearFiles, processingStatus, setProcessingStatus } = - useUploadStore(); - - const [config, setConfig] = useState(defaultConfig); - const [atlasResult, setAtlasResult] = useState(null); - - const handleFilesDrop = useCallback( - (acceptedFiles: File[]) => { - const newFiles: UploadedFile[] = acceptedFiles.map((file) => ({ - id: generateId(), - file, - name: file.name, - size: file.size, - type: file.type, - uploadedAt: new Date(), - })); - - newFiles.forEach((file) => addFile(file)); - }, - [addFile] - ); - - const handleConfigChange = (id: string, value: any) => { - setConfig((prev) => ({ ...prev, [id]: value })); - }; - - const handleResetConfig = () => { - setConfig(defaultConfig); - }; - - const handleProcess = async () => { - if (files.length === 0) return; - - setProcessingStatus({ - status: "uploading", - progress: 0, - message: getT("processing.uploadingSprites"), - }); - - const fileIds: string[] = []; - const filenames: string[] = []; - const errors: string[] = []; - - try { - // Upload all files - for (let i = 0; i < files.length; i++) { - const file = files[i]; - - const uploadProgress = Math.round(((i + 0.5) / files.length) * 50); - setProcessingStatus({ - status: "uploading", - progress: uploadProgress, - message: getT("processing.uploadProgress", { progress: uploadProgress }), - }); - - try { - const uploadResult = await uploadFile(file.file); - if (!uploadResult) { - throw new Error("Upload failed"); - } - fileIds.push(uploadResult.fileId); - filenames.push(file.name); - } catch (error) { - errors.push( - `${file.name}: ${error instanceof Error ? error.message : "Upload failed"}` - ); - } - } - - if (fileIds.length === 0) { - throw new Error("No files were successfully uploaded"); - } - - // Create atlas - setProcessingStatus({ - status: "processing", - progress: 75, - message: getT("processing.creatingAtlas"), - }); - - const result = await processTextureAtlas(fileIds, filenames, config); - - if (result.success && result.data) { - setAtlasResult({ - id: generateId(), - imageUrl: result.data.imageUrl, - metadataUrl: result.data.metadataUrl, - zipUrl: result.data.zipUrl, - imageFilename: result.data.imageFilename, - metadataFilename: result.data.metadataFilename, - zipFilename: result.data.zipFilename, - metadata: result.data.metadata, - createdAt: new Date(), - }); - - clearFiles(); - - setProcessingStatus({ - status: "completed", - progress: 100, - message: getT("processing.atlasComplete"), - }); - } else { - throw new Error(result.error || "Failed to create texture atlas"); - } - } catch (error) { - setProcessingStatus({ - status: "failed", - progress: 0, - message: getT("processing.processingFailed"), - error: error instanceof Error ? error.message : getT("processing.unknownError"), - }); - } - }; - - const handleDownload = (url: string, filename: string) => { - const link = document.createElement("a"); - link.href = url; - link.download = filename; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }; - - const canProcess = - files.length > 0 && processingStatus.status !== "processing" && files.length <= 500; - const configOptions = useConfigOptions(config, getT); - return ( -
- -
-
-
- -
-
-

{getT("tools.textureAtlas.title")}

-

- {getT("tools.textureAtlas.description")} -

-
+
+ {/* Header */} + +
+
+
-
- -
-
- - - ({ - ...opt, - value: config[opt.id as keyof TextureAtlasConfig], - }))} - onChange={handleConfigChange} - onReset={handleResetConfig} - /> - - {canProcess && ( - - )} -
- -
- {processingStatus.status !== "idle" && ( - - )} - - {atlasResult && ( -
-

- - {getT("results.processingComplete")} -

- -
- Texture Atlas -
- -
-
- {getT("tools.textureAtlas.dimensions")}: -

- {atlasResult.metadata.width} x {atlasResult.metadata.height} -

-
-
- {getT("tools.textureAtlas.sprites")}: -

{atlasResult.metadata.frameCount}

-
-
- {getT("tools.textureAtlas.imageFormat")}: -

{atlasResult.metadata.format}

-
-
- {getT("tools.textureAtlas.dataFormat")}: -

- {atlasResult.metadata.outputFormat === "cocos-creator" - ? "Cocos Creator JSON" - : atlasResult.metadata.outputFormat === "cocos2d" - ? "Cocos2d plist" - : "Generic JSON"} -

-
-
- -
- -
- - -
-
-
- )} - -
-

{getT("tools.textureAtlas.features")}

-
    - {(getT("tools.textureAtlas.featureList") as unknown as string[]).map( - (feature, index) => ( -
  • • {feature}
  • - ) - )} -
-
+
+

{getT("tools.textureAtlas.title")}

+

+ {getT("tools.textureAtlas.description")} +

+ + {/* Three-column layout */} +
+ {/* Left panel - File list */} + + + + + {/* Center panel - Canvas preview */} + + + + + {/* Right panel - Config */} + + + +
+ + {/* Animation preview dialog */} +
); } diff --git a/src/app/(dashboard)/tools/video-frames/page.tsx b/src/app/(dashboard)/tools/video-frames/page.tsx index d03559a..3c33d46 100644 --- a/src/app/(dashboard)/tools/video-frames/page.tsx +++ b/src/app/(dashboard)/tools/video-frames/page.tsx @@ -97,7 +97,7 @@ export default function VideoFramesPage() { [addFile] ); - const handleConfigChange = (id: string, value: any) => { + const handleConfigChange = (id: string, value: string | number | boolean | undefined) => { setConfig((prev) => ({ ...prev, [id]: value })); }; diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index d6602e0..c7771c0 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,11 +1,14 @@ "use client"; import Link from "next/link"; +import { usePathname } from "next/navigation"; import { Sparkles } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { useTranslation, getServerTranslations } from "@/lib/i18n"; +import { cn } from "@/lib/utils"; export function Footer() { + const pathname = usePathname(); const [mounted, setMounted] = useState(false); const { t } = useTranslation(); @@ -26,8 +29,14 @@ export function Footer() { [mounted] ); + // Check if we're in the dashboard area (tools pages) + const isDashboard = pathname?.startsWith("/tools"); + return ( -