From a6cc5a9827d860428600dd4e8f2a611597ce463c Mon Sep 17 00:00:00 2001 From: sokol Date: Thu, 19 Feb 2026 22:55:26 +0300 Subject: [PATCH] refactor: complete application rewrite with modern UI --- e2e/environment.spec.ts | 104 +- package-lock.json | 1193 +++++++++++++++++++++- package.json | 4 + postcss.config.js | 6 + src/App.css | 75 ++ src/App.tsx | 132 +-- src/components/ui/Badge.tsx | 42 + src/components/ui/Button.tsx | 71 ++ src/components/ui/Card.tsx | 85 ++ src/components/ui/CodeBlock.tsx | 49 + src/components/ui/Input.tsx | 68 ++ src/components/ui/Select.tsx | 55 + src/components/ui/Tabs.tsx | 75 ++ src/components/ui/index.ts | 16 + src/componets/FileChooser.tsx | 135 ++- src/componets/content/ConfigTemplate.tsx | 126 ++- src/componets/content/Content.tsx | 247 +++-- src/componets/env/Environment.tsx | 270 ++--- src/componets/env/EnvironmentParam.tsx | 168 +-- src/index.css | 139 +++ src/models/Config.tsx | 212 ++-- src/models/ConfigReader.tsx | 192 ++-- src/models/Env.ts | 181 +++- src/models/EnvParam.ts | 95 +- src/models/types.ts | 13 + tailwind.config.js | 77 ++ 26 files changed, 3036 insertions(+), 794 deletions(-) create mode 100644 postcss.config.js create mode 100644 src/components/ui/Badge.tsx create mode 100644 src/components/ui/Button.tsx create mode 100644 src/components/ui/Card.tsx create mode 100644 src/components/ui/CodeBlock.tsx create mode 100644 src/components/ui/Input.tsx create mode 100644 src/components/ui/Select.tsx create mode 100644 src/components/ui/Tabs.tsx create mode 100644 src/components/ui/index.ts create mode 100644 src/models/types.ts create mode 100644 tailwind.config.js diff --git a/e2e/environment.spec.ts b/e2e/environment.spec.ts index 99d3c37..f42f2c7 100644 --- a/e2e/environment.spec.ts +++ b/e2e/environment.spec.ts @@ -4,29 +4,32 @@ import * as fs from 'fs'; test.describe('Environment Management', () => { test('should not allow removing DEFAULT environment', async ({ page }) => { await page.goto('/'); - await page.click('button:has-text("Create new")'); - const removeButton = page.locator('button.btn-danger[title="Remove environment"]'); + await page.click('button:has-text("New Config")'); + await page.waitForTimeout(500); + const removeButton = page.locator('button[title="Remove environment"]'); await expect(removeButton).toBeDisabled(); }); test('should remove non-DEFAULT environment', async ({ page }) => { await page.goto('/'); - await page.click('button:has-text("Create new")'); + await page.click('button:has-text("New Config")'); + await page.waitForTimeout(500); page.once('dialog', async dialog => { await dialog.accept('toRemove'); }); - await page.click('button.btn-success[title="Add environment"]'); + await page.click('button[title="Add environment"]'); await page.waitForTimeout(500); await expect(page.locator('#environments option')).toHaveCount(2); page.once('dialog', async dialog => { await dialog.accept(); }); - await page.click('button.btn-danger[title="Remove environment"]'); + await page.click('button[title="Remove environment"]'); await page.waitForTimeout(300); await expect(page.locator('#environments option')).toHaveCount(1); }); test('should create new environment and switch without errors', async ({ page }) => { await page.goto('/'); - await page.click('button:has-text("Create new")'); + await page.click('button:has-text("New Config")'); + await page.waitForTimeout(500); page.once('dialog', async dialog => { await dialog.accept('env1'); }); - await page.click('button.btn-success[title="Add environment"]'); + await page.click('button[title="Add environment"]'); await page.waitForTimeout(500); await expect(page.locator('#environments option')).toHaveCount(2); await page.locator('#environments').selectOption({ index: 0 }); @@ -38,12 +41,13 @@ test.describe('Environment Management', () => { test('should create multiple environments and switch between them', async ({ page }) => { await page.goto('/'); - await page.click('button:has-text("Create new")'); + await page.click('button:has-text("New Config")'); + await page.waitForTimeout(500); page.once('dialog', async dialog => { await dialog.accept('env1'); }); - await page.click('button.btn-success[title="Add environment"]'); + await page.click('button[title="Add environment"]'); await page.waitForTimeout(500); page.once('dialog', async dialog => { await dialog.accept('env2'); }); - await page.click('button.btn-success[title="Add environment"]'); + await page.click('button[title="Add environment"]'); await page.waitForTimeout(500); await expect(page.locator('#environments option')).toHaveCount(3); await page.locator('#environments').selectOption({ index: 0 }); @@ -57,10 +61,11 @@ test.describe('Environment Management', () => { test('should add params and edit template manually', async ({ page }) => { await page.goto('/'); - await page.click('button:has-text("Create new")'); - const nameInput = page.locator('input[placeholder="name"]').first(); - const valueInput = page.locator('input[placeholder="value"]').first(); - const addButton = page.locator('button.btn-success').first(); + await page.click('button:has-text("New Config")'); + await page.waitForTimeout(500); + const nameInput = page.locator('input[placeholder="Parameter name"]').first(); + const valueInput = page.locator('input[placeholder="Parameter value"]').first(); + const addButton = page.locator('button[title="Add parameter"]').first(); await nameInput.fill('host'); await valueInput.fill('localhost:8080'); await addButton.click(); @@ -69,48 +74,49 @@ test.describe('Environment Management', () => { await valueInput.fill('9090'); await addButton.click(); await page.waitForTimeout(500); - await page.click('a:has-text("Content Template")'); + await page.click('button:has-text("Content Template")'); await page.waitForTimeout(500); - await expect(page.locator('button:has-text("Edit")')).toBeVisible(); - await page.click('button:has-text("Edit")'); + await expect(page.locator('button:has-text("Edit Template")')).toBeVisible(); + await page.click('button:has-text("Edit Template")'); await page.waitForTimeout(500); const textarea = page.locator('textarea'); await expect(textarea).toBeVisible(); await textarea.fill('{\n "!!! host": "@host@",\n "!!! port": "@port@",\n "!!! custom": "@custom@"\n}'); await page.waitForTimeout(300); - await page.click('button:has-text("Save")'); + await page.click('button:has-text("Save Changes")'); await page.waitForTimeout(500); - await expect(page.locator('button:has-text("Edit")')).toBeVisible(); + await expect(page.locator('button:has-text("Edit Template")')).toBeVisible(); const pageContent = await page.content(); expect(pageContent).toContain('!!! custom'); }); test('should not duplicate params when placeholder already exists', async ({ page }) => { await page.goto('/'); - await page.click('button:has-text("Create new")'); - const nameInput = page.locator('input[placeholder="name"]').first(); - const valueInput = page.locator('input[placeholder="value"]').first(); - const addButton = page.locator('button.btn-success').first(); + await page.click('button:has-text("New Config")'); + await page.waitForTimeout(500); + const nameInput = page.locator('input[placeholder="Parameter name"]').first(); + const valueInput = page.locator('input[placeholder="Parameter value"]').first(); + const addButton = page.locator('button[title="Add parameter"]').first(); await nameInput.fill('host'); await valueInput.fill('localhost:8080'); await addButton.click(); await page.waitForTimeout(500); - await page.click('a:has-text("Content Template")'); + await page.click('button:has-text("Content Template")'); await page.waitForTimeout(500); - await page.click('button:has-text("Edit")'); + await page.click('button:has-text("Edit Template")'); await page.waitForTimeout(300); const textarea = page.locator('textarea'); await textarea.fill('{\n "!!! host": "@host@",\n "apiUrl": "http://@host@/api"\n}'); await page.waitForTimeout(300); - await page.click('button:has-text("Save")'); + await page.click('button:has-text("Save Changes")'); await page.waitForTimeout(500); - await page.click('a:has-text("Env")'); + await page.click('button:has-text("Env")'); await page.waitForTimeout(300); await nameInput.fill('host'); await valueInput.fill('updated-host:9090'); await addButton.click(); await page.waitForTimeout(500); - await page.click('a:has-text("Content Template")'); + await page.click('button:has-text("Content Template")'); await page.waitForTimeout(500); const templateContent = await page.locator('.config-template-editor').textContent(); const hostKeyCount = (templateContent.match(/!!! host/g) || []).length; @@ -121,49 +127,49 @@ test.describe('Environment Management', () => { test('should validate template with unquoted placeholders', async ({ page }) => { await page.goto('/'); - await page.click('button:has-text("Create new")'); + await page.click('button:has-text("New Config")'); await page.waitForTimeout(500); - - // Add a parameter - await page.click('button:has-text("✚")'); + + // Add a parameter - use first() to get the new parameter inputs + await page.click('button[title="Add parameter"]'); await page.waitForTimeout(300); - const nameInput = page.locator('input[placeholder="name"]'); - const valueInput = page.locator('input[placeholder="value"]'); - const addButton = page.locator('button:has-text("✓")'); + const nameInput = page.locator('input[placeholder="Parameter name"]').last(); + const valueInput = page.locator('input[placeholder="Parameter value"]').last(); + const addButton = page.locator('button[title="Add parameter"]').last(); await nameInput.fill('port'); await valueInput.fill('8080'); await addButton.click(); await page.waitForTimeout(500); - + // Go to Content Template and edit with unquoted placeholder - await page.click('a:has-text("Content Template")'); + await page.click('button:has-text("Content Template")'); await page.waitForTimeout(300); - await page.click('button:has-text("Edit")'); + await page.click('button:has-text("Edit Template")'); await page.waitForTimeout(300); - + // Fill template with unquoted @port@ placeholder const textarea = page.locator('textarea'); await textarea.fill('{\n "Host": "@host@",\n "Port": @port@,\n "Url": "http://@host@:@port@/api"\n}'); await page.waitForTimeout(300); - + // Check that Save button is enabled (validation passed) - const saveButton = page.locator('button:has-text("Save")'); + const saveButton = page.locator('button:has-text("Save Changes")'); await expect(saveButton).toBeEnabled(); - + // Check that there's no JSON error - const errorAlert = page.locator('.alert-danger'); + const errorAlert = page.locator('.bg-red-50'); await expect(errorAlert).not.toBeVisible(); - + // Save the template await saveButton.click(); await page.waitForTimeout(500); - + // Verify it was saved - should be in view mode with Edit button visible - const editButton = page.locator('button:has-text("Edit")'); + const editButton = page.locator('button:has-text("Edit Template")'); await expect(editButton).toBeVisible(); - + // Verify the template content is displayed correctly - const codeContent = page.locator('code'); + const codeContent = page.locator('.hljs'); await expect(codeContent).toBeVisible(); const content = await codeContent.textContent(); expect(content).toContain('@port@'); @@ -171,7 +177,7 @@ test.describe('Environment Management', () => { test('should download config file with correct filename', async ({ page }) => { await page.goto('/'); - await page.click('button:has-text("Create new")'); + await page.click('button:has-text("New Config")'); await page.waitForTimeout(500); const [download] = await Promise.all([ page.waitForEvent('download'), diff --git a/package-lock.json b/package-lock.json index 9ee2cc2..d3f09b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "bootstrap": "^5.3.3", + "lucide-react": "^0.575.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-highlight": "^0.15.0" @@ -20,11 +21,14 @@ "@types/react-dom": "^18.3.5", "@types/react-highlight": "^0.12.8", "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.24", "eslint": "^9.17.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.16", "globals": "^16.5.0", "jsdom": "^27.4.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.19", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4", @@ -38,6 +42,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", @@ -1229,6 +1246,44 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@playwright/test": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", @@ -2160,6 +2215,47 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2177,6 +2273,43 @@ "node": ">=12" } }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2204,6 +2337,19 @@ "require-from-string": "^2.0.2" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bootstrap": { "version": "5.3.8", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", @@ -2234,6 +2380,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -2278,10 +2437,20 @@ "node": ">=6" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "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==", + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", "dev": true, "funding": [ { @@ -2326,6 +2495,44 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2346,6 +2553,16 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2389,6 +2606,19 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cssstyle": { "version": "5.3.6", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.6.tgz", @@ -2468,6 +2698,32 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -2764,6 +3020,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2778,6 +3064,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2809,6 +3105,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2847,6 +3156,20 @@ "dev": true, "license": "ISC" }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2862,6 +3185,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2908,6 +3241,19 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -2995,6 +3341,35 @@ "node": ">=0.8.19" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3018,6 +3393,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3032,6 +3417,18 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3163,6 +3560,300 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3196,6 +3887,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.575.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz", + "integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3213,6 +3913,43 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3233,6 +3970,18 @@ "dev": true, "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -3266,6 +4015,36 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/obug": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", @@ -3373,6 +4152,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3400,6 +4186,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/playwright": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", @@ -3476,6 +4282,140 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3496,6 +4436,27 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/react": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", @@ -3536,6 +4497,42 @@ "node": ">=0.10.0" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3546,6 +4543,27 @@ "node": ">=0.10.0" } }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3556,6 +4574,17 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.54.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", @@ -3598,6 +4627,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -3694,6 +4747,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3707,6 +4783,19 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -3714,6 +4803,77 @@ "dev": true, "license": "MIT" }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -3778,6 +4938,19 @@ "dev": true, "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", @@ -3817,6 +4990,13 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -3909,6 +5089,13 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", diff --git a/package.json b/package.json index 0fe473b..0428c46 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "bootstrap": "^5.3.3", + "lucide-react": "^0.575.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-highlight": "^0.15.0" @@ -24,11 +25,14 @@ "@types/react-dom": "^18.3.5", "@types/react-highlight": "^0.12.8", "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.24", "eslint": "^9.17.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.16", "globals": "^16.5.0", "jsdom": "^27.4.0", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.19", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4", diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/src/App.css b/src/App.css index e69de29..1d65600 100644 --- a/src/App.css +++ b/src/App.css @@ -0,0 +1,75 @@ +/* App-specific styles - most styling is done with Tailwind */ + +/* Smooth scrolling */ +html { + scroll-behavior: smooth; +} + +/* Custom scrollbar for webkit browsers */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f5f9; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +/* Highlight.js override for better dark mode support */ +.hljs { + background: #fafafa !important; + padding: 1rem !important; + border-radius: 0.5rem; +} + +/* Animation utilities */ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideIn { + from { + transform: translateY(-10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.animate-fade-in { + animation: fadeIn 0.3s ease-in-out; +} + +.animate-slide-in { + animation: slideIn 0.3s ease-out; +} + +/* Focus visible for better accessibility */ +:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +/* Transition utilities */ +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; +} diff --git a/src/App.tsx b/src/App.tsx index d9009c1..3fc60e3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,15 @@ -import { useState } from 'react' -import './App.css' -import 'bootstrap/dist/css/bootstrap.css' -import { Env } from './models/Env' -import Environment from "./componets/env" -import Content from './componets/content' -import { FileChooser } from './componets/FileChooser' -import { Config } from "./models/Config" -import logo from './assets/cgg.png' +import { useState } from 'react'; +import { File } from 'lucide-react'; +import { Env } from './models/Env'; +import { Environment } from './componets/env/Environment'; +import { Content } from './componets/content/Content'; +import { FileChooser } from './componets/FileChooser'; +import { Config } from './models/Config'; class AppState { private constructor( public config: Config = new Config(), - ) { } + ) {} static readonly Instance = new AppState(); @@ -23,7 +21,7 @@ class AppState { // Simulate async save with 1 second delay return await new Promise((resolve) => { setTimeout(() => { - console.log("Saved env:", env.name); + console.log('Saved env:', env.name); resolve(0); }, 1000); }); @@ -41,9 +39,9 @@ function App() { async function handleEnvChanged(env: Env) { // Optimistic update - update React state immediately - setEnvs(prevEnvs => { + setEnvs((prevEnvs) => { const newEnvs = [...prevEnvs]; - const idx = newEnvs.findIndex(x => x.id === env.id); + const idx = newEnvs.findIndex((x) => x.id === env.id); if (idx > -1) { newEnvs[idx] = env; } @@ -51,10 +49,9 @@ function App() { }); // Also update config.envs and template to keep them in sync - setConfig(prevConfig => { + setConfig((prevConfig) => { const newConfig = new Config(); - newConfig.envs = prevConfig.envs.map(e => e.id === env.id ? env : e); - // Update template JSON with params from this environment + newConfig.envs = prevConfig.envs.map((e) => (e.id === env.id ? env : e)); newConfig.template = prevConfig.template; newConfig.updateTemplateFromEnv(env); return newConfig; @@ -70,8 +67,8 @@ function App() { function handleEnvAdded(env: Env): number { const newIdx = envs.length; - setEnvs(prevEnvs => [...prevEnvs, env]); - setConfig(prevConfig => { + setEnvs((prevEnvs) => [...prevEnvs, env]); + setConfig((prevConfig) => { const newConfig = new Config(); newConfig.envs = [...prevConfig.envs, env]; newConfig.template = prevConfig.template; @@ -81,63 +78,80 @@ function App() { } function handleEnvRemoved(envId: number) { - setEnvs(prevEnvs => prevEnvs.filter(e => e.id !== envId)); - setConfig(prevConfig => { + setEnvs((prevEnvs) => prevEnvs.filter((e) => e.id !== envId)); + setConfig((prevConfig) => { const newConfig = new Config(); - newConfig.envs = prevConfig.envs.filter(e => e.id !== envId); + newConfig.envs = prevConfig.envs.filter((e) => e.id !== envId); newConfig.template = prevConfig.template; return newConfig; }); } function handleTemplateSaved(newContent: string) { - setConfig(prevConfig => { + setConfig((prevConfig) => { const newConfig = new Config(); newConfig.envs = prevConfig.envs; - newConfig.addTemplate(newContent); + newConfig.setTemplate(newContent); return newConfig; }); } return ( - <> -
-
- { - AppState.Instance.loadConfig(x); - setEnvs(x.envs); - setConfig(x); - }} config={config} /> +
+
+ {/* Header */} +
+ { + AppState.Instance.loadConfig(x); + setEnvs(x.envs); + setConfig(x); + }} + config={config} + />
- {envs.length > 0 ? - (
-
- await handleEnvChanged(e)} - onSelected={handleEnvSelected} - onAdd={handleEnvAdded} - onRemove={handleEnvRemoved} /> -
-
- -
-
) - : - ( -
-
- -
-
- )} + {envs.length > 0 ? ( +
+ {/* Environment Panel */} +
+
+ await handleEnvChanged(e)} + onSelected={handleEnvSelected} + onAdd={handleEnvAdded} + onRemove={handleEnvRemoved} + /> +
+
+ + {/* Content Panel */} +
+ +
+
+ ) : ( + /* Empty State */ +
+
+ +
+

+ No Configuration Loaded +

+

+ Create a new configuration or upload an existing XML file to get started +

+
+ )}
- - ) +
+ ); } -export default App - - - +export default App; diff --git a/src/components/ui/Badge.tsx b/src/components/ui/Badge.tsx new file mode 100644 index 0000000..be99d23 --- /dev/null +++ b/src/components/ui/Badge.tsx @@ -0,0 +1,42 @@ +import { HTMLAttributes, forwardRef } from 'react'; + +export type BadgeVariant = 'default' | 'success' | 'warning' | 'danger' | 'info'; + +interface BadgeProps extends HTMLAttributes { + variant?: BadgeVariant; + size?: 'sm' | 'md'; +} + +const variantStyles: Record = { + default: 'bg-slate-100 text-slate-700', + success: 'bg-green-100 text-green-800', + warning: 'bg-yellow-100 text-yellow-800', + danger: 'bg-red-100 text-red-800', + info: 'bg-blue-100 text-blue-800', +}; + +const sizeStyles: Record<'sm' | 'md', string> = { + sm: 'px-1.5 py-0.5 text-xs', + md: 'px-2 py-1 text-sm', +}; + +export const Badge = forwardRef( + ({ className = '', variant = 'default', size = 'sm', children, ...props }, ref) => { + return ( + + {children} + + ); + } +); + +Badge.displayName = 'Badge'; diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx new file mode 100644 index 0000000..c968482 --- /dev/null +++ b/src/components/ui/Button.tsx @@ -0,0 +1,71 @@ +import { LucideIcon } from 'lucide-react'; +import { ButtonHTMLAttributes, forwardRef } from 'react'; + +export type ButtonVariant = 'primary' | 'success' | 'danger' | 'secondary' | 'ghost'; +export type ButtonSize = 'sm' | 'md' | 'lg'; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: ButtonVariant; + size?: ButtonSize; + icon?: LucideIcon; + iconPosition?: 'left' | 'right'; + isLoading?: boolean; +} + +const variantStyles: Record = { + primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500 shadow-md hover:shadow-lg', + success: 'bg-green-600 text-white hover:bg-green-700 focus:ring-green-500 shadow-md hover:shadow-lg', + danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500 shadow-md hover:shadow-lg', + secondary: 'bg-slate-200 text-slate-700 hover:bg-slate-300 focus:ring-slate-400', + ghost: 'bg-transparent text-slate-600 hover:bg-slate-100 focus:ring-slate-400', +}; + +const sizeStyles: Record = { + sm: 'px-3 py-1.5 text-sm', + md: 'px-4 py-2 text-base', + lg: 'px-6 py-3 text-lg', +}; + +export const Button = forwardRef( + ( + { + className = '', + variant = 'primary', + size = 'md', + icon: Icon, + iconPosition = 'left', + isLoading = false, + disabled, + children, + ...props + }, + ref + ) => { + const baseStyles = ` + inline-flex items-center justify-center gap-2 + font-medium rounded-lg + transition-all duration-200 + focus:outline-none focus:ring-2 focus:ring-offset-2 + disabled:opacity-50 disabled:cursor-not-allowed + ${variantStyles[variant]} + ${sizeStyles[size]} + ${className} + `; + + return ( + + ); + } +); + +Button.displayName = 'Button'; diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx new file mode 100644 index 0000000..9d4b9a3 --- /dev/null +++ b/src/components/ui/Card.tsx @@ -0,0 +1,85 @@ +import { HTMLAttributes, forwardRef } from 'react'; + +interface CardProps extends HTMLAttributes { + variant?: 'default' | 'bordered' | 'elevated'; + padding?: 'none' | 'sm' | 'md' | 'lg'; +} + +const variantStyles: Record = { + default: 'bg-white', + bordered: 'bg-white border-2 border-slate-200', + elevated: 'bg-white shadow-lg', +}; + +const paddingStyles: Record = { + none: '', + sm: 'p-3', + md: 'p-4', + lg: 'p-6', +}; + +export const Card = forwardRef( + ({ className = '', variant = 'default', padding = 'md', children, ...props }, ref) => { + return ( +
+ {children} +
+ ); + } +); + +Card.displayName = 'Card'; + +interface CardHeaderProps extends HTMLAttributes {} + +export const CardHeader = forwardRef( + ({ className = '', children, ...props }, ref) => { + return ( +
+ {children} +
+ ); + } +); + +CardHeader.displayName = 'CardHeader'; + +interface CardBodyProps extends HTMLAttributes {} + +export const CardBody = forwardRef( + ({ className = '', children, ...props }, ref) => { + return ( +
+ {children} +
+ ); + } +); + +CardBody.displayName = 'CardBody'; + +interface CardFooterProps extends HTMLAttributes {} + +export const CardFooter = forwardRef( + ({ className = '', children, ...props }, ref) => { + return ( +
+ {children} +
+ ); + } +); + +CardFooter.displayName = 'CardFooter'; diff --git a/src/components/ui/CodeBlock.tsx b/src/components/ui/CodeBlock.tsx new file mode 100644 index 0000000..558d4ea --- /dev/null +++ b/src/components/ui/CodeBlock.tsx @@ -0,0 +1,49 @@ +import { useState } from 'react'; +import Highlight from 'react-highlight'; +import 'highlight.js/styles/atom-one-light.css'; + +interface CodeBlockProps { + code: string; + language?: 'json' | 'xml' | 'javascript' | 'typescript' | 'css' | 'html' | 'plaintext'; + showLineNumbers?: boolean; + maxHeight?: string; + className?: string; +} + +export function CodeBlock({ + code, + language = 'plaintext', + showLineNumbers = false, + maxHeight = 'none', + className = '', +}: CodeBlockProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + await navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+ +
+ +
+ + {code || '// Empty'} + +
+
+ ); +} diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 0000000..ebf43b2 --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,68 @@ +import { InputHTMLAttributes, forwardRef, useState } from 'react'; + +interface InputProps extends InputHTMLAttributes { + label?: string; + error?: string; + hint?: string; + icon?: React.ReactNode; +} + +export const Input = forwardRef( + ({ label, error, hint, icon, className = '', id, ...props }, ref) => { + const [isFocused, setIsFocused] = useState(false); + + const inputId = id || label?.toLowerCase().replace(/\s+/g, '-'); + + const baseStyles = ` + w-full px-3 py-2 + border rounded-lg + transition-all duration-200 + focus:outline-none focus:ring-2 focus:border-transparent + disabled:bg-slate-100 disabled:cursor-not-allowed + ${error + ? 'border-red-300 focus:ring-red-500 focus:border-red-500' + : 'border-slate-300 focus:ring-blue-500 focus:border-blue-500' + } + ${isFocused ? 'ring-2 ring-blue-500 border-transparent bg-blue-50' : 'bg-white'} + ${icon ? 'pl-10' : ''} + ${className} + `; + + return ( +
+ {label && ( + + )} + +
+ {icon && ( +
+ {icon} +
+ )} + + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + {...props} + /> +
+ + {error && ( +

{error}

+ )} + + {hint && !error && ( +

{hint}

+ )} +
+ ); + } +); + +Input.displayName = 'Input'; diff --git a/src/components/ui/Select.tsx b/src/components/ui/Select.tsx new file mode 100644 index 0000000..a9e918c --- /dev/null +++ b/src/components/ui/Select.tsx @@ -0,0 +1,55 @@ +import { SelectHTMLAttributes, forwardRef } from 'react'; + +interface SelectProps extends SelectHTMLAttributes { + label?: string; + error?: string; + options: { value: string | number; label: string; disabled?: boolean }[]; +} + +export const Select = forwardRef( + ({ label, error, options, className = '', id, ...props }, ref) => { + const selectId = id || label?.toLowerCase().replace(/\s+/g, '-'); + + const baseStyles = ` + w-full px-3 py-2 + border rounded-lg + bg-white cursor-pointer + transition-all duration-200 + focus:outline-none focus:ring-2 focus:border-transparent + disabled:bg-slate-100 disabled:cursor-not-allowed + ${error + ? 'border-red-300 focus:ring-red-500' + : 'border-slate-300 focus:ring-blue-500' + } + ${className} + `; + + return ( +
+ {label && ( + + )} + + + + {error && ( +

{error}

+ )} +
+ ); + } +); + +Select.displayName = 'Select'; diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx new file mode 100644 index 0000000..36a1978 --- /dev/null +++ b/src/components/ui/Tabs.tsx @@ -0,0 +1,75 @@ +import { HTMLAttributes } from 'react'; + +export interface Tab { + id: string; + label: string; + badge?: string | number; + badgeVariant?: 'default' | 'success' | 'warning' | 'danger'; +} + +export interface TabsProps { + tabs: Tab[]; + activeTab: string; + onChange: (tabId: string) => void; + className?: string; +} + +export function Tabs({ tabs, activeTab, onChange, className = '', ...props }: TabsProps) { + return ( +
+ +
+ ); +} + +interface TabPanelProps extends HTMLAttributes { + isActive: boolean; +} + +export function TabPanel({ isActive, children, className = '', ...props }: TabPanelProps) { + if (!isActive) return null; + + return ( +
+ {children} +
+ ); +} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts new file mode 100644 index 0000000..709523b --- /dev/null +++ b/src/components/ui/index.ts @@ -0,0 +1,16 @@ +export { Button } from './Button'; +export type { ButtonVariant, ButtonSize } from './Button'; + +export { Input } from './Input'; + +export { Card, CardHeader, CardBody, CardFooter } from './Card'; + +export { Select } from './Select'; + +export { Badge } from './Badge'; +export type { BadgeVariant } from './Badge'; + +export { Tabs, TabPanel } from './Tabs'; +export type { Tab } from './Tabs'; + +export { CodeBlock } from './CodeBlock'; diff --git a/src/componets/FileChooser.tsx b/src/componets/FileChooser.tsx index b86ca7d..00ed77a 100644 --- a/src/componets/FileChooser.tsx +++ b/src/componets/FileChooser.tsx @@ -1,41 +1,57 @@ -import { Env } from "../models/Env"; -import { ConfigReader } from "../models/ConfigReader"; -import { Config } from "../models/Config"; -import { ConfigBuilder } from "../builders/ConfigBuilder"; +import { useRef } from 'react'; +import { Upload, Download, FilePlus, File } from 'lucide-react'; +import { Button } from '../components/ui'; +import { Config } from '../models/Config'; +import { ConfigReader } from '../models/ConfigReader'; +import { ConfigBuilder } from '../builders/ConfigBuilder'; +import { Env } from '../models/Env'; -export function FileChooser(props: { onSelected: (x: Config) => void, config?: Config }) { - async function handleFile(x: React.ChangeEvent) { - let file = x.target.files![0]; +interface FileChooserProps { + onSelected: (config: Config) => void; + config?: Config; +} - console.log(file.name, file.type, file.size, "supported:", ConfigReader.isSupportedFormat(file)); - let reader = new ConfigReader(); - let cfg = await reader.parseFromFile(file); +export function FileChooser({ onSelected, config }: FileChooserProps) { + const fileInputRef = useRef(null); + + async function handleFileChange(event: React.ChangeEvent) { + const file = event.target.files?.[0]; + if (!file) return; + + console.log(file.name, file.type, file.size, 'supported:', ConfigReader.isSupportedFormat(file)); + + const reader = new ConfigReader(); + const cfg = await reader.parseFromFile(file); if (cfg !== null) { - props.onSelected(cfg); + onSelected(cfg); + } + + // Reset input + if (fileInputRef.current) { + fileInputRef.current.value = ''; } } - function handleNew(){ - let cfg = new Config(); - cfg.addEnvs([new Env(0, "DEFAULT", [])]); - cfg.addTemplate("{}"); - props.onSelected(cfg); + function handleNew() { + const cfg = new Config(); + cfg.setEnvs([new Env(0, 'DEFAULT', [])]); + cfg.setTemplate('{}'); + onSelected(cfg); } function handleDownload() { - if (!props.config) { - alert("No configuration loaded"); + if (!config) { + alert('No configuration loaded'); return; } - const xmlContent = ConfigBuilder.buildFullXml(props.config); + const xmlContent = ConfigBuilder.buildFullXml(config); const filename = ConfigBuilder.generateFilename(); - - // Create blob and download - const blob = new Blob([xmlContent], { type: "text/xml" }); + + const blob = new Blob([xmlContent], { type: 'text/xml' }); const url = URL.createObjectURL(blob); - const link = document.createElement("a"); + const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); @@ -45,25 +61,60 @@ export function FileChooser(props: { onSelected: (x: Config) => void, config?: C } return ( - <> -
- -
-
- -
-
or
+
+
+ {/* Logo/Brand */} +
+
+ +
+ Configucci +
-
- -
- +
+ + {/* Action Buttons */} +
+ + + + + or + + {/* File Upload */} +
+ +
+
+
+
); } diff --git a/src/componets/content/ConfigTemplate.tsx b/src/componets/content/ConfigTemplate.tsx index cb5b102..aed7336 100644 --- a/src/componets/content/ConfigTemplate.tsx +++ b/src/componets/content/ConfigTemplate.tsx @@ -1,29 +1,29 @@ -import { useState, useEffect } from "react"; -import Highlight from 'react-highlight'; -import 'highlight.js/styles/far.css'; -import { Config } from "../../models/Config"; +import { useState, useEffect } from 'react'; +import { Pencil, Save, XCircle, CheckCircle } from 'lucide-react'; +import { Button, CodeBlock, Badge } from '../../components/ui'; +import { Config } from '../../models/Config'; -interface ConfigTemplateProps { +interface ConfigTemplateEditorProps { config: Config; onSaved: (newContent: string) => void; } -export function ConfigTemplate(props: ConfigTemplateProps) { +export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorProps) { const [mode, setMode] = useState<'view' | 'edit'>('view'); - const [draftContent, setDraftContent] = useState(props.config.template.content); - const [originalContent, setOriginalContent] = useState(props.config.template.content); + const [draftContent, setDraftContent] = useState(config.template.content); + const [originalContent, setOriginalContent] = useState(config.template.content); const [jsonError, setJsonError] = useState(null); // Sync draft when config changes (only in view mode) useEffect(() => { if (mode === 'view') { - setDraftContent(props.config.template.content); + setDraftContent(config.template.content); } - }, [props.config.template.content, mode]); + }, [config.template.content, mode]); function handleEdit() { - setOriginalContent(props.config.template.content); - setDraftContent(props.config.template.content); + setOriginalContent(config.template.content); + setDraftContent(config.template.content); setJsonError(null); setMode('edit'); } @@ -40,7 +40,7 @@ export function ConfigTemplate(props: ConfigTemplateProps) { const sanitizedValue = draftContent.replace(/@[^@]+@/g, '1'); JSON.parse(sanitizedValue); setJsonError(null); - props.onSaved(draftContent); + onSaved(draftContent); setMode('view'); } catch (e) { setJsonError((e as Error).message); @@ -52,10 +52,7 @@ export function ConfigTemplate(props: ConfigTemplateProps) { // Validate JSON on every change try { if (value.trim()) { - // Replace @placeholders@ with valid JSON values for validation - // Strategy: Replace ALL @...@ patterns with "1" (valid for both string and numeric contexts) const sanitizedValue = value.replace(/@[^@]+@/g, '1'); - JSON.parse(sanitizedValue); setJsonError(null); } else { @@ -73,11 +70,11 @@ export function ConfigTemplate(props: ConfigTemplateProps) { const start = textarea.selectionStart; const end = textarea.selectionEnd; const value = textarea.value; - + // Insert 2 spaces at cursor position const newValue = value.substring(0, start) + ' ' + value.substring(end); handleDraftChange(newValue); - + // Move cursor after the inserted spaces setTimeout(() => { textarea.selectionStart = textarea.selectionEnd = start + 2; @@ -88,57 +85,86 @@ export function ConfigTemplate(props: ConfigTemplateProps) { const isValidJson = jsonError === null; return ( -
+
{mode === 'view' ? ( - <> -
- +
+
+
+ View Mode +
+
- - {props.config.template.content || "{}"} - - + + +
) : ( - <> -
- - - - {isValidJson ? 'Valid JSON' : 'Invalid JSON'} - + Revert + + + + {isValidJson ? ( + <> + + Valid JSON + + ) : ( + <> + + Invalid JSON + + )} +
+ {jsonError && ( -
- {jsonError} +
+

{jsonError}

)} +