diff --git a/packages/meta-sort-core/src/api/DuplicatesAPI.ts b/packages/meta-sort-core/src/api/DuplicatesAPI.ts deleted file mode 100644 index f928070..0000000 --- a/packages/meta-sort-core/src/api/DuplicatesAPI.ts +++ /dev/null @@ -1,166 +0,0 @@ -/** - * HTTP REST API Server for Duplicate Files - * - * Provides endpoints for accessing duplicate file detection results. - * Separate from FUSE API to maintain proper separation of concerns. - */ - -import * as http from 'http'; - -export interface DuplicatesAPIConfig { - /** Port to listen on (default: 3003) */ - port?: number; - /** Host to bind to (default: 'localhost') */ - host?: string; - /** Enable CORS (default: true) */ - enableCors?: boolean; - /** Mount path for duplicates endpoint (default: '/duplicates') */ - mountPath?: string; -} - -export class DuplicatesAPI { - private server: http.Server | null = null; - private config: Required; - - constructor(config: DuplicatesAPIConfig = {}) { - this.config = { - port: config.port ?? 3003, - host: config.host ?? 'localhost', - enableCors: config.enableCors ?? true, - mountPath: config.mountPath ?? '/duplicates' - }; - } - - /** - * Start the HTTP server - */ - public async start(): Promise { - return new Promise((resolve, reject) => { - this.server = http.createServer(this.handleRequest.bind(this)); - - this.server.on('error', reject); - - this.server.listen(this.config.port, this.config.host, () => { - console.log(`Duplicates API listening on http://${this.config.host}:${this.config.port}${this.config.mountPath}`); - resolve(); - }); - }); - } - - /** - * Stop the HTTP server - */ - public async stop(): Promise { - if (!this.server) { - return; - } - - return new Promise((resolve, reject) => { - this.server!.close((err) => { - if (err) { - reject(err); - } else { - console.log('Duplicates API stopped'); - this.server = null; - resolve(); - } - }); - }); - } - - /** - * Handle incoming HTTP requests - */ - private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise { - // CORS headers - if (this.config.enableCors) { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - res.writeHead(204); - res.end(); - return; - } - } - - try { - const url = new URL(req.url!, `http://${req.headers.host}`); - const pathname = url.pathname; - const method = req.method!; - - // Route the request - if (method === 'GET' && pathname === this.config.mountPath) { - await this.handleDuplicates(res); - } else if (method === 'GET' && pathname === '/health') { - this.handleHealth(res); - } else { - this.sendError(res, 404, 'Not Found'); - } - } catch (error) { - console.error('Error handling request:', error); - this.sendError(res, 500, 'Internal Server Error', error); - } - } - - /** - * Send JSON response - */ - private sendJson(res: http.ServerResponse, data: any, statusCode: number = 200): void { - res.writeHead(statusCode, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(data, null, 2)); - } - - /** - * Send error response - */ - private sendError(res: http.ServerResponse, statusCode: number, message: string, error?: any): void { - this.sendJson(res, { - error: message, - details: error?.message ?? error - }, statusCode); - } - - /** - * GET /duplicates - * Get duplicate files - * NOTE: This API is deprecated. Use UnifiedAPIServer's /api/duplicates endpoint instead. - * Disk-based duplicate logging has been removed for performance. - */ - private async handleDuplicates(res: http.ServerResponse): Promise { - try { - // Return empty results - duplicate detection now happens in-memory via UnifiedAPIServer - const hashDuplicates: Array<{ key: string; files: string[]; virtualPath?: string }> = []; - const titleDuplicates: Array<{ key: string; files: string[]; virtualPath?: string }> = []; - - this.sendJson(res, { - hashDuplicates, - titleDuplicates, - stats: { - hashGroupCount: 0, - hashFileCount: 0, - titleGroupCount: 0, - titleFileCount: 0 - }, - deprecated: true, - message: 'This API is deprecated. Use UnifiedAPIServer /api/duplicates endpoint instead.' - }); - } catch (error) { - console.error('Error in duplicates endpoint:', error); - this.sendError(res, 500, 'Failed to get duplicate data', error); - } - } - - /** - * GET /health - * Health check endpoint - */ - private handleHealth(res: http.ServerResponse): void { - this.sendJson(res, { - status: 'ok', - service: 'duplicates-api', - timestamp: new Date().toISOString() - }); - } -} diff --git a/packages/meta-sort-core/src/api/UnifiedAPIServer.ts b/packages/meta-sort-core/src/api/UnifiedAPIServer.ts index 72dc46b..acb09f4 100644 --- a/packages/meta-sort-core/src/api/UnifiedAPIServer.ts +++ b/packages/meta-sort-core/src/api/UnifiedAPIServer.ts @@ -10,7 +10,6 @@ import fastifyCors from '@fastify/cors'; import { VirtualFileSystem } from './VirtualFileSystem.js'; import { performanceMetrics } from '../metrics/PerformanceMetrics.js'; import { UnifiedProcessingStateManager } from '../logic/UnifiedProcessingStateManager.js'; -import { DuplicateResult } from '../logic/DuplicateFinder.js'; import type { IKVClient } from '../kv/IKVClient.js'; import type { KVManager } from '../kv/KVManager.js'; import type { ExtendedProcessingSnapshot, IKVClientWithPubSub } from '../types/ExtendedInterfaces.js'; @@ -35,7 +34,6 @@ export class UnifiedAPIServer { private app: FastifyInstance; private vfs: VirtualFileSystem; private unifiedStateManager: UnifiedProcessingStateManager | null = null; - private getDuplicateResult: (() => DuplicateResult | null) | null = null; private kvClient: IKVClient | null = null; private kvManager: KVManager | null = null; private getQueueStatus: (() => any) | null = null; @@ -56,7 +54,6 @@ export class UnifiedAPIServer { vfs: VirtualFileSystem, config: UnifiedAPIServerConfig = {}, unifiedStateManager?: UnifiedProcessingStateManager, - getDuplicateResult?: () => DuplicateResult | null, kvClient?: IKVClient, backgroundQueueConcurrency?: number, fastQueueConcurrency?: number, @@ -67,7 +64,6 @@ export class UnifiedAPIServer { ) { this.vfs = vfs; this.unifiedStateManager = unifiedStateManager || null; - this.getDuplicateResult = getDuplicateResult || null; this.kvClient = kvClient || null; this.kvManager = kvManager || null; this.getQueueStatus = getQueueStatus || null; @@ -161,7 +157,6 @@ export class UnifiedAPIServer { services: { fuse: 'ok', metrics: 'ok', - duplicates: 'ok', unifiedProcessing: this.unifiedStateManager ? 'ok' : 'unavailable' } }; @@ -187,8 +182,7 @@ export class UnifiedAPIServer { // Metrics API routes (under /api/metrics) this.setupMetricsRoutes(); - // Duplicates API routes (under /api/duplicates) - this.setupDuplicatesRoutes(); + // NOTE: Duplicates API has been moved to meta-dup service (port 8183) // NOTE: Metadata API routes (/api/metadata/*) and KV Browser routes (/api/kv/*) // have been moved to meta-core Go API. The editor UI now uses meta-core's endpoints. @@ -492,59 +486,8 @@ export class UnifiedAPIServer { }); } - /** - * Setup Duplicates API routes - */ - private setupDuplicatesRoutes(): void { - // Get duplicate files (from stored duplicate data) - this.app.get('/api/duplicates', async (request, reply) => { - try { - // Get stored duplicate result from WatchedFileProcessor - const duplicateResult = this.getDuplicateResult ? this.getDuplicateResult() : null; - - if (!duplicateResult) { - // No duplicates found yet (processing hasn't completed) - return { - hashDuplicates: [], - titleDuplicates: [], - stats: { - hashGroupCount: 0, - hashFileCount: 0, - titleGroupCount: 0, - titleFileCount: 0 - } - }; - } - - return { - hashDuplicates: duplicateResult.hashDuplicates, - titleDuplicates: duplicateResult.titleDuplicates, - stats: { - hashGroupCount: duplicateResult.hashDuplicates.length, - hashFileCount: duplicateResult.hashDuplicates.reduce((sum, g) => sum + g.files.length, 0), - titleGroupCount: duplicateResult.titleDuplicates.length, - titleFileCount: duplicateResult.titleDuplicates.reduce((sum, g) => sum + g.files.length, 0) - } - }; - } catch (error) { - console.error('Error retrieving duplicates:', error); - return reply.status(500).send({ - error: 'Failed to retrieve duplicates', - details: (error as Error).message - }); - } - }); - - // Refresh duplicates (trigger re-computation) - this.app.post('/api/duplicates/refresh', async (request, reply) => { - // Currently just acknowledges the request - // The duplicate detection runs automatically during processing - return { - status: 'ok', - message: 'Duplicate refresh triggered' - }; - }); - } + // NOTE: Duplicates API has been moved to meta-dup service (port 8183) + // Use http://localhost:8183/api/duplicates for duplicate detection // NOTE: setupMetadataRoutes() and setupKVBrowserRoutes() have been removed. // The metadata editor API (/api/metadata/*) and KV browser API (/api/kv/*) @@ -1905,7 +1848,6 @@ export class UnifiedAPIServer { console.log(`Unified API Server listening on http://${this.config.host}:${this.config.port}`); console.log(` - FUSE API: http://${this.config.host}:${this.config.port}/api/fuse/*`); console.log(` - Metrics API: http://${this.config.host}:${this.config.port}/api/metrics`); - console.log(` - Duplicates API: http://${this.config.host}:${this.config.port}/api/duplicates`); if (this.kvClient) { console.log(` - Metadata API: http://${this.config.host}:${this.config.port}/api/metadata/* (GET/PUT/DELETE/POST)`); console.log(` - KV Browser API: http://${this.config.host}:${this.config.port}/api/kv/* (GET)`); diff --git a/packages/meta-sort-core/src/api/api.test.ts b/packages/meta-sort-core/src/api/api.test.ts index 35f00cd..aea3e09 100644 --- a/packages/meta-sort-core/src/api/api.test.ts +++ b/packages/meta-sort-core/src/api/api.test.ts @@ -9,7 +9,6 @@ import { expect } from 'chai'; import { UnifiedAPIServer } from './UnifiedAPIServer.js'; import { VirtualFileSystem } from './VirtualFileSystem.js'; import { UnifiedProcessingStateManager } from '../logic/UnifiedProcessingStateManager.js'; -import { DuplicateResult } from '../logic/DuplicateFinder.js'; import type { IKVClient } from '../kv/IKVClient.js'; import type { FastifyInstance } from 'fastify'; @@ -205,25 +204,7 @@ class MockProcessingStateManager { } } -/** - * Mock Duplicate Result Provider - */ -function createMockDuplicateResult(): DuplicateResult { - return { - hashDuplicates: [ - { - key: 'abc123', - files: ['/files/movie1.mp4', '/files/movie1-copy.mp4'] - } - ], - titleDuplicates: [ - { - key: 'Movie Title', - files: ['/files/movie1.mp4', '/files/movie2.mp4'] - } - ] - }; -} +// NOTE: Duplicates API has been moved to meta-dup service (port 8183) // ============================================================================= // Test Suite @@ -234,7 +215,6 @@ describe('UnifiedAPIServer', function() { let app: FastifyInstance; let mockKV: MockKVClient; let mockStateManager: MockProcessingStateManager; - let duplicateResult = createMockDuplicateResult(); before(async function() { // Create mocks @@ -268,7 +248,6 @@ describe('UnifiedAPIServer', function() { vfs, { port: 3000, host: 'localhost', enableCors: true }, mockStateManager as any, - () => duplicateResult, mockKV as any, 4, // backgroundQueueConcurrency 16, // fastQueueConcurrency @@ -396,36 +375,8 @@ describe('UnifiedAPIServer', function() { }); }); - // ========================================================================= - // Duplicates API (Duplicates Page) - // ========================================================================= - - describe('Duplicates API (/api/duplicates)', function() { - it('GET /api/duplicates returns duplicate analysis', async function() { - const response = await app.inject({ - method: 'GET', - url: '/api/duplicates' - }); - - expect(response.statusCode).to.equal(200); - const body = JSON.parse(response.payload); - expect(body).to.have.property('hashDuplicates'); - expect(body).to.have.property('titleDuplicates'); - expect(Array.isArray(body.hashDuplicates)).to.be.true; - expect(Array.isArray(body.titleDuplicates)).to.be.true; - }); - - it('POST /api/duplicates/refresh acknowledges request', async function() { - const response = await app.inject({ - method: 'POST', - url: '/api/duplicates/refresh' - }); - - expect(response.statusCode).to.equal(200); - const body = JSON.parse(response.payload); - expect(body).to.have.property('status'); - }); - }); + // NOTE: Duplicates API has been moved to meta-dup service (port 8183) + // See dev/test/suites/meta-dup.bats for duplicate detection tests // ========================================================================= // Stats API (Dashboard) diff --git a/packages/meta-sort-core/src/index.ts b/packages/meta-sort-core/src/index.ts index e97282b..25c1ce6 100644 --- a/packages/meta-sort-core/src/index.ts +++ b/packages/meta-sort-core/src/index.ts @@ -74,7 +74,6 @@ fileProcessor.setPipeline(pipeline); // VFS and state manager are available immediately const vfs = fileProcessor.getVirtualFileSystem(); const unifiedStateManager = fileProcessor.getUnifiedStateManager(); -const getDuplicateResult = () => fileProcessor.getDuplicateResult(); // API Server will be initialized after KV is ready (deferred to async init) let apiServer: UnifiedAPIServer | null = null; @@ -164,7 +163,7 @@ process.on('SIGINT', async () => { port: config.FUSE_API_PORT, host: config.FUSE_API_HOST, enableCors: true, - }, unifiedStateManager, getDuplicateResult, kvClient || undefined, backgroundQueueConcurrency, fastQueueConcurrency, () => fileProcessor.getQueueStatus(), kvManager || undefined, getPluginManagerInstance, () => fileProcessor.getTaskScheduler()); + }, unifiedStateManager, kvClient || undefined, backgroundQueueConcurrency, fastQueueConcurrency, () => fileProcessor.getQueueStatus(), kvManager || undefined, getPluginManagerInstance, () => fileProcessor.getTaskScheduler()); await apiServer.start(); console.log('Unified API Server started successfully'); diff --git a/packages/meta-sort-core/src/logic/DuplicateFinder.ts b/packages/meta-sort-core/src/logic/DuplicateFinder.ts deleted file mode 100644 index d92b450..0000000 --- a/packages/meta-sort-core/src/logic/DuplicateFinder.ts +++ /dev/null @@ -1,113 +0,0 @@ -import {CID_ALGORITHM_NAMES} from "@metazla/meta-hash"; -import {VideoMeta, HashMeta, VersionMeta, FileNameMeta, FileStatMeta} from "@metazla/meta-interface"; -import {renamingRule} from "../config/RenamingRule.js"; - -type Local = VideoMeta & HashMeta & VersionMeta & FileNameMeta & FileStatMeta; - -export interface DuplicateGroup { - key: string; - files: string[]; - virtualPath?: string; -} - -export interface DuplicateResult { - hashDuplicates: DuplicateGroup[]; - titleDuplicates: DuplicateGroup[]; -} - -export class DuplicateFinder { - async findDuplicates(database: Map): Promise { - let [hashDuplicates, titleDuplicates] = await Promise.all([ - this.findDuplicatesGeneric(database, this.hashKeyExtractor, 'remove'), - this.findDuplicatesGeneric(database, this.pathExtractor, 'version') - ]); - - return { - hashDuplicates, - titleDuplicates - }; - } - - private hashKeyExtractor(metadata: Local): string | null { - return metadata[CID_ALGORITHM_NAMES.sha256] ?? null; - } - - private pathExtractor(metadata: Local): string | null { - //case insensitive - try { - return renamingRule(metadata, '')?.toLowerCase() ?? null; - } catch (e) { - return null; - } - } - - private async findDuplicatesGeneric( - database: Map, - keyExtractor: (metadata: Local) => string | null, - action?: 'remove' | 'version' - ): Promise { - const duplicates = new Map(); - const duplicateGroups: DuplicateGroup[] = []; - - for (const [filePath, metadata] of database) { - const key = keyExtractor(metadata); - if (key) { - const files = duplicates.get(key) ?? []; - files.push(filePath); - duplicates.set(key, files); - } - } - - for (const [key, files] of duplicates) { - if (files.length > 1) { - // Store duplicate group before modifying database - const sortedFiles = [...files].sort(); - - // Generate virtualPath for both hash and title duplicates - let virtualPath: string | undefined; - if (action === 'version') { - // For title duplicates, use the key (normalized title path) - virtualPath = key; - } else if (action === 'remove') { - // For hash duplicates, use renamingRule on the first file's metadata - const firstFile = sortedFiles[0]; - const metadata = database.get(firstFile); - if (metadata) { - try { - virtualPath = renamingRule(metadata, ''); - } catch (e) { - // If renamingRule fails, virtualPath remains undefined - } - } - } - - duplicateGroups.push({ - key, - files: sortedFiles, - virtualPath - }); - - //alphabetical order - const remainingFiles = files.slice(1).sort(); - //rename all problematic entries V2, V3, V4, V5 - let i = 2; - for (const file of remainingFiles) { - if (action === 'remove') { - database.delete(file); - } else if (action === 'version') { - let definition = database.get(file); - definition.version = "V" + i; - if (definition.fileType !== "subtitle") {//allow multiple version of subtitle in the same folder - definition.extra = "true";//mark as extra so it don't pollute the main list - } - i++; - } else { - throw new Error(); - } - } - } - } - - return duplicateGroups; - } -} diff --git a/packages/meta-sort-core/src/logic/WatchedFileProcessor.ts b/packages/meta-sort-core/src/logic/WatchedFileProcessor.ts index 3c60940..f0536d4 100644 --- a/packages/meta-sort-core/src/logic/WatchedFileProcessor.ts +++ b/packages/meta-sort-core/src/logic/WatchedFileProcessor.ts @@ -2,7 +2,6 @@ import {dirname, join, parse} from 'path'; import {config} from "../config/EnvConfig.js"; import {FileProcessorInterface} from "@metazla/meta-hash"; import {MetaDataToFolderStruct} from "./MetaDataToFolderStruct.js"; -import {DuplicateFinder, DuplicateResult} from "./DuplicateFinder.js"; import {FileType} from "@metazla/filename-tools"; import {FileAnalyzerInterface} from "./fileProcessor/FileAnalyzerInterface.js"; import {FileProcessorPiscina} from "./fileProcessor/FileProcessorPiscina.js"; @@ -33,8 +32,6 @@ export class WatchedFileProcessor implements FileProcessorInterface { virtualFileSystem: VirtualFileSystem; unifiedStateManager = new UnifiedProcessingStateManager(); fileProcessor:FileAnalyzerInterface = new FileProcessorPiscina(); - duplicateFinder = new DuplicateFinder(); - duplicateResult: DuplicateResult | null = null; fileType = new FileType(); supported = ['video','subtitle','torrent']; @@ -77,10 +74,6 @@ export class WatchedFileProcessor implements FileProcessorInterface { return this.unifiedStateManager; } - getDuplicateResult(): DuplicateResult | null { - return this.duplicateResult; - } - /** * Get the KV client */ @@ -415,20 +408,10 @@ export class WatchedFileProcessor implements FileProcessorInterface { console.log(`No files to process`); return; } + + // NOTE: Duplicate detection has been moved to meta-dup service + let startTime = performance.now(); - const newDuplicateResult = await this.duplicateFinder.findDuplicates(this.fileProcessor.getDatabase());//find duplicates and removes them to avoid conflict - const duplicateTime = Math.ceil(performance.now() - startTime); - const totalDuplicates = newDuplicateResult.hashDuplicates.length + newDuplicateResult.titleDuplicates.length; - console.log(`Duplicate find took ${duplicateTime}ms - found ${newDuplicateResult.hashDuplicates.length} hash groups, ${newDuplicateResult.titleDuplicates.length} title groups`); - - // Only update duplicateResult if we found new duplicates, or if it's the first run - if (totalDuplicates > 0 || this.duplicateResult === null) { - this.duplicateResult = newDuplicateResult; - } - - performanceMetrics.recordDuplicateDetection(duplicateTime, totalDuplicates); - - startTime = performance.now(); console.log(`Generating virtual structure for ${this.fileProcessor.getDatabase().size} files`); const virtualStructure = this.metaDataToFolderStruct.generateVirtualStructure(this.fileProcessor.getDatabase()); const virtualStructureTime = Math.ceil(performance.now() - startTime); diff --git a/packages/meta-sort-editor/README.md b/packages/meta-sort-editor/README.md new file mode 100644 index 0000000..d66fc08 --- /dev/null +++ b/packages/meta-sort-editor/README.md @@ -0,0 +1,140 @@ +# @metazla/meta-editor + +User-friendly metadata editor for MetaMesh. Search and edit file metadata directly through a web interface. + +## Features + +- **Search**: Find files by title, filename, or hash ID +- **Edit**: Modify all metadata fields including title, season, episode, year, Jellyfin metadata, and more +- **Batch Operations**: Update multiple files simultaneously +- **Real-time**: Direct integration with etcd via REST API +- **User-friendly**: Clean, intuitive interface with form validation + +## Development + +### Prerequisites + +- Node.js 21.6.2+ +- pnpm 10.19.0+ +- Meta-mesh API server running (provides the `/api/metadata/*` endpoints) + +### Running Locally + +```bash +# From the root directory +pnpm run start:editor + +# Or from the package directory +cd packages/meta-editor +pnpm run dev +``` + +The editor will be available at `http://localhost:5173` with API proxying to `http://localhost:3000`. + +### Building + +```bash +# From the root directory +pnpm run build:editor + +# Or from the package directory +cd packages/meta-editor +pnpm run build +``` + +The built files will be in `dist/` directory. + +## Docker Deployment + +In the Docker development environment, the editor is automatically available at: + +``` +http://localhost/editor +``` + +The nginx configuration serves the built files from `/app/packages/meta-editor/dist/`. + +## API Integration + +The editor communicates with the UnifiedAPIServer (meta-mesh) via REST API: + +- `POST /api/metadata/search` - Search for files +- `GET /api/metadata/:hashId` - Get file metadata +- `PUT /api/metadata/:hashId` - Update complete metadata +- `PUT /api/metadata/:hashId/property` - Update specific property +- `POST /api/metadata/batch` - Batch update multiple files + +See `src/api/metadataApi.ts` for the API client implementation. + +## Usage + +### Searching for Files + +1. Select search type: "Search by Title/Filename" or "Search by Hash ID" +2. Enter your search query +3. Click "Search" +4. Results will appear in the left sidebar + +### Editing Metadata + +1. Select a file from the search results +2. Modify any fields in the editor form +3. Click "Save Changes" to persist updates +4. Use "Reset" to discard changes + +### Supported Metadata Fields + +**Basic Information:** +- Title, Original Title, Show Title +- Video Type (Movie, TV, Anime, Other) +- Year, Season, Episode + +**Jellyfin Metadata:** +- Plot, IMDb ID, TMDb ID, AniDB ID +- Director, Studio, Rating, Runtime +- Genres, Premiered Date, Release Date + +**File Information:** +- Filename, File Size, MIME Type +- Hash values (SHA-256, MD5, etc.) - read-only + +## Project Structure + +``` +packages/meta-editor/ +├── src/ +│ ├── api/ # API client for metadata operations +│ ├── components/ # React components +│ │ ├── SearchBar.tsx # Search interface +│ │ ├── FileList.tsx # Search results list +│ │ └── MetadataEditor.tsx # Main editing form +│ ├── types/ # TypeScript type definitions +│ ├── utils/ # Utility functions +│ ├── App.tsx # Main application component +│ ├── main.tsx # Entry point +│ └── index.css # Global styles +├── index.html +├── vite.config.ts +├── tsconfig.json +└── package.json +``` + +## Technology Stack + +- **React 18** - UI framework +- **TypeScript 5.7** - Type safety +- **Vite 6** - Build tool and dev server +- **Native CSS** - Styling (no CSS framework) + +## Contributing + +When adding new features: + +1. Add type definitions to `src/types/index.ts` +2. Update API client in `src/api/metadataApi.ts` +3. Create/update components as needed +4. Update this README with new features + +## License + +Part of the MetaMesh project. diff --git a/packages/meta-sort-editor/eslint.config.js b/packages/meta-sort-editor/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/packages/meta-sort-editor/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/packages/meta-sort-editor/index.html b/packages/meta-sort-editor/index.html new file mode 100644 index 0000000..7fd12a8 --- /dev/null +++ b/packages/meta-sort-editor/index.html @@ -0,0 +1,13 @@ + + + + + + + MetaMesh Metadata Editor + + +
+ + + diff --git a/packages/meta-sort-editor/package-lock.json b/packages/meta-sort-editor/package-lock.json new file mode 100644 index 0000000..7bc4dea --- /dev/null +++ b/packages/meta-sort-editor/package-lock.json @@ -0,0 +1,1570 @@ +{ + "name": "@meta-sort/editor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@meta-sort/editor", + "version": "0.1.0", + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.3.3", + "vite": "^5.0.12" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/packages/meta-sort-editor/package.json b/packages/meta-sort-editor/package.json new file mode 100644 index 0000000..c891c2a --- /dev/null +++ b/packages/meta-sort-editor/package.json @@ -0,0 +1,23 @@ +{ + "name": "@meta-sort/editor", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "iso-639-3": "^3.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.3.3", + "vite": "^5.0.12" + } +} diff --git a/packages/meta-sort-editor/src/App.css b/packages/meta-sort-editor/src/App.css new file mode 100644 index 0000000..bdc8d26 --- /dev/null +++ b/packages/meta-sort-editor/src/App.css @@ -0,0 +1,177 @@ +.app { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.app-header { + padding: 2rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + background-color: rgba(0, 0, 0, 0.2); +} + +.app-header h1 { + margin: 0 0 0.5rem 0; + color: #646cff; +} + +.app-description { + margin: 0; + color: rgba(255, 255, 255, 0.6); +} + +.app-main { + flex: 1; + padding: 2rem; + max-width: 1400px; + width: 100%; + margin: 0 auto; +} + +.app-error { + padding: 1rem; + background-color: rgba(255, 0, 0, 0.1); + border: 1px solid rgba(255, 0, 0, 0.3); + border-radius: 8px; + color: #ff6b6b; + margin-bottom: 1rem; +} + +.app-loading { + text-align: center; + padding: 3rem 2rem; + color: rgba(255, 255, 255, 0.7); + font-size: 1.1rem; +} + +.app-welcome { + text-align: center; + padding: 4rem 2rem; + color: rgba(255, 255, 255, 0.6); +} + +.app-welcome h2 { + margin: 0 0 1rem 0; + color: rgba(255, 255, 255, 0.8); +} + +.app-welcome-features { + margin-top: 1rem; + margin-bottom: 1.5rem; + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.5); +} + +.app-content { + display: grid; + grid-template-columns: 350px 1fr; + gap: 1.5rem; + margin-top: 1rem; +} + +.app-sidebar { + min-height: 400px; +} + +.app-editor { + min-height: 400px; +} + +.app-no-selection { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + min-height: 400px; + background-color: rgba(255, 255, 255, 0.05); + border-radius: 8px; + color: rgba(255, 255, 255, 0.5); + text-align: center; + padding: 2rem; +} + +.app-footer { + padding: 1.5rem 2rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + text-align: center; + color: rgba(255, 255, 255, 0.5); + font-size: 0.9rem; +} + +.app-footer a { + color: #646cff; + text-decoration: none; +} + +.app-footer a:hover { + text-decoration: underline; +} + +@media (max-width: 1024px) { + .app-content { + grid-template-columns: 1fr; + } + + .app-sidebar { + order: 2; + } + + .app-editor { + order: 1; + } +} + +@media (max-width: 768px) { + .app-header, + .app-main, + .app-footer { + padding: 1rem; + } + + .app-content { + gap: 1rem; + } +} + +@media (prefers-color-scheme: light) { + .app-header { + border-bottom-color: rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.02); + } + + .app-description { + color: rgba(0, 0, 0, 0.6); + } + + .app-error { + background-color: rgba(255, 0, 0, 0.1); + border-color: rgba(255, 0, 0, 0.3); + color: #cc0000; + } + + .app-loading { + color: rgba(0, 0, 0, 0.7); + } + + .app-welcome { + color: rgba(0, 0, 0, 0.6); + } + + .app-welcome h2 { + color: rgba(0, 0, 0, 0.8); + } + + .app-welcome-features { + color: rgba(0, 0, 0, 0.5); + } + + .app-no-selection { + background-color: rgba(0, 0, 0, 0.02); + color: rgba(0, 0, 0, 0.5); + } + + .app-footer { + border-top-color: rgba(0, 0, 0, 0.1); + color: rgba(0, 0, 0, 0.5); + } +} diff --git a/packages/meta-sort-editor/src/App.tsx b/packages/meta-sort-editor/src/App.tsx new file mode 100644 index 0000000..ed4290c --- /dev/null +++ b/packages/meta-sort-editor/src/App.tsx @@ -0,0 +1,242 @@ +import { useState, useEffect } from 'react'; +import { SearchBar } from './components/SearchBar'; +import { FileList } from './components/FileList'; +import { MetadataEditor } from './components/MetadataEditor'; +import { BulkMetadataEditor } from './components/BulkMetadataEditor'; +import { MetadataAPI } from './api/metadataApi'; +import { SearchResult } from './types'; +import './App.css'; + +function App() { + const [searchResults, setSearchResults] = useState([]); + const [selectedHashId, setSelectedHashId] = useState(); + const [selectedHashIds, setSelectedHashIds] = useState>(new Set()); + const [bulkEditMode, setBulkEditMode] = useState(false); + const [isSearching, setIsSearching] = useState(false); + const [isLoadingDirectHash, setIsLoadingDirectHash] = useState(false); + const [error, setError] = useState(null); + + // Extract hash from URL path (supports /editor/file/hash or /editor/?hash=...) + const getHashFromUrl = (): string | null => { + const path = window.location.pathname; + const fileMatch = path.match(/\/file\/([a-z0-9]+)$/i); + if (fileMatch) { + return fileMatch[1]; + } + // Fallback to query param for backwards compatibility + const params = new URLSearchParams(window.location.search); + return params.get('hash'); + }; + + // Update URL when file is selected + const updateUrlForFile = (hashId: string | undefined) => { + const basePath = window.location.pathname.replace(/\/file\/[a-z0-9]+$/i, '').replace(/\/$/, ''); + if (hashId) { + const newPath = `${basePath}/file/${hashId}`; + window.history.pushState({ hashId }, '', newPath); + } else { + window.history.pushState({}, '', basePath || '/editor/'); + } + }; + + // Check for hash parameter in URL on component mount + useEffect(() => { + const loadHashFromUrl = async () => { + const hashParam = getHashFromUrl(); + + if (hashParam) { + try { + setIsLoadingDirectHash(true); + setError(null); + + // Try to get metadata for this hash + const metadata = await MetadataAPI.getMetadata(hashParam); + + // Create a search result from the metadata + const searchResult: SearchResult = { + hashId: hashParam, + metadata: metadata, + score: 1.0, + }; + + // Set the search results and select this file + setSearchResults([searchResult]); + setSelectedHashId(hashParam); + } catch (err: any) { + setError(`Failed to load file with hash ${hashParam}: ${err.message}`); + setSearchResults([]); + setSelectedHashId(undefined); + } finally { + setIsLoadingDirectHash(false); + } + } + }; + + loadHashFromUrl(); + + // Handle browser back/forward + const handlePopState = (event: PopStateEvent) => { + const hashId = event.state?.hashId || getHashFromUrl(); + if (hashId) { + setSelectedHashId(hashId); + } else { + setSelectedHashId(undefined); + } + }; + window.addEventListener('popstate', handlePopState); + return () => window.removeEventListener('popstate', handlePopState); + }, []); // Empty dependency array means this runs once on mount + + const handleSearch = async (query: string) => { + try { + setIsSearching(true); + setError(null); + + // Unified search - searches across all fields + const response = await MetadataAPI.search({ query }); + + setSearchResults(response.results); + + // Auto-select first result if available + if (response.results.length > 0) { + setSelectedHashId(response.results[0].hashId); + } else { + setSelectedHashId(undefined); + } + } catch (err: any) { + setError(err.message); + setSearchResults([]); + setSelectedHashId(undefined); + } finally { + setIsSearching(false); + } + }; + + const handleSelectFile = (hashId: string) => { + if (!bulkEditMode) { + setSelectedHashId(hashId); + setSelectedHashIds(new Set()); + updateUrlForFile(hashId); + } + }; + + const handleToggleSelection = (hashId: string) => { + const newSelection = new Set(selectedHashIds); + if (newSelection.has(hashId)) { + newSelection.delete(hashId); + } else { + newSelection.add(hashId); + } + setSelectedHashIds(newSelection); + }; + + const handleSelectAll = () => { + const allHashIds = new Set(searchResults.map(r => r.hashId)); + setSelectedHashIds(allHashIds); + }; + + const handleClearSelection = () => { + setSelectedHashIds(new Set()); + setBulkEditMode(false); + }; + + const handleBulkEditMode = (enabled: boolean) => { + setBulkEditMode(enabled); + if (enabled) { + setSelectedHashId(undefined); + } else { + setSelectedHashIds(new Set()); + } + }; + + const handleMetadataSaved = () => { + // Optionally refresh the search results to reflect changes + console.log('Metadata saved successfully'); + }; + + const handleBulkMetadataSaved = () => { + console.log('Bulk metadata saved successfully'); + handleClearSelection(); + }; + + return ( +
+
+

MetaMesh Metadata Editor

+

+ Search and edit metadata for your media files +

+
+ +
+ + + {error &&
{error}
} + + {isLoadingDirectHash && ( +
Loading file metadata...
+ )} + + {!isSearching && !isLoadingDirectHash && searchResults.length === 0 && !error && ( +
+

Welcome to MetaMesh Metadata Editor

+

Search for files by title, filename, file path, or hash ID to get started.

+

+ Unified search across all fields | Single file editing | Bulk editing | Direct access via URL +

+
+ )} + + {searchResults.length > 0 && ( +
+
+ +
+
+ {bulkEditMode && selectedHashIds.size > 0 ? ( + + ) : selectedHashId ? ( + + ) : ( +
+ {bulkEditMode + ? 'Select files from the list to bulk edit metadata' + : 'Select a file from the list to edit its metadata'} +
+ )} +
+
+ )} +
+ + +
+ ); +} + +export default App; diff --git a/packages/meta-sort-editor/src/api/metadataApi.ts b/packages/meta-sort-editor/src/api/metadataApi.ts new file mode 100644 index 0000000..deb209f --- /dev/null +++ b/packages/meta-sort-editor/src/api/metadataApi.ts @@ -0,0 +1,151 @@ +import { + FileMetadata, + SearchResponse, + UpdateResult, + BatchUpdateResponse, +} from '../types'; + +const API_BASE = '/api/metadata'; + +export class MetadataAPI { + /** + * Search for files by query, hashId, or property + */ + static async search(params: { + query?: string; + hashId?: string; + property?: string; + propertyValue?: string; + limit?: number; + }): Promise { + const response = await fetch(`${API_BASE}/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Search failed'); + } + + return response.json(); + } + + /** + * Get metadata for a specific file by hash ID + */ + static async getMetadata(hashId: string): Promise { + const response = await fetch(`${API_BASE}/${hashId}`); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to get metadata'); + } + + return response.json(); + } + + /** + * Update complete metadata for a file + */ + static async updateMetadata( + hashId: string, + metadata: FileMetadata + ): Promise { + const response = await fetch(`${API_BASE}/${hashId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(metadata), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to update metadata'); + } + + return response.json(); + } + + /** + * Update a specific property for a file + */ + static async updateProperty( + hashId: string, + property: string, + value: string + ): Promise { + const response = await fetch(`${API_BASE}/${hashId}/property`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ property, value }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to update property'); + } + + return response.json(); + } + + /** + * Batch update multiple files + */ + static async batchUpdate( + updates: Array<{ + hashId: string; + metadata?: FileMetadata; + properties?: Record; + }> + ): Promise { + const response = await fetch(`${API_BASE}/batch`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ updates }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Batch update failed'); + } + + return response.json(); + } + + /** + * Delete metadata for a file + */ + static async deleteMetadata(hashId: string): Promise { + const response = await fetch(`${API_BASE}/${hashId}`, { + method: 'DELETE', + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to delete metadata'); + } + } + + /** + * Get all hash IDs + */ + static async getAllHashIds(): Promise<{ hashIds: string[]; count: number }> { + const response = await fetch(`${API_BASE}/hash-ids`); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to get hash IDs'); + } + + return response.json(); + } +} diff --git a/packages/meta-sort-editor/src/components/BulkMetadataEditor.css b/packages/meta-sort-editor/src/components/BulkMetadataEditor.css new file mode 100644 index 0000000..5a461ff --- /dev/null +++ b/packages/meta-sort-editor/src/components/BulkMetadataEditor.css @@ -0,0 +1,382 @@ +.bulk-metadata-editor { + background-color: rgba(255, 255, 255, 0.05); + border-radius: 8px; + padding: 1.5rem; +} + +.bulk-metadata-editor-header { + margin-bottom: 1rem; + padding-bottom: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.bulk-metadata-editor-header h2 { + margin: 0 0 0.5rem 0; + font-size: 1.5rem; +} + +.bulk-info { + margin: 0 0 1rem 0; + color: rgba(255, 255, 255, 0.6); + font-size: 0.95rem; +} + +.bulk-metadata-editor-actions { + display: flex; + gap: 0.5rem; +} + +.bulk-metadata-editor-actions button.primary { + background-color: #646cff; + color: white; +} + +.bulk-metadata-editor-actions button.primary:hover:not(:disabled) { + background-color: #535bf2; +} + +.bulk-metadata-editor-message { + padding: 1rem; + border-radius: 4px; + margin-bottom: 1rem; +} + +.bulk-metadata-editor-message.success { + background-color: rgba(0, 200, 0, 0.1); + border: 1px solid rgba(0, 200, 0, 0.3); + color: #00ff00; +} + +.bulk-metadata-editor-message.error { + background-color: rgba(255, 0, 0, 0.1); + border: 1px solid rgba(255, 0, 0, 0.3); + color: #ff6b6b; +} + +.bulk-metadata-editor-form { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.bulk-metadata-section { + border: 1px solid rgba(255, 255, 255, 0.1); + border-left: 4px solid #646cff; + border-radius: 8px; + padding: 1.5rem; + background-color: rgba(0, 0, 0, 0.2); +} + +.bulk-metadata-section-header { + margin-bottom: 1.5rem; + padding-bottom: 0.75rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.bulk-metadata-section-header h3 { + margin: 0 0 0.25rem 0; + font-size: 1.1rem; + color: #646cff; +} + +.section-hint { + margin: 0.25rem 0 0 0; + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.5); + font-style: italic; +} + +.section-hint-bulk { + margin: 0.5rem 0 0 0; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.4); + font-weight: 500; +} + +/* Plugin-based bulk fields */ +.bulk-fields-container { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.bulk-field-wrapper { + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 4px; + padding: 0.75rem; + background-color: rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; +} + +.bulk-field-wrapper.has-value { + border-color: rgba(100, 108, 255, 0.3); + background-color: rgba(100, 108, 255, 0.05); +} + +.bulk-field-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.5rem; +} + +.field-key { + font-size: 0.85rem; + font-weight: 600; + color: rgba(255, 255, 255, 0.6); + text-transform: lowercase; + font-family: 'Courier New', monospace; +} + +.bulk-field-wrapper.has-value .field-key { + color: #646cff; +} + +.bulk-field-input { + /* Plugin component will render inside this */ +} + +/* Legacy bulk fields (for compatibility) */ +.bulk-metadata-field { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.bulk-metadata-field:last-child { + margin-bottom: 0; +} + +.field-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.field-header label { + font-size: 0.9rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.7); +} + +.clear-field-btn { + width: 20px; + height: 20px; + padding: 0; + border: 1px solid rgba(255, 0, 0, 0.3); + background-color: rgba(255, 0, 0, 0.1); + color: #ff6b6b; + border-radius: 50%; + cursor: pointer; + font-size: 16px; + line-height: 1; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; +} + +.clear-field-btn:hover { + background-color: rgba(255, 0, 0, 0.2); + border-color: rgba(255, 0, 0, 0.5); +} + +.bulk-metadata-field input, +.bulk-metadata-field textarea, +.bulk-metadata-field select { + width: 100%; +} + +.bulk-metadata-field-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 1rem; +} + +.bulk-metadata-field-row:last-child { + margin-bottom: 0; +} + +/* Modified fields summary */ +.modified-fields { + background-color: rgba(100, 108, 255, 0.05); + border-color: rgba(100, 108, 255, 0.3); +} + +.modified-fields h3 { + color: #646cff; +} + +.modified-fields-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.modified-field-item { + display: flex; + gap: 0.75rem; + align-items: center; + padding: 0.75rem; + background-color: rgba(100, 108, 255, 0.05); + border: 1px solid rgba(100, 108, 255, 0.2); + border-radius: 4px; +} + +.field-name { + font-weight: 600; + color: #646cff; + min-width: 150px; + font-family: 'Courier New', monospace; + font-size: 0.9rem; +} + +.field-value { + flex: 1; + word-break: break-word; + color: rgba(255, 255, 255, 0.8); + font-size: 0.9rem; +} + +.remove-field-btn { + padding: 0.25rem 0.75rem; + font-size: 0.85rem; + background-color: rgba(255, 0, 0, 0.1); + border: 1px solid rgba(255, 0, 0, 0.3); + color: #ff6b6b; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.remove-field-btn:hover { + background-color: rgba(255, 0, 0, 0.2); + border-color: rgba(255, 0, 0, 0.5); +} + +@media (max-width: 768px) { + .bulk-metadata-editor-header { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .bulk-metadata-editor-actions { + width: 100%; + flex-direction: column; + } + + .bulk-metadata-editor-actions button { + width: 100%; + } + + .bulk-metadata-field-row { + grid-template-columns: 1fr; + } + + .modified-field-item { + flex-direction: column; + align-items: flex-start; + } + + .field-name { + min-width: auto; + } + + .remove-field-btn { + align-self: flex-end; + } +} + +@media (prefers-color-scheme: light) { + .bulk-metadata-editor { + background-color: rgba(0, 0, 0, 0.02); + } + + .bulk-metadata-editor-header { + border-bottom-color: rgba(0, 0, 0, 0.1); + } + + .bulk-info { + color: rgba(0, 0, 0, 0.6); + } + + .bulk-metadata-section { + border-color: rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.02); + } + + .bulk-metadata-section-header { + border-bottom-color: rgba(0, 0, 0, 0.1); + } + + .section-hint { + color: rgba(0, 0, 0, 0.5); + } + + .section-hint-bulk { + color: rgba(0, 0, 0, 0.4); + } + + .bulk-field-wrapper { + border-color: rgba(0, 0, 0, 0.1); + background-color: rgba(0, 0, 0, 0.02); + } + + .bulk-field-wrapper.has-value { + border-color: rgba(100, 108, 255, 0.3); + background-color: rgba(100, 108, 255, 0.05); + } + + .field-key { + color: rgba(0, 0, 0, 0.6); + } + + .bulk-field-wrapper.has-value .field-key { + color: #646cff; + } + + .field-header label { + color: rgba(0, 0, 0, 0.7); + } + + .field-value { + color: rgba(0, 0, 0, 0.8); + } + + .bulk-metadata-editor-message.success { + background-color: rgba(0, 200, 0, 0.1); + border-color: rgba(0, 200, 0, 0.3); + color: #008800; + } + + .bulk-metadata-editor-message.error { + background-color: rgba(255, 0, 0, 0.1); + border-color: rgba(255, 0, 0, 0.3); + color: #cc0000; + } + + .modified-fields { + background-color: rgba(100, 108, 255, 0.05); + border-color: rgba(100, 108, 255, 0.3); + } + + .modified-field-item { + background-color: rgba(100, 108, 255, 0.05); + border-color: rgba(100, 108, 255, 0.2); + } + + .clear-field-btn { + border-color: rgba(255, 0, 0, 0.3); + background-color: rgba(255, 0, 0, 0.1); + color: #cc0000; + } + + .clear-field-btn:hover { + background-color: rgba(255, 0, 0, 0.2); + border-color: rgba(255, 0, 0, 0.5); + } +} diff --git a/packages/meta-sort-editor/src/components/BulkMetadataEditor.tsx b/packages/meta-sort-editor/src/components/BulkMetadataEditor.tsx new file mode 100644 index 0000000..f054584 --- /dev/null +++ b/packages/meta-sort-editor/src/components/BulkMetadataEditor.tsx @@ -0,0 +1,280 @@ +import { useState, useEffect } from 'react'; +import { FileMetadata, PluginSchema } from '../types'; +import { MetadataAPI } from '../api/metadataApi'; +import { DynamicFieldRenderer } from './DynamicFieldRenderer'; +import './BulkMetadataEditor.css'; + +interface PluginsResponse { + plugins: PluginSchema[]; +} + +interface BulkMetadataEditorProps { + hashIds: string[]; + onSaved?: () => void; + onCancel?: () => void; +} + +export function BulkMetadataEditor({ hashIds, onSaved, onCancel }: BulkMetadataEditorProps) { + const [plugins, setPlugins] = useState([]); + const [commonValues, setCommonValues] = useState>({}); + const [editedFields, setEditedFields] = useState>({}); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + // Load plugins and metadata on mount + useEffect(() => { + const loadData = async () => { + try { + setIsLoading(true); + setError(null); + + // Fetch plugins + const pluginsResponse = await fetch('/api/plugins'); + if (pluginsResponse.ok) { + const data: PluginsResponse = await pluginsResponse.json(); + // Sort by priority + const sorted = [...data.plugins].sort((a, b) => + (a.priority ?? 100) - (b.priority ?? 100) + ); + setPlugins(sorted); + } + + // Load metadata for all selected files + const metadataPromises = hashIds.map((hashId) => MetadataAPI.getMetadata(hashId)); + const metadata = await Promise.all(metadataPromises); + + // Calculate common values + const commons: Record = {}; + + // Get all editable fields from all plugins + const allFields: string[] = []; + plugins.forEach((plugin) => { + if (plugin.metadataSchema) { + Object.entries(plugin.metadataSchema).forEach(([key, field]) => { + if (!field.readonly) { + allFields.push(key); + } + }); + } + }); + + allFields.forEach((field) => { + const values = metadata.map((m) => m[field as keyof FileMetadata]); + + // Filter out undefined and null values + const definedValues = values.filter((v) => v !== undefined && v !== null); + + if (definedValues.length === 0) { + return; + } + + // Safely stringify values + const uniqueValues = Array.from( + new Set( + definedValues.map((v) => { + try { + return JSON.stringify(v); + } catch { + return String(v); + } + }) + ) + ); + + if (uniqueValues.length === 1) { + try { + commons[field] = JSON.parse(uniqueValues[0]); + } catch { + commons[field] = uniqueValues[0]; + } + } else { + commons[field] = '*'; + } + }); + + setCommonValues(commons); + } catch (err: any) { + setError(`Failed to load data: ${err.message}`); + } finally { + setIsLoading(false); + } + }; + + loadData(); + }, [hashIds]); + + const handleFieldChange = (field: string, value: any) => { + setEditedFields({ + ...editedFields, + [field]: value, + }); + }; + + const handleClearField = (field: string) => { + const newFields = { ...editedFields }; + delete newFields[field]; + setEditedFields(newFields); + }; + + const handleSave = async () => { + if (Object.keys(editedFields).length === 0) { + setError('No fields have been edited'); + return; + } + + try { + setIsSaving(true); + setError(null); + setSuccessMessage(null); + + // Build batch update payload + const updates = hashIds.map((hashId) => ({ + hashId, + metadata: editedFields, + })); + + const result = await MetadataAPI.batchUpdate(updates); + + if (result.errors > 0) { + setError( + `Updated ${result.success} files successfully, but ${result.errors} failed. Check console for details.` + ); + console.error('Batch update errors:', result.results.filter((r) => r.error)); + } else { + setSuccessMessage(`Successfully updated ${result.success} files!`); + } + + if (onSaved) { + onSaved(); + } + + setTimeout(() => setSuccessMessage(null), 3000); + } catch (err: any) { + setError(err.message); + } finally { + setIsSaving(false); + } + }; + + const hasChanges = Object.keys(editedFields).length > 0; + + // Filter to only editable plugins (those with non-readonly fields) + const editablePlugins = plugins.filter((plugin) => { + if (!plugin.metadataSchema) return false; + return Object.values(plugin.metadataSchema).some((field) => !field.readonly); + }); + + if (isLoading) { + return ( +
+
+ Loading metadata for {hashIds.length} files... +
+
+ ); + } + + // Build display metadata (edited values or common values) + const displayMetadata: Record = { ...commonValues, ...editedFields }; + + return ( +
+
+

Bulk Edit Metadata

+

+ Editing {hashIds.length} file{hashIds.length !== 1 ? 's' : ''} - Only modified fields will + be updated. Fields showing "*" have different values across files. +

+
+ {onCancel && ( + + )} + +
+
+ + {successMessage && ( +
{successMessage}
+ )} + {error &&
{error}
} + +
+ {editablePlugins.map((plugin) => { + if (!plugin.metadataSchema) return null; + + // Get editable fields for this plugin + const editableFields = Object.entries(plugin.metadataSchema).filter( + ([, field]) => !field.readonly + ); + + if (editableFields.length === 0) return null; + + return ( +
+
+

{plugin.name}

+ {plugin.description && ( +

{plugin.description}

+ )} +

+ Common values shown. "*" indicates different values. Edit to update all files. +

+
+ +
+ {editableFields.map(([key, field]) => ( + + ))} +
+
+ ); + })} + + {/* Modified Fields Summary */} + {hasChanges && ( +
+

Fields to be Updated ({Object.keys(editedFields).length})

+
+ {Object.entries(editedFields).map(([key, value]) => ( +
+ {key}: + + {Array.isArray(value) + ? value.join(', ') + : typeof value === 'object' + ? JSON.stringify(value) + : String(value || '(empty)')} + + +
+ ))} +
+
+ )} +
+
+ ); +} diff --git a/packages/meta-sort-editor/src/components/DynamicFieldRenderer.css b/packages/meta-sort-editor/src/components/DynamicFieldRenderer.css new file mode 100644 index 0000000..fac5e4f --- /dev/null +++ b/packages/meta-sort-editor/src/components/DynamicFieldRenderer.css @@ -0,0 +1,297 @@ +.dynamic-field { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.dynamic-field:last-child { + margin-bottom: 0; +} + +.dynamic-field label { + font-size: 0.9rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.7); +} + +.dynamic-field input[type="text"], +.dynamic-field input[type="number"], +.dynamic-field select, +.dynamic-field textarea { + width: 100%; + padding: 0.5rem 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + font-size: 0.9rem; + background: rgba(0, 0, 0, 0.3); + color: #fff; + transition: border-color 0.2s; +} + +.dynamic-field input:focus, +.dynamic-field select:focus, +.dynamic-field textarea:focus { + outline: none; + border-color: #646cff; +} + +.dynamic-field input:disabled, +.dynamic-field select:disabled, +.dynamic-field textarea:disabled { + background: rgba(0, 0, 0, 0.2); + color: rgba(255, 255, 255, 0.5); + cursor: not-allowed; +} + +.dynamic-field textarea { + resize: vertical; + min-height: 80px; + font-family: inherit; +} + +.field-hint { + display: block; + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.4); +} + +.checkbox-label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + font-weight: normal; + color: rgba(255, 255, 255, 0.7); +} + +.checkbox-label input[type="checkbox"] { + width: 1.1rem; + height: 1.1rem; + cursor: pointer; + accent-color: #646cff; +} + +.checkbox-label input:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.readonly-value { + padding: 0.5rem 0.75rem; + background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + color: rgba(255, 255, 255, 0.7); + font-family: 'Courier New', monospace; + font-size: 0.9rem; + word-break: break-all; +} + +.field-link { + color: #646cff; + text-decoration: none; +} + +.field-link:hover { + text-decoration: underline; +} + +/* CID field styling */ +.cid-field .cid-value-container { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.cid-field .cid-value { + flex: 1; + padding: 0.5rem 0.75rem; + background: rgba(0, 0, 0, 0.3); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Courier New', monospace; + font-size: 0.85rem; + color: #a0d8ef; + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cid-field .cid-value:hover { + background: rgba(0, 0, 0, 0.4); + border-color: #646cff; +} + +.cid-field .cid-open-button { + display: inline-flex; + align-items: center; + padding: 0.5rem 0.75rem; + background: #646cff; + color: #fff; + border-radius: 4px; + text-decoration: none; + font-size: 0.85rem; + font-weight: 500; + white-space: nowrap; + transition: background 0.2s; +} + +.cid-field .cid-open-button:hover { + background: #535bf2; +} + +/* JSON field styling */ +.json-field .json-viewer { + background: rgba(0, 0, 0, 0.4); + color: #d4d4d4; + padding: 1rem; + border-radius: 4px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Courier New', monospace; + font-size: 0.85rem; + overflow-x: auto; + max-height: 300px; + overflow-y: auto; + margin: 0; + white-space: pre-wrap; + word-break: break-all; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +/* Language string field styling */ +.language-string-field .language-entries { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.language-string-field .language-entry { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.language-string-field .lang-input { + flex: 1; + min-width: 0; +} + +.language-string-field .lang-code { + display: inline-block; + width: 50px; + padding: 0.4rem 0.5rem; + background: rgba(100, 108, 255, 0.2); + border: 1px solid rgba(100, 108, 255, 0.3); + border-radius: 4px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Courier New', monospace; + font-size: 0.8rem; + color: #a0d8ef; + text-align: center; + flex-shrink: 0; +} + +.language-string-field .lang-text { + flex: 1; + padding: 0.4rem 0.75rem; + background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px; + color: rgba(255, 255, 255, 0.8); + font-size: 0.9rem; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.language-string-field .lang-remove-btn { + width: 32px; + height: 32px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 100, 100, 0.2); + border: 1px solid rgba(255, 100, 100, 0.3); + border-radius: 4px; + color: #ff6b6b; + font-size: 1.2rem; + cursor: pointer; + transition: background 0.2s, border-color 0.2s; +} + +.language-string-field .lang-remove-btn:hover { + background: rgba(255, 100, 100, 0.3); + border-color: rgba(255, 100, 100, 0.5); +} + +.language-string-field .lang-add-btn { + align-self: flex-start; + padding: 0.4rem 0.75rem; + background: rgba(100, 108, 255, 0.2); + border: 1px solid rgba(100, 108, 255, 0.3); + border-radius: 4px; + color: #646cff; + font-size: 0.85rem; + cursor: pointer; + transition: background 0.2s, border-color 0.2s; +} + +.language-string-field .lang-add-btn:hover { + background: rgba(100, 108, 255, 0.3); + border-color: rgba(100, 108, 255, 0.5); +} + +/* Light theme */ +@media (prefers-color-scheme: light) { + .dynamic-field label { + color: rgba(0, 0, 0, 0.7); + } + + .dynamic-field input[type="text"], + .dynamic-field input[type="number"], + .dynamic-field select, + .dynamic-field textarea { + border-color: rgba(0, 0, 0, 0.1); + background: #fff; + color: #333; + } + + .dynamic-field input:disabled, + .dynamic-field select:disabled, + .dynamic-field textarea:disabled { + background: #f5f5f5; + color: rgba(0, 0, 0, 0.5); + } + + .field-hint { + color: rgba(0, 0, 0, 0.5); + } + + .checkbox-label { + color: rgba(0, 0, 0, 0.7); + } + + .readonly-value { + background: #f8f9fa; + border-color: #e9ecef; + color: #495057; + } + + .json-field .json-viewer { + background: #1e1e1e; + border-color: rgba(0, 0, 0, 0.2); + } + + .language-string-field .lang-code { + background: rgba(100, 108, 255, 0.1); + color: #4a50c8; + } + + .language-string-field .lang-text { + background: #f8f9fa; + border-color: #e9ecef; + color: #495057; + } +} diff --git a/packages/meta-sort-editor/src/components/DynamicFieldRenderer.tsx b/packages/meta-sort-editor/src/components/DynamicFieldRenderer.tsx new file mode 100644 index 0000000..164226a --- /dev/null +++ b/packages/meta-sort-editor/src/components/DynamicFieldRenderer.tsx @@ -0,0 +1,397 @@ +import { SchemaField, FileMetadata } from '../types'; +import { LanguageCombobox, formatLanguageOption } from './LanguageCombobox'; +import './DynamicFieldRenderer.css'; + +interface DynamicFieldRendererProps { + fieldKey: string; + schema: SchemaField; + metadata: FileMetadata; + onChange: (key: string, value: any) => void; + readonly?: boolean; +} + +/** + * Renders a single metadata field based on its schema definition + */ +export function DynamicFieldRenderer({ + fieldKey, + schema, + metadata, + onChange, + readonly = false, +}: DynamicFieldRendererProps) { + const isReadonly = readonly || schema.readonly; + const fieldType = schema.type || 'string'; + + // Get value from metadata using path-style key + const getValue = (): any => { + if (!metadata) return undefined; + + // First, check if the key exists as a flat key (e.g., "fileinfo/duration") + if (fieldKey in metadata) { + return (metadata as any)[fieldKey]; + } + + // Fall back to nested traversal + const parts = fieldKey.split('/'); + let value: any = metadata; + for (const part of parts) { + if (value === undefined || value === null) return undefined; + value = value[part]; + } + return value; + }; + + // Set value in metadata using path-style key + const setValue = (newValue: any) => { + onChange(fieldKey, newValue); + }; + + const value = getValue(); + + // Render link if configured + const renderValueWithLink = (displayValue: string) => { + if (schema.link && displayValue) { + const url = schema.link.replace('{value}', encodeURIComponent(displayValue)); + return ( + + {displayValue} + + ); + } + return displayValue; + }; + + // Render based on field type + switch (fieldType) { + case 'boolean': + return ( +
+ + {schema.hint && {schema.hint}} +
+ ); + + case 'number': + return ( +
+ + setValue(e.target.value ? Number(e.target.value) : undefined)} + disabled={isReadonly} + min={schema.min} + max={schema.max} + placeholder={schema.placeholder} + /> + {schema.hint && {schema.hint}} +
+ ); + + case 'select': + return ( +
+ + + {schema.hint && {schema.hint}} +
+ ); + + case 'text': + return ( +
+ +