diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..01ab3f8a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig (https://editorconfig.org) - matches the Biome formatter settings +# so editors agree before Biome ever runs. +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +# Markdown hard line breaks are two trailing spaces; do not strip them. +trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52a7849e..ea93a48d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,10 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Run ESLint + - name: Check formatting (Biome) + run: bun run format + + - name: Run linters (oxlint + ESLint) run: bun run lint - name: Run TypeScript type check @@ -48,6 +51,12 @@ jobs: USER_EMAIL: user@libredb.org USER_PASSWORD: test-user + - name: Build library package (tsup) + run: bun run build:lib + + - name: Check package types resolution (attw) + run: bun run attw + test: name: Unit & Integration Tests runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 5de3c51a..01b0b40d 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,7 @@ data/ docs/superpowers/ +# attw packaging scratch (type-resolution check) +.attw/ +*.tgz + diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000..c062ece2 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", + "plugins": ["typescript", "oxc", "react", "react-hooks", "jsx-a11y", "nextjs", "import"], + "categories": { + "correctness": "error", + "suspicious": "error", + "perf": "warn", + "pedantic": "off", + "style": "off" + }, + "rules": { + "react/react-in-jsx-scope": "off", + "react/no-unstable-nested-components": "off", + "import/no-unassigned-import": "off", + "import/no-named-as-default": "off", + "react-hooks/exhaustive-deps": "off", + "no-unused-vars": "off", + "no-underscore-dangle": "off", + "no-shadow": "off", + "no-control-regex": "off", + "jsx-a11y/no-static-element-interactions": "warn", + "jsx-a11y/click-events-have-key-events": "warn", + "jsx-a11y/label-has-associated-control": "warn", + "jsx-a11y/prefer-tag-over-role": "warn", + "jsx-a11y/no-autofocus": "warn", + "jsx-a11y/control-has-associated-label": "warn", + "jsx-a11y/no-noninteractive-element-interactions": "warn", + "jsx-a11y/anchor-has-content": "warn" + }, + "overrides": [ + { + "files": ["tests/**", "**/*.test.ts", "**/*.test.tsx"], + "rules": { + "typescript/no-extraneous-class": "off", + "no-useless-constructor": "off", + "no-new": "off", + "no-constant-binary-expression": "off" + } + } + ], + "ignorePatterns": ["dist/**", ".next/**", "out/**", "build/**", "coverage/**", "node_modules/**", "next-env.d.ts"] +} diff --git a/CLAUDE.md b/CLAUDE.md index 6b4cbea7..ce359c18 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,14 +24,19 @@ Web-based SQL IDE for cloud-native teams: PostgreSQL, MySQL, SQLite, Oracle, SQL bun install # deps (Bun preferred) bun dev # dev server (Turbopack) bun run build # production build -bun run lint # ESLint 9 +bun run format # Biome formatter check (format:fix to write); CSS/JSON excluded +bun run lint # oxlint (fast, syntactic) then ESLint 9 (eslint-config-next + narrow type-aware layer) +bun run lint:oxc # oxlint only bun run typecheck # TypeScript strict bun run test # all layers: unit + api + integration + hooks + components bun run test:e2e # Playwright (requires build) bun run test:coverage # coverage report bun run build:lib # tsup → @libredb/studio package dist (see rule below) +bun run attw # validate published type-resolution against the packed tarball (needs build:lib first) ``` +> **Toolchain rationale (Biome formatter, oxlint, type-aware ESLint layer, attw) lives in [`docs/TOOLCHAIN.md`](docs/TOOLCHAIN.md).** Biome is formatter-only (lineWidth 120); oxlint is the fast syntactic layer in front of ESLint; `eslint-config-next` still owns React/Next/hooks; a narrow `typescript-eslint` type-aware layer guards `src/app/api` + `src/lib/db` against floating promises; attw uses `--profile node16` (the package targets Node >=24 + modern bundlers, so node10 is ignored). + > **`build:lib` after platform-facing changes:** after changing any component used by platform (workspace, providers, …), run `build:lib` — `bun run build` (Next.js) does NOT update the package dist. > **Tests — always `bun run test`, never bare `bun test`.** Component tests need isolated execution groups (`tests/run-components.sh`) to avoid `mock.module()` cross-contamination. @@ -40,7 +45,7 @@ bun run build:lib # tsup → @libredb/studio package dist (see rule below ## Pre-Commit Verification (MANDATORY) -After every code change, run all four locally before claiming done — they match CI (`ci.yml`, `docker-build-push.yml`): `bun run lint` · `bun run typecheck` · `bun run test` · `bun run build`. A local pass on all four guarantees CI passes; do not skip any. +After every code change, run all five locally before claiming done — they match CI (`ci.yml`, `docker-build-push.yml`): `bun run format` · `bun run lint` · `bun run typecheck` · `bun run test` · `bun run build`. A local pass guarantees CI passes; do not skip any. (`bun run lint` runs oxlint then ESLint; the CI `lint-and-build` job additionally runs `build:lib` + `attw`.) ## Architecture diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..48e5d79b --- /dev/null +++ b/biome.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.5.1/schema.json", + "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, + "files": { + "includes": ["src/**", "tests/**", "e2e/**", "scripts/**", "*.ts", "*.mts", "*.mjs"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 120 + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "semicolons": "always" + } + }, + "css": { "formatter": { "enabled": false } }, + "json": { "formatter": { "enabled": false } }, + "linter": { "enabled": false }, + "assist": { "enabled": false } +} diff --git a/bun.lock b/bun.lock index 47d4bb5c..43b53869 100644 --- a/bun.lock +++ b/bun.lock @@ -74,6 +74,8 @@ "zod": "^4.1.12", }, "devDependencies": { + "@arethetypeswrong/cli": "^0.18.4", + "@biomejs/biome": "^2.5", "@playwright/test": "^1.58.2", "@tailwindcss/postcss": "^4", "@testing-library/react": "^16.3.2", @@ -89,9 +91,11 @@ "eslint-config-next": "^16.1.6", "happy-dom": "^20.6.1", "knip": "^6.17.1", + "oxlint": "^1.71", "tailwindcss": "^4", "tsup": "^8.5.1", "typescript": "^6.0.3", + "typescript-eslint": "^8.62", }, "peerDependencies": { "react": "^19", @@ -102,6 +106,12 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@andrewbranch/untar.js": ["@andrewbranch/untar.js@1.0.3", "", {}, "sha512-Jh15/qVmrLGhkKJBdXlK1+9tY4lZruYjsgkDFj08ZmDiWVBLJcqkok7Z0/R0In+i1rScBpJlSvrTS2Lm41Pbnw=="], + + "@arethetypeswrong/cli": ["@arethetypeswrong/cli@0.18.4", "", { "dependencies": { "@arethetypeswrong/core": "0.18.4", "chalk": "^4.1.2", "cli-table3": "^0.6.3", "commander": "^10.0.1", "marked": "^9.1.2", "marked-terminal": "^7.1.0", "semver": "^7.5.4" }, "bin": { "attw": "./dist/index.js" } }, "sha512-kNWo6LTzGAuLYPpJ7Sgo63whSUeeSuKMlYx6IBgzs4ONEG807gW4hSSENvpeCHzO2H2wIzG5EFl0OKBbqGBAyA=="], + + "@arethetypeswrong/core": ["@arethetypeswrong/core@0.18.4", "", { "dependencies": { "@andrewbranch/untar.js": "^1.0.3", "@loaderkit/resolve": "^1.0.2", "cjs-module-lexer": "^1.2.3", "fflate": "^0.8.3", "lru-cache": "^11.0.1", "semver": "^7.5.4", "typescript": "5.6.1-rc", "validate-npm-package-name": "^5.0.0" } }, "sha512-M5F0ePyN6h2Z6XxRiyIPqjGbltotXLjR0CKA0uKspsDu0QmgTNYvRb4RSQPMUs2ZXZHCCYpbaZbFbYOXLxCjUA=="], + "@azure-rest/core-client": ["@azure-rest/core-client@2.5.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A=="], "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], @@ -170,6 +180,28 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@biomejs/biome": ["@biomejs/biome@2.5.1", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.1", "@biomejs/cli-darwin-x64": "2.5.1", "@biomejs/cli-linux-arm64": "2.5.1", "@biomejs/cli-linux-arm64-musl": "2.5.1", "@biomejs/cli-linux-x64": "2.5.1", "@biomejs/cli-linux-x64-musl": "2.5.1", "@biomejs/cli-win32-arm64": "2.5.1", "@biomejs/cli-win32-x64": "2.5.1" }, "bin": { "biome": "bin/biome" } }, "sha512-IXWLCxKmae+rI7LOHS1B3EbVisQ6GRAWbhN9msa6KjNCyFWrvKZWR4oUdinaNssrV852OrSHuSPa95h1GPJc7Q=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-npqDzvqv7vFaWRiNN1Te71siRgPaqS9MpqgYCdP/CrUbkJ7ApezaeaKjueKHRN/JH/6lRjJQAHi8acQDCAz22w=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-RgwTqPAM8g2tn1j+b5oRjF/DbSBX8a4gwojtuG9XuhfK7GgomvZ9+T+tqjXiVbjLEeGJOoL6VEk8mvRTVeSybw=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yhV35CzZh38VyMvTEXi3JTjxZBs++oCKK9KG8vB6VI5+uvQvZNR3BFWEKKzuOmx9DJJj7sQpZ4LQJcmbGTs3+Q=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WMcvMLgByyTqVxGlq918NBBYliq9FRR9GAQVETHb+VjGVqXCZFfHlZHC1FX4ibuYY/Hg6TJE3rHU0xVrdJXNRw=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-J/7uHSX7NfoYDI7HijAkd8lnQIOrRb2W7j3X+tw4R+N5ExvXGsyXFiGdQcfcxfOmNQmZVSQOCDk757fwpzqQcg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ANTowtlLmPYm5yeMckWY8Xzb9Ix+JJP3tgHR/n6xRj1VWyIzzWtfRfih9hv9VmClwadpBvZduISZIbBsIlYG3A=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-zgXnKNgWPC4iPF7Y1lR3STUeCUuZRpD6IiOrC7TZTlh0Lx6FiVUT05myuMQHQ9D+1cc7uyMldi4forE6lp0ivQ=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6uxpR9hvaglANkZemeSiN/FhYgkGasrEGn267eXIWvjrjJ2LhDlk251IhjVJq6MXzkV2/bcXwLwSroLyPtqRZg=="], + + "@braidai/lang": ["@braidai/lang@1.1.2", "", {}, "sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA=="], + + "@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="], + "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], @@ -332,6 +364,8 @@ "@libredb/libredb": ["@libredb/libredb@0.0.3", "", {}, "sha512-bZ4/VW4e1QBdnhlgpiyt1YjW617zhUTdtdDhtYrI0UBnubHv8x8X2i2otKKgbw0bg2I8Izr/xRl5sk8gaErUyw=="], + "@loaderkit/resolve": ["@loaderkit/resolve@1.0.6", "", { "dependencies": { "@braidai/lang": "^1.0.0" } }, "sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg=="], + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], @@ -448,6 +482,44 @@ "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.20.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw=="], + "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.71.0", "", { "os": "android", "cpu": "arm" }, "sha512-ImGmd1njEg4FEJH03jhRnveEegtO3czCtfptvaHivKAZQIYATbVFBrrzbaYMYv0oJioTnxZAZVSyV+oL7W8S2g=="], + + "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.71.0", "", { "os": "android", "cpu": "arm64" }, "sha512-4A5BEexBrwY1YFF8Kiq/lp/wQPRG79G3BWIE1FuWaM5MvmpYSd+7ZySVcKkHdwo0UDzdQGddp6pD9mpctMqLnw=="], + + "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.71.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9wJA9GJulLwS2usU3CEisI/ESDO1n1z9eyTCvApMDrAkbJ1ve0mORgTMjcWWsKxkzkeZ2N/Gpra5IQE7x8tYgQ=="], + + "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.71.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-PlLCjS06V0PeJMAJwzjrExw1sYNW9Gch3JtNlcwwZDXGlTYDuwHNN89zYH8LTXFfgkVtsYvs2nv0FqrzyuFDzg=="], + + "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.71.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Lhil7bWre0ncxbUoDoxfS0JzpTz17BRQKW7iwoAUY8GJ66+WwJEfYPCFJ1P0WgVZR5/O/b3Q2pENlHOjeXLOGQ=="], + + "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.71.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Oo9/L58PYD3RC0x05d2upAPLllHytTjHQGsnC06P6Ynn7jKkp5mdImQxXdJ3+FnBaKspNpGogzgVsi6g872LiA=="], + + "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.71.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mSHfyfgJrEbyIR29ejaeS50BdPk+GoNPlC1dckpDiUZbJAIel68sjSMdOt4WY0/gva+ECC7FNITQkxMJU+vSBw=="], + + "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.71.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-n9yY4M2tiy3aij4AqtlnspzpfdpeT5JQfK2/w2d8oyp5W0FRwOb1dIeX99nORNcxGr08iD9bH8N5XFz3I2iy8w=="], + + "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.71.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJZrs5sDZtTaPIOiemRQQmo82Ezy+vOGXemPc4Ok7iVVsYsFa7SlW6Z5XN819VfsqBHRm3NJ3rTdnR8+bJYJdQ=="], + + "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.71.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cwl7VKGERIy9p+G+AvZdfy/06q0aHXaTt/mMRReC751iuNYJgqKjB7NydXSS30nBT9vtr2tunciOtrR4fD6FUA=="], + + "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.71.0", "", { "os": "linux", "cpu": "none" }, "sha512-eZ8ieVXvzGi8jr7+ybQGPK2STw3mldfxZlgA2738iflfB/rzA69sE6m5rDRpQaxC7dpm745Enlh1Tod0QAk9Gg=="], + + "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.71.0", "", { "os": "linux", "cpu": "none" }, "sha512-puMDbQYe6+NXwfMusojoA7CXGn2b3utukmd23PQqc1E3XhVCwyZ+FueSMzDYeNgDV2dUfIVXAAKZBcFDeCL6sA=="], + + "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.71.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-4NJLxBs1ujISCt3L/1FcywLs73PWtJuw+piD6feK2V6h6OS6P7xu9/sWt1DTRLibe6QCzmfZzmM/2HPORoV/Lg=="], + + "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.71.0", "", { "os": "linux", "cpu": "x64" }, "sha512-cFDaiR8L3430qp88tfZnvFlt3KotFhR/DlbIL0nHOMMYiG/9Wy4l+6f7t8G8pTa9bd8Lt8+M0y/qjRQ/xcB74g=="], + + "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.71.0", "", { "os": "linux", "cpu": "x64" }, "sha512-orfixdt76KlpNly9z0PkWBBNfwjKz+JFVLP/7wnVchlKNU9Dpt9InU/ZggeSej6fC7qwHmHNOGlhLnQXcYoGuA=="], + + "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.71.0", "", { "os": "none", "cpu": "arm64" }, "sha512-9emQu2lAp6yhPB3XuI+++vR+l/o6JR1X+EpxwcumPdQXBWXEPAsquPGL7l158EqU8SebQMXTUa/S5zN98juyHw=="], + + "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.71.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-bd5kI8spYwTm3BILDtGhi73zoup5dw8MlPQNT8YB3BD5UIsjNe3K9/4ctrzQMX4SZMoK5HgzVLkLJzacEXB7fA=="], + + "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.71.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-W4HvOHGzVLHcrmFu+bMrJlho+/yrlX5ZNdJZqGe8MEldkQG+RHYhxxad9P4jvWAYFmIqUA5i9DQ8QsJqSU9GIw=="], + + "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.71.0", "", { "os": "win32", "cpu": "x64" }, "sha512-D2kyEIPHk/G/wiZLnwTVC/sVst+T/lKldVOjAFpgTIBUAOlry72e5OiapDbDBF4LfJLkN5ypJb/8Eu6yJzkveQ=="], + "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], @@ -610,6 +682,8 @@ "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], @@ -720,25 +794,25 @@ "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/type-utils": "8.62.0", "@typescript-eslint/utils": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.62.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o+mpz7EYiMzXoySXiKmzlabIvTVqUuK5yLrAedRPRDA0IpPFMUV1IXt6OqljIxX/kumN6EjUYp41Hqelh6p/Dw=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-dzHeT2gySzZtLDsuqxU9AkYgIsQoHAHtRBpOqM+Ofzx1Bwrd2RcCjQJ+6iQbsHOIR6NS33bF2W1k3blN1zLDrA=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.62.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.62.0", "@typescript-eslint/types": "^8.62.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-wexnCqiTg7BOGtbLDftYpRWlmLq4xfoMd7BKFR6Y75sZS3QmRKLdN3yWLhmIYgqMmP/OXWpj3H8odkb5nGURCQ=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0" } }, "sha512-1lX38kNxXIRb8mEc3lbq5mdHq1Pf2+U0nFU65KfT18mtPxxl0fvjuEE92mHuXPuCtElJhOrddOpyMlM3Z0umEA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.62.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-y2GAdB6ykaXUvuspbYnizQc4oDDz0Tz/Yc7iWrXf9mx8vm/L/0vLHCe0tS2boG96Zy+DivnVDQ9ZUEWoHqqx1g=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/utils": "8.62.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+g5O3j0w2ldzC86Pv6fvbO/xhAonbJFIdf/MKQ1d30gndlsVzUOE83ldfSE15Qrl9fhFjK6AovHs5Wpp6vx86w=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.62.0", "", {}, "sha512-KvAclkktORPvM54TgLgA4z9HIV1M8zOgw9ZVNXl9f/8dLYfXYX1wkMXP7qmabpijQRV5bHJLOmoyGQbLMaUYeg=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.62.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.62.0", "@typescript-eslint/tsconfig-utils": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/visitor-keys": "8.62.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+hVbNxtW64pIcZWDPGbyaKF7vp2IBTVY5ma1blwwksrjdsbdqqEKvJWMGbBofei4F6Dovx1M0RJgoFeNu2279A=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.62.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.62.0", "@typescript-eslint/types": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-82r66fi9zYwZ+mTq3vKgwjbZ1PVk/DJzrXFLpG6RnBbdvH8TEGVHIs9H4d2drhkOzf0syZuD/OZvvlu6GDbP4g=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.62.0", "", { "dependencies": { "@typescript-eslint/types": "8.62.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-CY3uyFSRbcQv3nnSv8S0+lDftMVz6P963PoRlxrV7ew/Md564g9ut60PYzdLM5qW4jFn93GBF+Soi90ISAN+GQ=="], "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="], @@ -794,7 +868,9 @@ "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -886,16 +962,26 @@ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "char-regex": ["char-regex@1.0.2", "", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "classcat": ["classcat@5.0.5", "", {}, "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w=="], + "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], + + "cli-table3": ["cli-table3@0.6.5", "", { "dependencies": { "string-width": "^4.2.0" }, "optionalDependencies": { "@colors/colors": "1.5.0" } }, "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ=="], + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], @@ -906,7 +992,7 @@ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "commander": ["commander@10.0.1", "", {}, "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], @@ -1022,12 +1108,16 @@ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "emojilib": ["emojilib@2.4.0", "", {}, "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -1106,6 +1196,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], @@ -1142,6 +1234,8 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], @@ -1184,6 +1278,8 @@ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], + "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], @@ -1236,6 +1332,8 @@ "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], @@ -1376,7 +1474,7 @@ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], - "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "lru-cache": ["lru-cache@11.5.1", "", {}, "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A=="], "lru.min": ["lru.min@1.1.3", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="], @@ -1386,7 +1484,9 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + "marked": ["marked@9.1.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q=="], + + "marked-terminal": ["marked-terminal@7.3.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "ansi-regex": "^6.1.0", "chalk": "^5.4.1", "cli-highlight": "^2.1.11", "cli-table3": "^0.6.5", "node-emoji": "^2.2.0", "supports-hyperlinks": "^3.1.0" }, "peerDependencies": { "marked": ">=1 <16" } }, "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -1448,6 +1548,8 @@ "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], + "node-emoji": ["node-emoji@2.2.0", "", { "dependencies": { "@sindresorhus/is": "^4.6.0", "char-regex": "^1.0.2", "emojilib": "^2.4.0", "skin-tone": "^2.0.0" } }, "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw=="], + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "oauth4webapi": ["oauth4webapi@3.8.5", "", {}, "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg=="], @@ -1484,12 +1586,18 @@ "oxc-resolver": ["oxc-resolver@11.20.0", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.20.0", "@oxc-resolver/binding-android-arm64": "11.20.0", "@oxc-resolver/binding-darwin-arm64": "11.20.0", "@oxc-resolver/binding-darwin-x64": "11.20.0", "@oxc-resolver/binding-freebsd-x64": "11.20.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.20.0", "@oxc-resolver/binding-linux-arm-musleabihf": "11.20.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.20.0", "@oxc-resolver/binding-linux-arm64-musl": "11.20.0", "@oxc-resolver/binding-linux-ppc64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-musl": "11.20.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-musl": "11.20.0", "@oxc-resolver/binding-openharmony-arm64": "11.20.0", "@oxc-resolver/binding-wasm32-wasi": "11.20.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.20.0", "@oxc-resolver/binding-win32-x64-msvc": "11.20.0" } }, "sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g=="], + "oxlint": ["oxlint@1.71.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.71.0", "@oxlint/binding-android-arm64": "1.71.0", "@oxlint/binding-darwin-arm64": "1.71.0", "@oxlint/binding-darwin-x64": "1.71.0", "@oxlint/binding-freebsd-x64": "1.71.0", "@oxlint/binding-linux-arm-gnueabihf": "1.71.0", "@oxlint/binding-linux-arm-musleabihf": "1.71.0", "@oxlint/binding-linux-arm64-gnu": "1.71.0", "@oxlint/binding-linux-arm64-musl": "1.71.0", "@oxlint/binding-linux-ppc64-gnu": "1.71.0", "@oxlint/binding-linux-riscv64-gnu": "1.71.0", "@oxlint/binding-linux-riscv64-musl": "1.71.0", "@oxlint/binding-linux-s390x-gnu": "1.71.0", "@oxlint/binding-linux-x64-gnu": "1.71.0", "@oxlint/binding-linux-x64-musl": "1.71.0", "@oxlint/binding-openharmony-arm64": "1.71.0", "@oxlint/binding-win32-arm64-msvc": "1.71.0", "@oxlint/binding-win32-ia32-msvc": "1.71.0", "@oxlint/binding-win32-x64-msvc": "1.71.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.22.1", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-U1m1X+C0vDj7DC1e13IoZULzEcPczE7UOMTs8VlZGHUEIUaSTZKo5qkPsQEfzpgnQ29Pea/w3Xntk62UCecxZw=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@6.0.1", "", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -1600,6 +1708,8 @@ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], @@ -1628,7 +1738,7 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], @@ -1656,6 +1766,8 @@ "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "skin-tone": ["skin-tone@2.0.0", "", { "dependencies": { "unicode-emoji-modifier-base": "^1.0.0" } }, "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA=="], + "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], @@ -1684,6 +1796,8 @@ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], @@ -1698,6 +1812,8 @@ "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], @@ -1708,6 +1824,8 @@ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], @@ -1742,7 +1860,7 @@ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], @@ -1768,7 +1886,7 @@ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], + "typescript-eslint": ["typescript-eslint@8.62.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.62.0", "@typescript-eslint/parser": "8.62.0", "@typescript-eslint/typescript-estree": "8.62.0", "@typescript-eslint/utils": "8.62.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8QxXi+ZACKX0kaqO4gY8kn0RSD9gFfaHDWwjqtEN48aWCBkX4MJaufWN+c3BzlrXLOxfywDL8CaoqUwcRq4j4Q=="], "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], @@ -1778,6 +1896,8 @@ "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + "unicode-emoji-modifier-base": ["unicode-emoji-modifier-base@1.0.0", "", {}, "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g=="], + "unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -1796,6 +1916,8 @@ "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "validate-npm-package-name": ["validate-npm-package-name@5.0.1", "", {}, "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ=="], + "vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="], "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], @@ -1820,6 +1942,8 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], @@ -1828,10 +1952,16 @@ "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "yargs": ["yargs@16.2.2", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w=="], + + "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], @@ -1840,8 +1970,16 @@ "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], + "@arethetypeswrong/core/typescript": ["typescript@5.6.1-rc", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ=="], + "@babel/core/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -1906,10 +2044,6 @@ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - "@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], @@ -1920,6 +2054,8 @@ "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "eslint-config-next/typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="], + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-import-resolver-typescript/get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], @@ -1930,33 +2066,41 @@ "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "eslint-plugin-jsx-a11y/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="], + "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "happy-dom/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - "is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "jsonwebtoken/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - "knip/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "knip/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + "marked-terminal/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "mlly/acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "monaco-editor/marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + + "mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "nearley/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - "node-abi/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], + + "pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -1966,7 +2110,9 @@ "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], @@ -1992,8 +2138,6 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], - "@typescript-eslint/typescript-estree/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - "@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], @@ -2004,6 +2148,14 @@ "bun-types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "eslint-config-next/typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="], + "eslint-import-resolver-typescript/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -2019,5 +2171,65 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/eslint-plugin/ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "eslint-config-next/typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], } } diff --git a/docs/TOOLCHAIN.md b/docs/TOOLCHAIN.md new file mode 100644 index 00000000..585d28a4 --- /dev/null +++ b/docs/TOOLCHAIN.md @@ -0,0 +1,220 @@ +# LibreDB Studio Toolchain - 2026 Adoption Record + +> Status: IMPLEMENTED (PR #98, phased; each phase green through CI before the next). A per-tool adoption +> record for five tools, ported from the researched-then-adversarially-verified decision record in +> `libredb-database/docs/TOOLCHAIN.md` and adapted to Studio's reality: a Next.js 16 + React 19 + TSX +> application that ALSO ships as the dual-format npm package `@libredb/studio` (consumed by +> `libredb-platform`). The database record is the rationale source of truth; this document records only what +> changes for Studio and why. Deviations surfaced during implementation are marked "as implemented". + +## Scope + +Five tools, deliberately a subset of the database gate (no size-limit, commitlint, changesets, secretlint, +license, etc.): + +| Tool | Decision | Reason for Studio | +| --- | --- | --- | +| `@biomejs/biome` (format-only) | ADOPT | No formatter today - the one unambiguous gap. Same as database. | +| `oxlint` | ADOPT | Fast Rust syntactic linter; a sub-second fail-fast layer in front of ESLint. | +| `typescript-eslint` + `eslint` | KEEP (Strategy A) | `eslint-config-next` stays as-is and keeps owning React/Next/hooks rules; oxlint is layered on top. | +| `knip` | KEEP | Already wired into the CI gate. Verify, do not rebuild. | +| `@arethetypeswrong/cli` (attw) | ADOPT | Higher value here than in database: 5 subpath exports x dual CJS+ESM x both `.d.ts` and `.d.mts`. | + +## How Studio differs from database (and why the configs change) + +| Dimension | libredb-database | libredb-studio | +| --- | --- | --- | +| Type | Pure ESM TS library, synchronous core, ZERO runtime deps | Next.js 16 + React 19 + TSX (256 ts/tsx, 121 tsx), async-heavy (API routes, DB drivers) | +| Build | `tsc` + isolatedDeclarations, single entry | `tsup`, dual ESM+CJS, 5 subpath exports (`.`, `/providers`, `/types`, `/components`, `/workspace`) | +| Linting today | oxlint + type-aware-only ESLint | `eslint-config-next` (core-web-vitals + typescript + react-hooks) | +| Formatter today | Biome (present) | None (no prettier) | +| knip | present | present (in CI gate) | +| Tests | single `bun test` | process-isolated (`run-core.sh` / `run-components.sh`) to avoid `mock.module()` cross-contamination | + +Consequences: + +- **attw uses the DEFAULT profile, NOT `--profile esm-only`** - the package is intentionally dual CJS+ESM, + so attw must verify CJS resolution too. +- **ESLint is NOT reduced to type-aware-only** (the database move). `eslint-config-next` is the canonical + Next linter and Studio ships as a Next app; reducing it would drop curated Next/React coverage. +- **CSS is excluded from the Biome formatter** - the platform-integration rules + (`.claude/rules/platform-integration.md`) warn that `globals.css` can break silently when embedded in + platform. Keep CSS out of Biome's scope as a safe start. +- **attw needs `build:lib` (tsup), not `next build`** - do not mix the two in CI. + +## Why lineWidth = 120 (carried over from database) + +Not the Biome/Prettier default of 80. The 80 default is terminal/prose-era inertia; code is scanned, not +read like prose. Reformatting the database repo from 80 to 120 was a net -245 lines because width-80 +over-wrapped signatures and calls that fit cleanly on one line at 120. 120 is the JetBrains default and the +modern wide-but-still-review-friendly choice (140 strains side-by-side review). Biome's JS formatter is +configured: 2-space indent, double quotes, semicolons always. + +## Why Biome is formatter-only + +Biome's type-aware lint rules use a re-implemented inference engine its own authors say "cannot guarantee +full coverage or alignment with TS." Linting stays with oxlint (syntactic) + ESLint (`eslint-config-next`, +including the type-aware Next rules). Biome's `linter` and `assist` are disabled. + +## Phase 0 - Prep (shared) + +- Add `.editorconfig` (identical to database: 2-space, LF, UTF-8, final newline, trim trailing whitespace; + `*.md` exempted from trim since hard breaks use trailing spaces) so editors agree before Biome runs. +- Branch `feat/toolchain` off `main` (trunk-based). + +## Phase 1 - Biome (formatter only) + +Lowest-risk, path-clearing step. One-shot full-repo reformat. + +`biome.json`: + +```jsonc +{ + "$schema": "https://biomejs.dev/schemas/2.5.1/schema.json", + "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 120 }, + "javascript": { "formatter": { "quoteStyle": "double", "semicolons": "always" } }, + "css": { "formatter": { "enabled": false } }, + "linter": { "enabled": false }, + "assist": { "enabled": false } +} +``` + +Scripts: + +```jsonc +"format": "biome format src tests *.ts *.mjs", +"format:fix": "biome format --write src tests *.ts *.mjs" +``` + +Notes: + +- Style is double-quote + semicolons: consistent with database and with the existing `eslint.config.mjs`. + The repo is inconsistent today (`tsup.config.ts` is single-quote / no-semi); the reformat unifies it. +- `css.formatter.enabled: false` keeps `globals.css` and other CSS untouched (platform-integration risk). +- Deliverable: a single `chore(format): adopt Biome formatter` PR (~256 files). Afterwards run `build:lib` + and verify BOTH modes (standalone + embedded), per the repo's UI-change rule. Coordinate timing to avoid + clashing with open PRs. + +## Phase 2 - Oxlint + +Sub-second syntactic linter; a fail-fast layer in front of ESLint. + +`.oxlintrc.json` (plugins + categories; the rule tuning below is the as-implemented set): + +```jsonc +{ + "plugins": ["typescript", "oxc", "react", "react-hooks", "jsx-a11y", "nextjs", "import"], + "categories": { "correctness": "error", "suspicious": "error", "perf": "warn", "pedantic": "off", "style": "off" } +} +``` + +As implemented (the first run surfaced ~1300 findings; the breakdown drove these decisions): + +- Disabled as false-positives / eslint-config-next-owned duplicates / idioms: + - `react/react-in-jsx-scope` (936) - the project uses the automatic JSX runtime (`jsx: react-jsx`), so React + need not be in scope. This is correct, not a workaround; eslint-config-next disables it too. + - `import/no-unassigned-import` (202) - intentional side-effect imports (setup/registration). + - `no-underscore-dangle` (64) - the `_`-prefix is the codebase's deliberate intentionally-unused marker. + - `no-unused-vars` (25) and `react-hooks/exhaustive-deps` (2) - eslint-config-next already owns these as + warnings; disabling in oxlint avoids duplicate/contradictory reporting. + - `no-shadow` (17) - same call as database; shadcn/ui vendored components shadow idiomatically. + - `no-control-regex` (4) - intentional control-char matching in `logger.ts` log-injection sanitization. + - `react/no-unstable-nested-components` - the shadcn calendar + TanStack cell-renderer idiom. + - `import/no-named-as-default` - the monaco default+named export. +- Scoped to tests via `overrides` (test idioms): `typescript/no-extraneous-class`, `no-useless-constructor`, + `no-new` (constructor-throws assertions), `no-constant-binary-expression` (intentional falsy-class test data). +- `jsx-a11y` rules that fired (8) are downgraded to `warn`, not disabled: accessibility matters, but fixing + ~60 a11y findings (many in vendored shadcn/ui, several needing markup/behaviour changes) belongs in a + dedicated accessibility pass, not a tooling-adoption PR. They remain a visible, non-blocking backlog signal. +- Three genuine bugs oxlint surfaced were FIXED, not silenced (see the Phase 2 commit): a dead + `typeof ... || "unknown"` fallback in `profile/route.ts`, a dropped error cause in `seed/config-loader.ts`, + and a useless regex escape in `merge-lcov.mjs`. +- The `unicorn` plugin is NOT added (taste noise, same call as database). NOTE: an unknown rule key is a HARD + error in oxlint (it exits 1), not a warning - `react/jsx-uses-react` does not exist and had to be removed; + only `react/react-in-jsx-scope` is needed for the automatic runtime. +- Scripts: `"lint:oxc": "oxlint"`, and `lint` runs oxlint first: `"lint": "oxlint && eslint ."`. + +## Phase 3 - typescript-eslint + ESLint (Strategy A: keep Next, layer oxlint) + +`eslint-config-next` stays exactly as it is in `eslint.config.mjs` (it owns core-web-vitals, the typescript +config, and the react-hooks rules). Oxlint is layered on top for fast syntactic feedback; ESLint remains the +curated Next/React safety net. + +As implemented, the narrow type-aware layer WAS added (it earned its place): a `typescript-eslint` flat-config +block scoped to the async-heavy code (`src/app/api/**`, `src/lib/db/**`) via `parserOptions.projectService`, +enabling `@typescript-eslint/no-floating-promises`, `no-misused-promises`, `await-thenable` as errors. It +immediately caught five genuine fire-and-forget bugs (async functions invoked in setInterval/setTimeout/process +signal handlers without handling the promise, in `factory.ts`, `mysql.ts`, `postgres.ts`), fixed with the +`void` operator. Scoping keeps lint fast; eslint-config-next still owns everything else. + +Rejected for Studio: the database-style reduction of ESLint to type-aware-only with React/Next rules moved +to oxlint. For a shipping Next app the risk of losing `eslint-config-next`'s curated coverage outweighs the +single-linter simplicity. + +## Phase 4 - attw (@arethetypeswrong/cli) + +High value here: the package has 5 subpath exports, dual CJS+ESM, and emits both `.d.ts` and `.d.mts` - the +exact surface where types-resolution and CJS/ESM-masquerading bugs hide. + +```jsonc +// scripts +"attw": "rm -rf .attw && bun pm pack --quiet --destination .attw && attw .attw/*.tgz --profile node16", +"prepublishOnly": "tsup && bun run attw" +``` + +Notes: + +- `--profile node16` (as implemented, a deviation from the planned default profile). The first run was green + for the main `.` entry under all modes, but the four subpath exports (`/providers`, `/types`, `/components`, + `/workspace`) failed ONLY on the legacy `node10` resolution algorithm (node16 CJS+ESM and bundler were all + green). node10 cannot resolve subpath exports without redirect stubs, and the package requires Node >=24 and + is consumed by modern bundlers (Next.js/platform), so supporting node10 is moot. `--profile node16` scopes + the check to node16 CJS+ESM (the real consumer scenarios) and is more honest and precise than the broad + `--ignore-rules no-resolution`, which could mask a real node16 failure. +- `rm -rf .attw` runs FIRST (not trailing): a trailing `&& rm` would mask attw's exit code, and pre-cleaning + drops a stale tarball from a previous version bump. +- attw needs `dist/` from `build:lib` (tsup), so `prepublishOnly` runs `tsup` before `attw`. In CI use + `build:lib`, never `next build`, before attw. +- Git-ignore `.attw/` and `*.tgz` (packaging scratch). +- CI: the `lint-and-build` job runs `build:lib` then `attw` (plus a Biome format check) so the package + surface is gated on every PR. + +## Phase 5 - knip (keep, verify) + +Each new tool (`biome`, `oxlint`, `attw`, `typescript-eslint`) gets a real package.json script or a config +import, so knip resolves them and counts them as used. As implemented, `bun run knip` was green with NO +`knip.json` change needed (database's finding held: scripts suffice, even for `attw` whose binary name differs +from `@arethetypeswrong/cli`, and `typescript-eslint` is seen via the `eslint.config.mjs` import). + +## CI and pre-commit integration + +As implemented in `.github/workflows/ci.yml`, the "Lint, Typecheck and Build" job runs, in order: Biome format +check (`bun run format`), linters (`bun run lint`, i.e. oxlint then ESLint), typecheck, knip, `next build`, +`build:lib`, then `attw`. oxlint is folded into `bun run lint` rather than a separate step. The pre-commit git +hook (`.claude/settings.json`) runs `lint && typecheck && test && build` and now transitively enforces oxlint +and the type-aware layer via `bun run lint`. + +## Rollout order and per-phase gate + +1. Biome formatter + `.editorconfig` (one-shot reformat PR). +2. Oxlint (tune rules to green). +3. ESLint Strategy A wiring + optional type-aware layer. +4. attw + `.gitignore` + `prepublishOnly` + CI packaging step. +5. knip verification. + +Each phase must end green on the repo's checks - `bun run lint`, `bun run typecheck`, `bun run test`, +`bun run build` - PLUS `bun run build:lib` and a both-modes (standalone + embedded) verification for any +phase that can affect output. + +## Studio-specific risks + +1. Big-bang reformat diff churn - coordinate with open PRs / platform; one PR; verify both modes. +2. platform-integration rules - keep CSS out of Biome; verify the embedded mode after the reformat. +3. Oxlint React noise on the first run - expect minor rule tuning. +4. attw must use `build:lib`, not `next build`. +5. `mock.module()` test isolation is unaffected by these static tools. + +## Suggested package versions + +`@biomejs/biome@^2.5`, `oxlint@^1.71`, `@arethetypeswrong/cli@^0.18.4`. `eslint` / `eslint-config-next` / +`typescript-eslint` / `knip` stay at their current Studio versions. diff --git a/e2e/admin-dashboard.spec.ts b/e2e/admin-dashboard.spec.ts index 0e848a64..1cdcd961 100644 --- a/e2e/admin-dashboard.spec.ts +++ b/e2e/admin-dashboard.spec.ts @@ -1,67 +1,67 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; -test.describe('Admin Dashboard', () => { +test.describe("Admin Dashboard", () => { test.beforeEach(async ({ page }) => { // Login as admin - await page.goto('/login'); - await page.locator('input[type="email"]').fill('admin@libredb.org'); - await page.locator('input[type="password"]').fill('test-admin'); - await page.getByRole('button', { name: /sign in/i }).click(); - await page.waitForURL('**/admin**'); + await page.goto("/login"); + await page.locator('input[type="email"]').fill("admin@libredb.org"); + await page.locator('input[type="password"]').fill("test-admin"); + await page.getByRole("button", { name: /sign in/i }).click(); + await page.waitForURL("**/admin**"); }); - test('admin dashboard loads', async ({ page }) => { - await expect(page.locator('text=Admin Dashboard')).toBeVisible({ timeout: 10000 }); + test("admin dashboard loads", async ({ page }) => { + await expect(page.locator("text=Admin Dashboard")).toBeVisible({ timeout: 10000 }); }); - test('shows 5 tab triggers', async ({ page }) => { - await expect(page.getByRole('tab', { name: /Overview/i })).toBeVisible({ timeout: 10000 }); - await expect(page.getByRole('tab', { name: /Operations/i })).toBeVisible(); - await expect(page.getByRole('tab', { name: /Monitoring/i })).toBeVisible(); - await expect(page.getByRole('tab', { name: /Security/i })).toBeVisible(); - await expect(page.getByRole('tab', { name: /Audit/i })).toBeVisible(); + test("shows 5 tab triggers", async ({ page }) => { + await expect(page.getByRole("tab", { name: /Overview/i })).toBeVisible({ timeout: 10000 }); + await expect(page.getByRole("tab", { name: /Operations/i })).toBeVisible(); + await expect(page.getByRole("tab", { name: /Monitoring/i })).toBeVisible(); + await expect(page.getByRole("tab", { name: /Security/i })).toBeVisible(); + await expect(page.getByRole("tab", { name: /Audit/i })).toBeVisible(); }); - test('default tab is overview', async ({ page }) => { + test("default tab is overview", async ({ page }) => { // Overview content is mounted by default — assert on the content region, not // empty-state copy, so the test holds whether or not seed connections exist. - await expect(page.getByTestId('admin-content-overview')).toBeVisible({ timeout: 10000 }); - await expect(page.getByRole('tab', { name: /Overview/i })).toHaveAttribute('aria-selected', 'true'); + await expect(page.getByTestId("admin-content-overview")).toBeVisible({ timeout: 10000 }); + await expect(page.getByRole("tab", { name: /Overview/i })).toHaveAttribute("aria-selected", "true"); }); - test('can switch to operations tab', async ({ page }) => { + test("can switch to operations tab", async ({ page }) => { await page.locator('button:has-text("Operations"), [role="tab"]:has-text("Operations")').first().click(); // Operations content region mounts regardless of connection state (empty // state or populated dashboard), so this is stable across environments. - await expect(page.getByTestId('admin-content-operations')).toBeVisible({ timeout: 5000 }); - await expect(page.getByRole('tab', { name: /Operations/i })).toHaveAttribute('aria-selected', 'true'); + await expect(page.getByTestId("admin-content-operations")).toBeVisible({ timeout: 5000 }); + await expect(page.getByRole("tab", { name: /Operations/i })).toHaveAttribute("aria-selected", "true"); }); - test('can switch to security tab', async ({ page }) => { + test("can switch to security tab", async ({ page }) => { await page.locator('button:has-text("Security"), [role="tab"]:has-text("Security")').first().click(); await page.waitForTimeout(500); // Security tab should show Data Masking content - await expect(page.locator('text=Data Masking').first()).toBeVisible({ timeout: 5000 }); + await expect(page.locator("text=Data Masking").first()).toBeVisible({ timeout: 5000 }); }); - test('can switch to audit tab', async ({ page }) => { + test("can switch to audit tab", async ({ page }) => { await page.locator('button:has-text("Audit"), [role="tab"]:has-text("Audit")').first().click(); await page.waitForTimeout(500); // Audit tab should show operations/queries - await expect(page.locator('text=Operations').first()).toBeVisible({ timeout: 5000 }); + await expect(page.locator("text=Operations").first()).toBeVisible({ timeout: 5000 }); }); - test('editor button navigates to studio', async ({ page }) => { + test("editor button navigates to studio", async ({ page }) => { const editorBtn = page.locator('button:has-text("Editor"), a:has-text("Editor")').first(); await editorBtn.click(); - await page.waitForURL('/'); - await expect(page).toHaveURL('/'); + await page.waitForURL("/"); + await expect(page).toHaveURL("/"); }); - test('logout button redirects to login', async ({ page }) => { + test("logout button redirects to login", async ({ page }) => { const logoutBtn = page.locator('button:has-text("Logout")').first(); await logoutBtn.click(); - await page.waitForURL('**/login**'); + await page.waitForURL("**/login**"); await expect(page).toHaveURL(/\/login/); }); }); diff --git a/e2e/connection-management.spec.ts b/e2e/connection-management.spec.ts index ec56031c..30cb74c4 100644 --- a/e2e/connection-management.spec.ts +++ b/e2e/connection-management.spec.ts @@ -1,41 +1,41 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; -test.describe('Connection Management', () => { +test.describe("Connection Management", () => { test.beforeEach(async ({ page }) => { // Login as user (simpler redirect, avoids admin → studio navigation issues) - await page.goto('/login'); - await page.locator('input[type="email"]').fill('user@libredb.org'); - await page.locator('input[type="password"]').fill('test-user'); - await page.getByRole('button', { name: 'Sign In' }).click(); - await page.waitForURL('/'); + await page.goto("/login"); + await page.locator('input[type="email"]').fill("user@libredb.org"); + await page.locator('input[type="password"]').fill("test-user"); + await page.getByRole("button", { name: "Sign In" }).click(); + await page.waitForURL("/"); // Wait for studio to fully load - await expect(page.locator('text=Query 1').first()).toBeVisible({ timeout: 10000 }); + await expect(page.locator("text=Query 1").first()).toBeVisible({ timeout: 10000 }); }); - test('add connection button opens modal', async ({ page }) => { + test("add connection button opens modal", async ({ page }) => { // The sidebar header has buttons next to LibreDB Studio logo // The last button in that row is the add connection button - const sidebarButtons = page.locator('text=LibreDB Studio').locator('..').locator('..').locator('button'); + const sidebarButtons = page.locator("text=LibreDB Studio").locator("..").locator("..").locator("button"); await sidebarButtons.last().click(); // Connection modal should appear await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); }); - test('connection modal shows database type selector', async ({ page }) => { + test("connection modal shows database type selector", async ({ page }) => { // Open connection modal - const sidebarButtons = page.locator('text=LibreDB Studio').locator('..').locator('..').locator('button'); + const sidebarButtons = page.locator("text=LibreDB Studio").locator("..").locator("..").locator("button"); await sidebarButtons.last().click(); const dialog = page.locator('[role="dialog"]'); await expect(dialog).toBeVisible({ timeout: 5000 }); // Should show database type options inside the dialog - await expect(dialog.locator('text=PostgreSQL')).toBeVisible({ timeout: 5000 }); + await expect(dialog.locator("text=PostgreSQL")).toBeVisible({ timeout: 5000 }); }); - test('connection modal has required fields', async ({ page }) => { - const sidebarButtons = page.locator('text=LibreDB Studio').locator('..').locator('..').locator('button'); + test("connection modal has required fields", async ({ page }) => { + const sidebarButtons = page.locator("text=LibreDB Studio").locator("..").locator("..").locator("button"); await sidebarButtons.last().click(); await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); @@ -44,14 +44,14 @@ test.describe('Connection Management', () => { await expect(page.locator('input[value="localhost"]').first()).toBeVisible(); }); - test('connection modal can be closed', async ({ page }) => { - const sidebarButtons = page.locator('text=LibreDB Studio').locator('..').locator('..').locator('button'); + test("connection modal can be closed", async ({ page }) => { + const sidebarButtons = page.locator("text=LibreDB Studio").locator("..").locator("..").locator("button"); await sidebarButtons.last().click(); await expect(page.locator('[role="dialog"]')).toBeVisible({ timeout: 5000 }); // Press Escape to close - await page.keyboard.press('Escape'); + await page.keyboard.press("Escape"); await expect(page.locator('[role="dialog"]')).not.toBeVisible({ timeout: 3000 }); }); diff --git a/e2e/export.spec.ts b/e2e/export.spec.ts index e0c807d6..1c079c08 100644 --- a/e2e/export.spec.ts +++ b/e2e/export.spec.ts @@ -1,16 +1,16 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; -test.describe('Export Functionality', () => { +test.describe("Export Functionality", () => { test.beforeEach(async ({ page }) => { // Login as user - await page.goto('/login'); - await page.locator('input[type="email"]').fill('user@libredb.org'); - await page.locator('input[type="password"]').fill('test-user'); - await page.getByRole('button', { name: /sign in/i }).click(); - await page.waitForURL('/'); + await page.goto("/login"); + await page.locator('input[type="email"]').fill("user@libredb.org"); + await page.locator('input[type="password"]').fill("test-user"); + await page.getByRole("button", { name: /sign in/i }).click(); + await page.waitForURL("/"); }); - test('export dropdown is not visible when no results', async ({ page }) => { + test("export dropdown is not visible when no results", async ({ page }) => { // Without query results, export dropdown should not be prominent // The export button appears in the results panel header await page.waitForTimeout(1000); @@ -20,7 +20,7 @@ test.describe('Export Functionality', () => { await expect(exportBtn).toHaveCount(0); }); - test('history tab has export functionality', async ({ page }) => { + test("history tab has export functionality", async ({ page }) => { // Switch to history tab const historyTab = page.locator('button:has-text("History")').first(); await historyTab.click(); @@ -29,6 +29,6 @@ test.describe('Export Functionality', () => { await page.waitForTimeout(500); // The history panel has export options (CSV/JSON) - await expect(page.locator('text=History').first()).toBeVisible(); + await expect(page.locator("text=History").first()).toBeVisible(); }); }); diff --git a/e2e/login.spec.ts b/e2e/login.spec.ts index 048bb1cd..dd4a546a 100644 --- a/e2e/login.spec.ts +++ b/e2e/login.spec.ts @@ -1,84 +1,84 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; -test.describe('Login Flow', () => { +test.describe("Login Flow", () => { test.beforeEach(async ({ page }) => { - await page.goto('/login'); + await page.goto("/login"); }); - test('shows login page with email and password fields', async ({ page }) => { - await expect(page.locator('text=LibreDB Studio').first()).toBeVisible(); + test("shows login page with email and password fields", async ({ page }) => { + await expect(page.locator("text=LibreDB Studio").first()).toBeVisible(); await expect(page.locator('input[type="email"]').first()).toBeVisible(); await expect(page.locator('input[type="password"]').first()).toBeVisible(); await expect(page.locator('button:has-text("Sign In")').first()).toBeVisible(); }); - test('admin login redirects to /admin', async ({ page }) => { - await page.locator('input[type="email"]').fill('admin@libredb.org'); - await page.locator('input[type="password"]').fill('test-admin'); - await page.getByRole('button', { name: /sign in/i }).click(); - await page.waitForURL('**/admin**'); + test("admin login redirects to /admin", async ({ page }) => { + await page.locator('input[type="email"]').fill("admin@libredb.org"); + await page.locator('input[type="password"]').fill("test-admin"); + await page.getByRole("button", { name: /sign in/i }).click(); + await page.waitForURL("**/admin**"); await expect(page).toHaveURL(/\/admin/); }); - test('user login redirects to /', async ({ page }) => { - await page.locator('input[type="email"]').fill('user@libredb.org'); - await page.locator('input[type="password"]').fill('test-user'); - await page.getByRole('button', { name: /sign in/i }).click(); - await page.waitForURL('/'); - await expect(page).toHaveURL('/'); + test("user login redirects to /", async ({ page }) => { + await page.locator('input[type="email"]').fill("user@libredb.org"); + await page.locator('input[type="password"]').fill("test-user"); + await page.getByRole("button", { name: /sign in/i }).click(); + await page.waitForURL("/"); + await expect(page).toHaveURL("/"); }); - test('wrong password shows error', async ({ page }) => { - await page.locator('input[type="email"]').fill('admin@libredb.org'); - await page.locator('input[type="password"]').fill('wrong-password'); - await page.getByRole('button', { name: /sign in/i }).click(); + test("wrong password shows error", async ({ page }) => { + await page.locator('input[type="email"]').fill("admin@libredb.org"); + await page.locator('input[type="password"]').fill("wrong-password"); + await page.getByRole("button", { name: /sign in/i }).click(); // Should stay on login page await expect(page).toHaveURL(/\/login/); }); - test('empty fields shows validation error', async ({ page }) => { - await page.getByRole('button', { name: /sign in/i }).click(); + test("empty fields shows validation error", async ({ page }) => { + await page.getByRole("button", { name: /sign in/i }).click(); // Should stay on login page await expect(page).toHaveURL(/\/login/); }); - test('authenticated admin accessing /login redirects to /admin', async ({ page }) => { + test("authenticated admin accessing /login redirects to /admin", async ({ page }) => { // Login as admin first - await page.locator('input[type="email"]').fill('admin@libredb.org'); - await page.locator('input[type="password"]').fill('test-admin'); - await page.getByRole('button', { name: /sign in/i }).click(); - await page.waitForURL('**/admin**'); + await page.locator('input[type="email"]').fill("admin@libredb.org"); + await page.locator('input[type="password"]').fill("test-admin"); + await page.getByRole("button", { name: /sign in/i }).click(); + await page.waitForURL("**/admin**"); // Try navigating back to /login - await page.goto('/login'); + await page.goto("/login"); await expect(page).toHaveURL(/\/admin/); }); - test('authenticated user accessing /login redirects to /', async ({ page }) => { + test("authenticated user accessing /login redirects to /", async ({ page }) => { // Login as user first - await page.locator('input[type="email"]').fill('user@libredb.org'); - await page.locator('input[type="password"]').fill('test-user'); - await page.getByRole('button', { name: /sign in/i }).click(); - await page.waitForURL('/'); + await page.locator('input[type="email"]').fill("user@libredb.org"); + await page.locator('input[type="password"]').fill("test-user"); + await page.getByRole("button", { name: /sign in/i }).click(); + await page.waitForURL("/"); // Try navigating back to /login - await page.goto('/login'); - await expect(page).toHaveURL('/'); + await page.goto("/login"); + await expect(page).toHaveURL("/"); }); - test('unauthenticated user accessing / redirects to /login', async ({ page }) => { - await page.goto('/'); + test("unauthenticated user accessing / redirects to /login", async ({ page }) => { + await page.goto("/"); await expect(page).toHaveURL(/\/login/); }); - test('user role cannot access /admin', async ({ page }) => { - await page.locator('input[type="email"]').fill('user@libredb.org'); - await page.locator('input[type="password"]').fill('test-user'); - await page.getByRole('button', { name: /sign in/i }).click(); - await page.waitForURL('/'); + test("user role cannot access /admin", async ({ page }) => { + await page.locator('input[type="email"]').fill("user@libredb.org"); + await page.locator('input[type="password"]').fill("test-user"); + await page.getByRole("button", { name: /sign in/i }).click(); + await page.waitForURL("/"); // Try accessing admin page - await page.goto('/admin'); + await page.goto("/admin"); // Should redirect away from admin await expect(page).not.toHaveURL(/\/admin/); }); diff --git a/e2e/query-execution.spec.ts b/e2e/query-execution.spec.ts index e5dbef4c..db132ff2 100644 --- a/e2e/query-execution.spec.ts +++ b/e2e/query-execution.spec.ts @@ -1,35 +1,37 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; -test.describe('Query Execution', () => { +test.describe("Query Execution", () => { test.beforeEach(async ({ page }) => { // Login as user - await page.goto('/login'); - await page.locator('input[type="email"]').fill('user@libredb.org'); - await page.locator('input[type="password"]').fill('test-user'); - await page.getByRole('button', { name: 'Sign In' }).click(); - await page.waitForURL('/'); + await page.goto("/login"); + await page.locator('input[type="email"]').fill("user@libredb.org"); + await page.locator('input[type="password"]').fill("test-user"); + await page.getByRole("button", { name: "Sign In" }).click(); + await page.waitForURL("/"); }); - test('query editor is visible after login', async ({ page }) => { + test("query editor is visible after login", async ({ page }) => { // The Monaco editor or its container should be visible - await expect(page.locator('.monaco-editor, [data-testid="query-editor"], textarea').first()).toBeVisible({ timeout: 10000 }); + await expect(page.locator('.monaco-editor, [data-testid="query-editor"], textarea').first()).toBeVisible({ + timeout: 10000, + }); }); - test('run button is visible', async ({ page }) => { + test("run button is visible", async ({ page }) => { // Run button shows as "RUN" in the toolbar - await expect(page.getByRole('button', { name: 'RUN' })).toBeVisible({ timeout: 10000 }); + await expect(page.getByRole("button", { name: "RUN" })).toBeVisible({ timeout: 10000 }); }); - test('bottom panel shows results tab', async ({ page }) => { + test("bottom panel shows results tab", async ({ page }) => { // Results tab button should be visible in the bottom panel - await expect(page.getByRole('button', { name: 'Results' })).toBeVisible({ timeout: 10000 }); + await expect(page.getByRole("button", { name: "Results" })).toBeVisible({ timeout: 10000 }); }); - test('bottom panel has history tab', async ({ page }) => { - await expect(page.getByRole('button', { name: 'History' })).toBeVisible({ timeout: 10000 }); + test("bottom panel has history tab", async ({ page }) => { + await expect(page.getByRole("button", { name: "History" })).toBeVisible({ timeout: 10000 }); }); - test('bottom panel has charts tab', async ({ page }) => { - await expect(page.getByRole('button', { name: 'Charts' })).toBeVisible({ timeout: 10000 }); + test("bottom panel has charts tab", async ({ page }) => { + await expect(page.getByRole("button", { name: "Charts" })).toBeVisible({ timeout: 10000 }); }); }); diff --git a/e2e/tab-management.spec.ts b/e2e/tab-management.spec.ts index f326667f..dde9864a 100644 --- a/e2e/tab-management.spec.ts +++ b/e2e/tab-management.spec.ts @@ -1,64 +1,64 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; -test.describe('Tab Management', () => { +test.describe("Tab Management", () => { test.beforeEach(async ({ page }) => { // Login as user - await page.goto('/login'); - await page.locator('input[type="email"]').fill('user@libredb.org'); - await page.locator('input[type="password"]').fill('test-user'); - await page.getByRole('button', { name: 'Sign In' }).click(); - await page.waitForURL('/'); + await page.goto("/login"); + await page.locator('input[type="email"]').fill("user@libredb.org"); + await page.locator('input[type="password"]').fill("test-user"); + await page.getByRole("button", { name: "Sign In" }).click(); + await page.waitForURL("/"); // Wait for studio to fully load - await expect(page.locator('text=Query 1').first()).toBeVisible({ timeout: 10000 }); + await expect(page.locator("text=Query 1").first()).toBeVisible({ timeout: 10000 }); }); - test('default tab exists with name Query 1', async ({ page }) => { - await expect(page.locator('text=Query 1').first()).toBeVisible(); + test("default tab exists with name Query 1", async ({ page }) => { + await expect(page.locator("text=Query 1").first()).toBeVisible(); }); - test('can add a new tab', async ({ page }) => { + test("can add a new tab", async ({ page }) => { // The tab bar's plus icon is a sibling of the "Query 1" tab div // Navigate from Query 1 text → its parent tab div → the parent tab bar → find the direct child SVG plus - const query1Parent = page.locator('text=Query 1').first().locator('..'); - const tabBar = query1Parent.locator('..'); - const tabPlusIcon = tabBar.locator(':scope > svg').first(); + const query1Parent = page.locator("text=Query 1").first().locator(".."); + const tabBar = query1Parent.locator(".."); + const tabPlusIcon = tabBar.locator(":scope > svg").first(); await tabPlusIcon.click(); // New tab "Query 2" should appear - await expect(page.locator('text=Query 2')).toBeVisible({ timeout: 5000 }); + await expect(page.locator("text=Query 2")).toBeVisible({ timeout: 5000 }); }); - test('can switch between tabs', async ({ page }) => { + test("can switch between tabs", async ({ page }) => { // Add a second tab using the same strategy - const query1Parent = page.locator('text=Query 1').first().locator('..'); - const tabBar = query1Parent.locator('..'); - const tabPlusIcon = tabBar.locator(':scope > svg').first(); + const query1Parent = page.locator("text=Query 1").first().locator(".."); + const tabBar = query1Parent.locator(".."); + const tabPlusIcon = tabBar.locator(":scope > svg").first(); await tabPlusIcon.click(); - await expect(page.locator('text=Query 2')).toBeVisible({ timeout: 5000 }); + await expect(page.locator("text=Query 2")).toBeVisible({ timeout: 5000 }); // Click on Query 1 to switch back - await page.locator('text=Query 1').first().click(); + await page.locator("text=Query 1").first().click(); await page.waitForTimeout(300); }); - test('can close a tab when multiple exist', async ({ page }) => { + test("can close a tab when multiple exist", async ({ page }) => { // Add a second tab - const query1Parent = page.locator('text=Query 1').first().locator('..'); - const tabBar = query1Parent.locator('..'); - const tabPlusIcon = tabBar.locator(':scope > svg').first(); + const query1Parent = page.locator("text=Query 1").first().locator(".."); + const tabBar = query1Parent.locator(".."); + const tabPlusIcon = tabBar.locator(":scope > svg").first(); await tabPlusIcon.click(); - await expect(page.locator('text=Query 2')).toBeVisible({ timeout: 5000 }); + await expect(page.locator("text=Query 2")).toBeVisible({ timeout: 5000 }); // Close Query 2 — the X icon is inside the Query 2 tab div // Hover the tab to reveal the X icon, then click - const query2Parent = page.locator('text=Query 2').first().locator('..'); + const query2Parent = page.locator("text=Query 2").first().locator(".."); await query2Parent.hover(); - const closeIcon = query2Parent.locator('svg').last(); + const closeIcon = query2Parent.locator("svg").last(); await closeIcon.click(); // Query 2 should no longer exist - await expect(page.locator('text=Query 2')).not.toBeVisible({ timeout: 3000 }); + await expect(page.locator("text=Query 2")).not.toBeVisible({ timeout: 3000 }); // Query 1 should still exist - await expect(page.locator('text=Query 1').first()).toBeVisible(); + await expect(page.locator("text=Query 1").first()).toBeVisible(); }); }); diff --git a/eslint.config.mjs b/eslint.config.mjs index 6412e124..39601419 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,17 +1,12 @@ import { defineConfig, globalIgnores } from "eslint/config"; import nextCoreWebVitals from "eslint-config-next/core-web-vitals"; import nextTypescript from "eslint-config-next/typescript"; +import tseslint from "typescript-eslint"; const eslintConfig = defineConfig([ ...nextCoreWebVitals, ...nextTypescript, - globalIgnores([ - ".next/**", - "out/**", - "build/**", - "dist/**", - "next-env.d.ts", - ]), + globalIgnores([".next/**", "out/**", "build/**", "dist/**", "next-env.d.ts"]), { rules: { "@typescript-eslint/no-explicit-any": "warn", @@ -24,6 +19,25 @@ const eslintConfig = defineConfig([ "react-hooks/incompatible-library": "warn", }, }, + // Narrow type-aware safety net for the async-heavy code paths (API routes + // and DB providers). These rules need the real TypeScript type checker + // (projectService), so they are scoped to keep lint fast and to catch + // unhandled-promise bugs where they matter most. Strategy A: eslint-config-next + // still owns all React/Next/hooks linting above; this only adds promise safety. + ...tseslint.config({ + files: ["src/app/api/**/*.ts", "src/lib/db/**/*.ts"], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/await-thenable": "error", + }, + }), ]); export default eslintConfig; diff --git a/next.config.ts b/next.config.ts index 84b21e1c..977bb237 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,20 +7,20 @@ const nextConfig: NextConfig = { }, // Use standalone output for Docker/Kubernetes deployments // For Vercel, this is automatically handled - output: process.env.DOCKER_BUILD === 'true' ? 'standalone' : undefined, + output: process.env.DOCKER_BUILD === "true" ? "standalone" : undefined, // Externalize native modules to reduce bundle size and memory usage // These packages will be loaded from node_modules at runtime - serverExternalPackages: ['pg', 'mysql2', 'mongodb', 'better-sqlite3', 'ssh2'], + serverExternalPackages: ["pg", "mysql2", "mongodb", "better-sqlite3", "ssh2"], images: { remotePatterns: [ { - protocol: 'https', - hostname: '**', + protocol: "https", + hostname: "**", }, { - protocol: 'http', - hostname: '**', + protocol: "http", + hostname: "**", }, ], }, diff --git a/package.json b/package.json index 3e0d1bfc..798eb5f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@libredb/studio", - "version": "0.9.32", + "version": "0.9.33", "private": false, "publishConfig": { "access": "public" @@ -79,9 +79,13 @@ "dev": "next dev", "build": "next build", "build:lib": "tsup", - "prepublishOnly": "tsup", + "attw": "rm -rf .attw && bun pm pack --quiet --destination .attw && attw .attw/*.tgz --profile node16", + "prepublishOnly": "tsup && bun run attw", "start": "next start", - "lint": "eslint .", + "format": "biome format .", + "format:fix": "biome format --write .", + "lint": "oxlint && eslint .", + "lint:oxc": "oxlint", "typecheck": "tsc --noEmit", "test": "bun test tests/unit tests/api tests/integration && bun test tests/hooks && bun run test:components", "test:ci": "bash tests/run-core.sh && bun run test:components", @@ -167,6 +171,8 @@ "zod": "^4.1.12" }, "devDependencies": { + "@arethetypeswrong/cli": "^0.18.4", + "@biomejs/biome": "^2.5", "@playwright/test": "^1.58.2", "@tailwindcss/postcss": "^4", "@testing-library/react": "^16.3.2", @@ -182,8 +188,10 @@ "eslint-config-next": "^16.1.6", "happy-dom": "^20.6.1", "knip": "^6.17.1", + "oxlint": "^1.71", "tailwindcss": "^4", "tsup": "^8.5.1", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "typescript-eslint": "^8.62" } } diff --git a/playwright.config.ts b/playwright.config.ts index 78362bfb..95e6c36d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,34 +1,34 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ - testDir: './e2e', + testDir: "./e2e", fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, - reporter: process.env.CI ? 'html' : 'list', + reporter: process.env.CI ? "html" : "list", use: { - baseURL: 'http://localhost:3000', - trace: 'on-first-retry', - screenshot: 'only-on-failure', + baseURL: "http://localhost:3000", + trace: "on-first-retry", + screenshot: "only-on-failure", }, projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, ], webServer: { - command: 'bun run build && bun start', - url: 'http://localhost:3000', + command: "bun run build && bun start", + url: "http://localhost:3000", reuseExistingServer: !process.env.CI, timeout: 120_000, env: { - JWT_SECRET: 'test-jwt-secret-for-e2e-tests-32ch', - ADMIN_EMAIL: 'admin@libredb.org', - ADMIN_PASSWORD: 'test-admin', - USER_EMAIL: 'user@libredb.org', - USER_PASSWORD: 'test-user', + JWT_SECRET: "test-jwt-secret-for-e2e-tests-32ch", + ADMIN_EMAIL: "admin@libredb.org", + ADMIN_PASSWORD: "test-admin", + USER_EMAIL: "user@libredb.org", + USER_PASSWORD: "test-user", }, }, }); diff --git a/scripts/merge-lcov.mjs b/scripts/merge-lcov.mjs index 2cb2a243..67609720 100644 --- a/scripts/merge-lcov.mjs +++ b/scripts/merge-lcov.mjs @@ -151,7 +151,7 @@ function isNonExecutableLine(src) { if (t.startsWith("//")) return true; // line comment if (t.startsWith("/*")) return true; // block comment opener if (t === "*/" || /^\*( |$|\/)/.test(t)) return true; // JSDoc / block comment body - if (/^[{}()\[\];,]+$/.test(t)) return true; // bare structural punctuation + if (/^[{}()[\];,]+$/.test(t)) return true; // bare structural punctuation return false; } @@ -190,7 +190,10 @@ function serializeRecords(records) { } const fnf = sortedFunctions.length; - const fnh = sortedFunctions.reduce((acc, [fnName]) => acc + ((record.functionHits.get(fnName) || 0) > 0 ? 1 : 0), 0); + const fnh = sortedFunctions.reduce( + (acc, [fnName]) => acc + ((record.functionHits.get(fnName) || 0) > 0 ? 1 : 0), + 0, + ); lines.push(`FNF:${fnf}`); lines.push(`FNH:${fnh}`); diff --git a/src/app/admin/error.tsx b/src/app/admin/error.tsx index fd099f24..adaa9a77 100644 --- a/src/app/admin/error.tsx +++ b/src/app/admin/error.tsx @@ -1,16 +1,10 @@ -'use client'; +"use client"; -import { useEffect } from 'react'; +import { useEffect } from "react"; -export default function AdminError({ - error, - reset, -}: { - error: Error & { digest?: string }; - reset: () => void; -}) { +export default function AdminError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { useEffect(() => { - console.error('[AdminErrorBoundary]', error.message, error.digest); + console.error("[AdminErrorBoundary]", error.message, error.digest); }, [error]); return ( @@ -18,14 +12,9 @@ export default function AdminError({

Admin Dashboard Error

- The admin dashboard encountered an error. You can try again or return - to the main studio. + The admin dashboard encountered an error. You can try again or return to the main studio.

- {error.digest && ( -

- Error ID: {error.digest} -

- )} + {error.digest &&

Error ID: {error.digest}

}
@@ -255,7 +273,7 @@ function LoginFormInner({ authProvider }: { authProvider: string }) { type="submit" disabled={isLoading} > - {isLoading ? 'Authenticating...' : 'Sign In'} + {isLoading ? "Authenticating..." : "Sign In"} @@ -276,7 +294,7 @@ function LoginFormInner({ authProvider }: { authProvider: string }) {
- {['PostgreSQL', 'MySQL', 'MongoDB', 'Oracle', 'SQL Server'].map((db) => ( + {["PostgreSQL", "MySQL", "MongoDB", "Oracle", "SQL Server"].map((db) => ( ; } diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index 609f296a..aaec87ac 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -1,6 +1,6 @@ -'use client'; +"use client"; -import { MonitoringDashboard } from '@/components/monitoring/MonitoringDashboard'; +import { MonitoringDashboard } from "@/components/monitoring/MonitoringDashboard"; export default function MonitoringPage() { // Middleware handles authentication, no need for client-side check diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 9db485f5..71cca961 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -1,4 +1,4 @@ -import Link from 'next/link'; +import Link from "next/link"; export default function NotFound() { return ( @@ -6,9 +6,7 @@ export default function NotFound() {
404

Page Not Found

-

- The page you are looking for does not exist or has been moved. -

+

The page you are looking for does not exist or has been moved.

; diff --git a/src/components/AIAutopilotPanel.tsx b/src/components/AIAutopilotPanel.tsx index 3d1b71aa..6964ef93 100644 --- a/src/components/AIAutopilotPanel.tsx +++ b/src/components/AIAutopilotPanel.tsx @@ -1,9 +1,9 @@ "use client"; -import React, { useState, useRef } from 'react'; -import { Loader2, Sparkles, RefreshCw, Play, Copy, Check } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { DatabaseConnection } from '@/lib/types'; +import React, { useState, useRef } from "react"; +import { Loader2, Sparkles, RefreshCw, Play, Copy, Check } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { DatabaseConnection } from "@/lib/types"; interface AIAutopilotPanelProps { connection: DatabaseConnection | null; @@ -13,7 +13,7 @@ interface AIAutopilotPanelProps { export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: AIAutopilotPanelProps) { const [isLoading, setIsLoading] = useState(false); - const [report, setReport] = useState(''); + const [report, setReport] = useState(""); const [error, setError] = useState(null); const [copiedIndex, setCopiedIndex] = useState(null); const reportRef = useRef(null); @@ -22,14 +22,14 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: if (!connection) return; setIsLoading(true); setError(null); - setReport(''); + setReport(""); try { // Fetch monitoring data in parallel const [monitoringRes] = await Promise.all([ - fetch('/api/db/monitoring', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + fetch("/api/db/monitoring", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ connection, options: { @@ -47,23 +47,30 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: } // Build filtered schema - let filteredSchema = ''; + let filteredSchema = ""; if (schemaContext) { try { const tables = JSON.parse(schemaContext); - filteredSchema = tables.slice(0, 30).map((t: { name: string; rowCount?: number; columns?: { name: string; type: string }[] }) => { - const cols = t.columns?.slice(0, 6).map(c => `${c.name} (${c.type})`).join(', ') || ''; - return `${t.name} (${t.rowCount || 0} rows): ${cols}`; - }).join('\n'); + filteredSchema = tables + .slice(0, 30) + .map((t: { name: string; rowCount?: number; columns?: { name: string; type: string }[] }) => { + const cols = + t.columns + ?.slice(0, 6) + .map((c) => `${c.name} (${c.type})`) + .join(", ") || ""; + return `${t.name} (${t.rowCount || 0} rows): ${cols}`; + }) + .join("\n"); } catch { filteredSchema = schemaContext.substring(0, 2000); } } // Call autopilot AI - const response = await fetch('/api/ai/autopilot', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const response = await fetch("/api/ai/autopilot", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ slowQueries: monitoringData?.slowQueries, indexStats: monitoringData?.indexes, @@ -77,13 +84,13 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: if (!response.ok) { const errData = await response.json(); - throw new Error(errData.error || 'Autopilot analysis failed'); + throw new Error(errData.error || "Autopilot analysis failed"); } const reader = response.body?.getReader(); - if (!reader) throw new Error('No reader'); + if (!reader) throw new Error("No reader"); - let fullResponse = ''; + let fullResponse = ""; while (true) { const { done, value } = await reader.read(); if (done) break; @@ -91,7 +98,7 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: setReport(fullResponse); } } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); + setError(err instanceof Error ? err.message : "Unknown error"); } finally { setIsLoading(false); } @@ -105,16 +112,16 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: // Simple markdown rendering (headers, bold, lists) const renderMarkdown = (text: string) => { - const lines = text.split('\n'); + const lines = text.split("\n"); const elements: React.ReactNode[] = []; let inCodeBlock = false; - let codeContent = ''; + let codeContent = ""; let codeBlockIdx = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; - if (line.startsWith('```')) { + if (line.startsWith("```")) { if (inCodeBlock) { // End of code block const blockIndex = codeBlockIdx++; @@ -130,7 +137,11 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: className="p-1 rounded bg-white/10 hover:bg-white/20 text-zinc-400" title="Copy" > - {copiedIndex === blockIndex ? : } + {copiedIndex === blockIndex ? ( + + ) : ( + + )} {onExecuteQuery && (
-
+
, ); - codeContent = ''; + codeContent = ""; inCodeBlock = false; } else { inCodeBlock = true; @@ -153,24 +164,46 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: } if (inCodeBlock) { - codeContent += line + '\n'; + codeContent += line + "\n"; continue; } // Headers - if (line.startsWith('## ')) { - elements.push(

{line.slice(3)}

); - } else if (line.startsWith('### ')) { - elements.push(

{line.slice(4)}

); - } else if (line.startsWith('- ')) { + if (line.startsWith("## ")) { + elements.push( +

+ {line.slice(3)} +

, + ); + } else if (line.startsWith("### ")) { + elements.push( +

+ {line.slice(4)} +

, + ); + } else if (line.startsWith("- ")) { const content = line.slice(2).replace(/\*\*(.*?)\*\*/g, '$1'); - elements.push(
  • ); + elements.push( +
  • , + ); } else if (line.match(/^\d+\.\s/)) { const content = line.replace(/\*\*(.*?)\*\*/g, '$1'); - elements.push(
  • ); + elements.push( +
  • , + ); } else if (line.trim()) { const content = line.replace(/\*\*(.*?)\*\*/g, '$1'); - elements.push(

    ); + elements.push( +

    , + ); } else { elements.push(

    ); } @@ -187,24 +220,24 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }:
    - - AI Performance Autopilot - + AI Performance Autopilot
  • @@ -222,16 +255,10 @@ export function AIAutopilotPanel({ connection, schemaContext, onExecuteQuery }: )} {error && ( -
    - {error} -
    +
    {error}
    )} - {report && ( -
    - {renderMarkdown(report)} -
    - )} + {report &&
    {renderMarkdown(report)}
    }
    ); diff --git a/src/components/CodeGenerator.tsx b/src/components/CodeGenerator.tsx index afc652ec..e577358f 100644 --- a/src/components/CodeGenerator.tsx +++ b/src/components/CodeGenerator.tsx @@ -1,9 +1,9 @@ "use client"; -import React, { useState, useMemo } from 'react'; -import { Code, X, Copy, Check, ChevronDown } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { TableSchema } from '@/lib/types'; +import React, { useState, useMemo } from "react"; +import { Code, X, Copy, Check, ChevronDown } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { TableSchema } from "@/lib/types"; interface CodeGeneratorProps { isOpen: boolean; @@ -13,22 +13,22 @@ interface CodeGeneratorProps { databaseType?: string; } -type Language = 'typescript' | 'zod' | 'prisma' | 'go' | 'python' | 'java'; +type Language = "typescript" | "zod" | "prisma" | "go" | "python" | "java"; const LANGUAGES: { id: Language; label: string; ext: string }[] = [ - { id: 'typescript', label: 'TypeScript Interface', ext: 'ts' }, - { id: 'zod', label: 'Zod Schema', ext: 'ts' }, - { id: 'prisma', label: 'Prisma Model', ext: 'prisma' }, - { id: 'go', label: 'Go Struct', ext: 'go' }, - { id: 'python', label: 'Python Dataclass', ext: 'py' }, - { id: 'java', label: 'Java POJO', ext: 'java' }, + { id: "typescript", label: "TypeScript Interface", ext: "ts" }, + { id: "zod", label: "Zod Schema", ext: "ts" }, + { id: "prisma", label: "Prisma Model", ext: "prisma" }, + { id: "go", label: "Go Struct", ext: "go" }, + { id: "python", label: "Python Dataclass", ext: "py" }, + { id: "java", label: "Java POJO", ext: "java" }, ]; export function toPascalCase(str: string): string { return str .replace(/[_-](\w)/g, (_, c) => c.toUpperCase()) - .replace(/^\w/, c => c.toUpperCase()) - .replace(/s$/, ''); // Remove trailing 's' (pluralized table name) + .replace(/^\w/, (c) => c.toUpperCase()) + .replace(/s$/, ""); // Remove trailing 's' (pluralized table name) } export function toCamelCase(str: string): string { @@ -37,72 +37,107 @@ export function toCamelCase(str: string): string { } export function toSnakeCase(str: string): string { - return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, ''); + return str + .replace(/([A-Z])/g, "_$1") + .toLowerCase() + .replace(/^_/, ""); } export function mapSqlTypeToTS(sqlType: string): string { const t = sqlType.toLowerCase(); - if (t.includes('int') || t.includes('float') || t.includes('double') || t.includes('decimal') || t.includes('numeric') || t.includes('real') || t.includes('serial')) return 'number'; - if (t.includes('bool')) return 'boolean'; - if (t.includes('date') || t.includes('time')) return 'Date'; - if (t.includes('json')) return 'Record'; - if (t.includes('uuid')) return 'string'; - if (t.includes('array')) return 'unknown[]'; - return 'string'; + if ( + t.includes("int") || + t.includes("float") || + t.includes("double") || + t.includes("decimal") || + t.includes("numeric") || + t.includes("real") || + t.includes("serial") + ) + return "number"; + if (t.includes("bool")) return "boolean"; + if (t.includes("date") || t.includes("time")) return "Date"; + if (t.includes("json")) return "Record"; + if (t.includes("uuid")) return "string"; + if (t.includes("array")) return "unknown[]"; + return "string"; } export function mapSqlTypeToZod(sqlType: string): string { const t = sqlType.toLowerCase(); - if (t.includes('int') || t.includes('float') || t.includes('double') || t.includes('decimal') || t.includes('numeric') || t.includes('real') || t.includes('serial')) return 'z.number()'; - if (t.includes('bool')) return 'z.boolean()'; - if (t.includes('date') || t.includes('time')) return 'z.date()'; - if (t.includes('json')) return 'z.record(z.unknown())'; - if (t.includes('uuid')) return 'z.string().uuid()'; - return 'z.string()'; + if ( + t.includes("int") || + t.includes("float") || + t.includes("double") || + t.includes("decimal") || + t.includes("numeric") || + t.includes("real") || + t.includes("serial") + ) + return "z.number()"; + if (t.includes("bool")) return "z.boolean()"; + if (t.includes("date") || t.includes("time")) return "z.date()"; + if (t.includes("json")) return "z.record(z.unknown())"; + if (t.includes("uuid")) return "z.string().uuid()"; + return "z.string()"; } export function mapSqlTypeToPrisma(sqlType: string): string { const t = sqlType.toLowerCase(); - if (t.includes('serial') || t === 'integer' || t === 'int' || t === 'int4') return 'Int'; - if (t.includes('bigint') || t.includes('int8')) return 'BigInt'; - if (t.includes('float') || t.includes('double') || t.includes('decimal') || t.includes('numeric') || t.includes('real')) return 'Float'; - if (t.includes('bool')) return 'Boolean'; - if (t.includes('timestamp') || t.includes('datetime')) return 'DateTime'; - if (t.includes('date')) return 'DateTime'; - if (t.includes('json')) return 'Json'; - return 'String'; + if (t.includes("serial") || t === "integer" || t === "int" || t === "int4") return "Int"; + if (t.includes("bigint") || t.includes("int8")) return "BigInt"; + if ( + t.includes("float") || + t.includes("double") || + t.includes("decimal") || + t.includes("numeric") || + t.includes("real") + ) + return "Float"; + if (t.includes("bool")) return "Boolean"; + if (t.includes("timestamp") || t.includes("datetime")) return "DateTime"; + if (t.includes("date")) return "DateTime"; + if (t.includes("json")) return "Json"; + return "String"; } export function mapSqlTypeToGo(sqlType: string): string { const t = sqlType.toLowerCase(); - if (t.includes('serial') || t === 'integer' || t === 'int' || t === 'int4') return 'int'; - if (t.includes('bigint') || t.includes('int8')) return 'int64'; - if (t.includes('float') || t.includes('real')) return 'float32'; - if (t.includes('double') || t.includes('decimal') || t.includes('numeric')) return 'float64'; - if (t.includes('bool')) return 'bool'; - if (t.includes('date') || t.includes('time')) return 'time.Time'; - return 'string'; + if (t.includes("serial") || t === "integer" || t === "int" || t === "int4") return "int"; + if (t.includes("bigint") || t.includes("int8")) return "int64"; + if (t.includes("float") || t.includes("real")) return "float32"; + if (t.includes("double") || t.includes("decimal") || t.includes("numeric")) return "float64"; + if (t.includes("bool")) return "bool"; + if (t.includes("date") || t.includes("time")) return "time.Time"; + return "string"; } export function mapSqlTypeToPython(sqlType: string): string { const t = sqlType.toLowerCase(); - if (t.includes('int') || t.includes('serial')) return 'int'; - if (t.includes('float') || t.includes('double') || t.includes('decimal') || t.includes('numeric') || t.includes('real')) return 'float'; - if (t.includes('bool')) return 'bool'; - if (t.includes('date') || t.includes('time')) return 'datetime'; - if (t.includes('json')) return 'dict'; - return 'str'; + if (t.includes("int") || t.includes("serial")) return "int"; + if ( + t.includes("float") || + t.includes("double") || + t.includes("decimal") || + t.includes("numeric") || + t.includes("real") + ) + return "float"; + if (t.includes("bool")) return "bool"; + if (t.includes("date") || t.includes("time")) return "datetime"; + if (t.includes("json")) return "dict"; + return "str"; } export function mapSqlTypeToJava(sqlType: string): string { const t = sqlType.toLowerCase(); - if (t.includes('serial') || t === 'integer' || t === 'int' || t === 'int4') return 'Integer'; - if (t.includes('bigint') || t.includes('int8')) return 'Long'; - if (t.includes('float') || t.includes('real')) return 'Float'; - if (t.includes('double') || t.includes('decimal') || t.includes('numeric')) return 'Double'; - if (t.includes('bool')) return 'Boolean'; - if (t.includes('date') || t.includes('time')) return 'LocalDateTime'; - return 'String'; + if (t.includes("serial") || t === "integer" || t === "int" || t === "int4") return "Integer"; + if (t.includes("bigint") || t.includes("int8")) return "Long"; + if (t.includes("float") || t.includes("real")) return "Float"; + if (t.includes("double") || t.includes("decimal") || t.includes("numeric")) return "Double"; + if (t.includes("bool")) return "Boolean"; + if (t.includes("date") || t.includes("time")) return "LocalDateTime"; + return "String"; } export function generateCode(lang: Language, table: TableSchema): string { @@ -110,81 +145,81 @@ export function generateCode(lang: Language, table: TableSchema): string { const columns = table.columns || []; switch (lang) { - case 'typescript': { - const fields = columns.map(c => { + case "typescript": { + const fields = columns.map((c) => { const tsType = mapSqlTypeToTS(c.type); - const nullable = c.nullable ? ' | null' : ''; + const nullable = c.nullable ? " | null" : ""; return ` ${toCamelCase(c.name)}: ${tsType}${nullable};`; }); - return `export interface ${name} {\n${fields.join('\n')}\n}`; + return `export interface ${name} {\n${fields.join("\n")}\n}`; } - case 'zod': { - const fields = columns.map(c => { + case "zod": { + const fields = columns.map((c) => { let zodType = mapSqlTypeToZod(c.type); - if (c.nullable) zodType += '.nullable()'; + if (c.nullable) zodType += ".nullable()"; return ` ${toCamelCase(c.name)}: ${zodType},`; }); - return `import { z } from 'zod';\n\nexport const ${name}Schema = z.object({\n${fields.join('\n')}\n});\n\nexport type ${name} = z.infer;`; + return `import { z } from 'zod';\n\nexport const ${name}Schema = z.object({\n${fields.join("\n")}\n});\n\nexport type ${name} = z.infer;`; } - case 'prisma': { - const fields = columns.map(c => { + case "prisma": { + const fields = columns.map((c) => { const prismaType = mapSqlTypeToPrisma(c.type); - const nullable = c.nullable ? '?' : ''; - const pk = c.isPrimary ? ' @id' : ''; - const auto = c.type.toLowerCase().includes('serial') ? ' @default(autoincrement())' : ''; + const nullable = c.nullable ? "?" : ""; + const pk = c.isPrimary ? " @id" : ""; + const auto = c.type.toLowerCase().includes("serial") ? " @default(autoincrement())" : ""; return ` ${c.name} ${prismaType}${nullable}${pk}${auto}`; }); - return `model ${name} {\n${fields.join('\n')}\n\n @@map("${table.name}")\n}`; + return `model ${name} {\n${fields.join("\n")}\n\n @@map("${table.name}")\n}`; } - case 'go': { - const fields = columns.map(c => { + case "go": { + const fields = columns.map((c) => { const goType = mapSqlTypeToGo(c.type); - const nullable = c.nullable ? '*' : ''; + const nullable = c.nullable ? "*" : ""; const fieldName = toPascalCase(c.name); return `\t${fieldName} ${nullable}${goType} \`json:"${c.name}" db:"${c.name}"\``; }); - const needsTime = columns.some(c => c.type.toLowerCase().includes('date') || c.type.toLowerCase().includes('time')); - const imports = needsTime ? '\nimport "time"\n' : ''; - return `package models${imports}\n\ntype ${name} struct {\n${fields.join('\n')}\n}`; + const needsTime = columns.some( + (c) => c.type.toLowerCase().includes("date") || c.type.toLowerCase().includes("time"), + ); + const imports = needsTime ? '\nimport "time"\n' : ""; + return `package models${imports}\n\ntype ${name} struct {\n${fields.join("\n")}\n}`; } - case 'python': { - const fields = columns.map(c => { + case "python": { + const fields = columns.map((c) => { const pyType = mapSqlTypeToPython(c.type); const optional = c.nullable ? `Optional[${pyType}]` : pyType; return ` ${toSnakeCase(c.name)}: ${optional}`; }); - const needsOptional = columns.some(c => c.nullable); - const needsDatetime = columns.some(c => c.type.toLowerCase().includes('date') || c.type.toLowerCase().includes('time')); - const imports: string[] = ['from dataclasses import dataclass']; - if (needsOptional) imports.push('from typing import Optional'); - if (needsDatetime) imports.push('from datetime import datetime'); - return `${imports.join('\n')}\n\n\n@dataclass\nclass ${name}:\n${fields.join('\n')}`; + const needsOptional = columns.some((c) => c.nullable); + const needsDatetime = columns.some( + (c) => c.type.toLowerCase().includes("date") || c.type.toLowerCase().includes("time"), + ); + const imports: string[] = ["from dataclasses import dataclass"]; + if (needsOptional) imports.push("from typing import Optional"); + if (needsDatetime) imports.push("from datetime import datetime"); + return `${imports.join("\n")}\n\n\n@dataclass\nclass ${name}:\n${fields.join("\n")}`; } - case 'java': { - const fields = columns.map(c => { + case "java": { + const fields = columns.map((c) => { const javaType = mapSqlTypeToJava(c.type); return ` private ${javaType} ${toCamelCase(c.name)};`; }); - const needsLocalDateTime = columns.some(c => c.type.toLowerCase().includes('date') || c.type.toLowerCase().includes('time')); - const imports = needsLocalDateTime ? 'import java.time.LocalDateTime;\n\n' : ''; - return `${imports}public class ${name} {\n${fields.join('\n')}\n}`; + const needsLocalDateTime = columns.some( + (c) => c.type.toLowerCase().includes("date") || c.type.toLowerCase().includes("time"), + ); + const imports = needsLocalDateTime ? "import java.time.LocalDateTime;\n\n" : ""; + return `${imports}public class ${name} {\n${fields.join("\n")}\n}`; } } } -export function CodeGenerator({ - isOpen, - onClose, - tableName, - tableSchema, - databaseType, -}: CodeGeneratorProps) { - const [language, setLanguage] = useState('typescript'); +export function CodeGenerator({ isOpen, onClose, tableName, tableSchema, databaseType }: CodeGeneratorProps) { + const [language, setLanguage] = useState("typescript"); const [copied, setCopied] = useState(false); const [showLangDropdown, setShowLangDropdown] = useState(false); const code = useMemo(() => { - if (!tableSchema) return '// No schema available'; + if (!tableSchema) return "// No schema available"; return generateCode(language, tableSchema); }, [language, tableSchema]); @@ -196,7 +231,7 @@ export function CodeGenerator({ if (!isOpen) return null; - const currentLang = LANGUAGES.find(l => l.id === language)!; + const currentLang = LANGUAGES.find((l) => l.id === language)!; return (
    @@ -207,9 +242,7 @@ export function CodeGenerator({ Code Generator {tableName} - {databaseType && ( - {databaseType} - )} + {databaseType && {databaseType}}
    {showLangDropdown && (
    - {LANGUAGES.map(lang => ( + {LANGUAGES.map((lang) => (
    {/* Footer */}

    - Generated from {tableName} • {tableSchema?.columns?.length || 0} columns • {currentLang.ext} format + Generated from {tableName} • {tableSchema?.columns?.length || 0}{" "} + columns • {currentLang.ext} format

    diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 785d3707..4d925a3f 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useEffect, useState, useMemo } from 'react'; +import React, { useEffect, useState, useMemo } from "react"; import { CommandDialog, CommandInput, @@ -9,7 +9,7 @@ import { CommandGroup, CommandItem, CommandShortcut, -} from '@/components/ui/command'; +} from "@/components/ui/command"; import { Table2, Play, @@ -23,10 +23,10 @@ import { Sparkles, AlignLeft, Save, -} from 'lucide-react'; -import { DatabaseConnection, TableSchema, SavedQuery, QueryHistoryItem } from '@/lib/types'; -import { storage } from '@/lib/storage'; -import { getDBIcon } from '@/lib/db-ui-config'; +} from "lucide-react"; +import { DatabaseConnection, TableSchema, SavedQuery, QueryHistoryItem } from "@/lib/types"; +import { storage } from "@/lib/storage"; +import { getDBIcon } from "@/lib/db-ui-config"; interface CommandPaletteProps { connections: DatabaseConnection[]; @@ -70,13 +70,13 @@ export function CommandPalette({ // Register Cmd+K / Ctrl+K keyboard shortcut useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); - setOpen(prev => !prev); + setOpen((prev) => !prev); } }; - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); }, []); // Load saved queries and history @@ -98,10 +98,7 @@ export function CommandPalette({ className="sm:max-w-[560px] bg-[#0a0a0a] border-white/10" showCloseButton={false} > - + No results found. @@ -154,10 +151,7 @@ export function CommandPalette({ {connections.map((conn) => { const Icon = getDBIcon(conn.type); return ( - runAction(() => onSelectConnection(conn))} - > + runAction(() => onSelectConnection(conn))}> {conn.name} {activeConnection?.id === conn.id && ( @@ -173,10 +167,7 @@ export function CommandPalette({ {schema.length > 0 && ( {schema.map((table) => ( - runAction(() => onTableClick(table.name))} - > + runAction(() => onTableClick(table.name))}> {table.name} @@ -192,10 +183,7 @@ export function CommandPalette({ {savedQueries.length > 0 && ( {savedQueries.map((sq: SavedQuery) => ( - runAction(() => onLoadSavedQuery(sq.query))} - > + runAction(() => onLoadSavedQuery(sq.query))}> {sq.name} @@ -210,10 +198,7 @@ export function CommandPalette({ {historyItems.length > 0 && ( {historyItems.map((item: QueryHistoryItem) => ( - runAction(() => onLoadHistoryQuery(item.query))} - > + runAction(() => onLoadHistoryQuery(item.query))}> {item.query.substring(0, 60)} {item.executionTime}ms diff --git a/src/components/ConnectionModal.tsx b/src/components/ConnectionModal.tsx index 740f25b5..b69d342c 100644 --- a/src/components/ConnectionModal.tsx +++ b/src/components/ConnectionModal.tsx @@ -1,17 +1,37 @@ "use client"; -import { Dialog, DialogContent, DialogTitle, DialogDescription } from '@/components/ui/dialog'; -import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@/components/ui/drawer'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { DatabaseConnection, ConnectionEnvironment, ENVIRONMENT_COLORS, ENVIRONMENT_LABELS, SSLMode } from '@/lib/types'; -import { Database, ShieldCheck, Zap, Globe, Key, Link, CheckCircle2, XCircle, ClipboardPaste, Lock, ChevronDown, Terminal, Settings2 } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { getDBConfig, isFileBased } from '@/lib/db-ui-config'; -import { motion, AnimatePresence } from 'framer-motion'; -import { useConnectionForm } from '@/hooks/use-connection-form'; -import { useIsMobile } from '@/hooks/use-mobile'; +import { Dialog, DialogContent, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from "@/components/ui/drawer"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + DatabaseConnection, + ConnectionEnvironment, + ENVIRONMENT_COLORS, + ENVIRONMENT_LABELS, + SSLMode, +} from "@/lib/types"; +import { + Database, + ShieldCheck, + Zap, + Globe, + Key, + Link, + CheckCircle2, + XCircle, + ClipboardPaste, + Lock, + ChevronDown, + Terminal, + Settings2, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { getDBConfig, isFileBased } from "@/lib/db-ui-config"; +import { motion, AnimatePresence } from "framer-motion"; +import { useConnectionForm } from "@/hooks/use-connection-form"; +import { useIsMobile } from "@/hooks/use-mobile"; interface ConnectionModalProps { isOpen: boolean; @@ -19,53 +39,91 @@ interface ConnectionModalProps { onConnect: (conn: DatabaseConnection) => void; editConnection?: DatabaseConnection | null; /** Optional API adapter: when provided, bypasses the built-in /api/db/test-connection fetch. */ - onTestConnection?: (connection: DatabaseConnection) => Promise<{ success: boolean; latency?: number; error?: string }>; + onTestConnection?: ( + connection: DatabaseConnection, + ) => Promise<{ success: boolean; latency?: number; error?: string }>; } -export function ConnectionModal({ isOpen, onClose, onConnect, editConnection, onTestConnection }: ConnectionModalProps) { +export function ConnectionModal({ + isOpen, + onClose, + onConnect, + editConnection, + onTestConnection, +}: ConnectionModalProps) { const isMobile = useIsMobile(); const { // Connection fields - type, setType, - name, setName, - host, setHost, - port, setPort, - user, setUser, - password, setPassword, - database, setDatabase, - connectionString, setConnectionString, - mongoConnectionMode, setMongoConnectionMode, - environment, setEnvironment, + type, + setType, + name, + setName, + host, + setHost, + port, + setPort, + user, + setUser, + password, + setPassword, + database, + setDatabase, + connectionString, + setConnectionString, + mongoConnectionMode, + setMongoConnectionMode, + environment, + setEnvironment, // UI state isTesting, - testResult, setTestResult, - pasteInput, setPasteInput, - showPasteInput, setShowPasteInput, + testResult, + setTestResult, + pasteInput, + setPasteInput, + showPasteInput, + setShowPasteInput, isEditMode, // SSL/TLS - showSSL, setShowSSL, - sslMode, setSSLMode, - caCert, setCaCert, - clientCert, setClientCert, - clientKey, setClientKey, + showSSL, + setShowSSL, + sslMode, + setSSLMode, + caCert, + setCaCert, + clientCert, + setClientCert, + clientKey, + setClientKey, // Advanced (Oracle/MSSQL) - showAdvanced, setShowAdvanced, - serviceName, setServiceName, - instanceName, setInstanceName, + showAdvanced, + setShowAdvanced, + serviceName, + setServiceName, + instanceName, + setInstanceName, // SSH Tunnel - showSSH, setShowSSH, - sshEnabled, setSSHEnabled, - sshHost, setSSHHost, - sshPort, setSSHPort, - sshUsername, setSSHUsername, - sshAuthMethod, setSSHAuthMethod, - sshPassword, setSSHPassword, - sshPrivateKey, setSSHPrivateKey, - sshPassphrase, setSSHPassphrase, + showSSH, + setShowSSH, + sshEnabled, + setSSHEnabled, + sshHost, + setSSHHost, + sshPort, + setSSHPort, + sshUsername, + setSSHUsername, + sshAuthMethod, + setSSHAuthMethod, + sshPassword, + setSSHPassword, + sshPrivateKey, + setSSHPrivateKey, + sshPassphrase, + setSSHPassphrase, // Handlers handleTestConnection, @@ -82,7 +140,7 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection, on
    @@ -95,12 +153,14 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection, on

    - {isEditMode ? 'Edit Connection' : 'New Connection'} + {isEditMode ? "Edit Connection" : "New Connection"}

    - {isEditMode ? 'Update your database connection parameters.' : 'Configure your database connection parameters securely.'} + {isEditMode + ? "Update your database connection parameters." + : "Configure your database connection parameters securely."}

    {!isEditMode && ( ))}
    @@ -209,10 +266,15 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection, on type === db.value ? "bg-blue-600/10 border-blue-500/50 shadow-[0_0_20px_rgba(59,130,246,0.1)]" : "bg-zinc-900/50 border-white/5 hover:border-white/10 hover:bg-zinc-900", - isEditMode && type !== db.value && "opacity-30 cursor-not-allowed" + isEditMode && type !== db.value && "opacity-30 cursor-not-allowed", )} > - + {db.label} @@ -220,155 +282,169 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection, on ))} -
    - <> - {/* Connection string mode toggle */} - {getDBConfig(type).showConnectionStringToggle && ( -
    - - +
    + <> + {/* Connection string mode toggle */} + {getDBConfig(type).showConnectionStringToggle && ( +
    + + +
    + )} + + {getDBConfig(type).showConnectionStringToggle && mongoConnectionMode === "connectionString" ? ( + <> +
    +
    + +
    - )} + setConnectionString(e.target.value)} + placeholder="mongodb://localhost:27017/mydb or mongodb+srv://..." + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" + /> +
    +
    +
    + + +
    + setDatabase(e.target.value)} + placeholder="Extracted from URI if not provided" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" + /> +
    + + ) : isFileBased(type) ? ( +
    +
    + + +
    + setDatabase(e.target.value)} + placeholder="/path/to/database file" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" + /> +
    + ) : ( + <> +
    +
    + + +
    +
    + setHost(e.target.value)} + placeholder="localhost" + className="md:col-span-3 h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs" + /> + setPort(e.target.value)} + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" + /> +
    +
    - {getDBConfig(type).showConnectionStringToggle && mongoConnectionMode === 'connectionString' ? ( - <> -
    -
    - - -
    - setConnectionString(e.target.value)} - placeholder="mongodb://localhost:27017/mydb or mongodb+srv://..." - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" - /> -
    -
    -
    - - -
    - setDatabase(e.target.value)} - placeholder="Extracted from URI if not provided" - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" - /> -
    - - ) : isFileBased(type) ? ( +
    - - + +
    setDatabase(e.target.value)} - placeholder="/path/to/database file" - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" + id="user" + value={user} + onChange={(e) => setUser(e.target.value)} + placeholder="postgres" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs" />
    - ) : ( - <> -
    -
    - - -
    -
    - setHost(e.target.value)} - placeholder="localhost" - className="md:col-span-3 h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs" - /> - setPort(e.target.value)} - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" - /> -
    -
    - -
    -
    -
    - - -
    - setUser(e.target.value)} - placeholder="postgres" - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs" - /> -
    -
    -
    - - -
    - setPassword(e.target.value)} - placeholder="••••••••" - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs" - /> -
    +
    +
    + +
    + setPassword(e.target.value)} + placeholder="••••••••" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs" + /> +
    +
    -
    -
    - - -
    - setDatabase(e.target.value)} - placeholder="production_db" - className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" - /> -
    - - )} - -
    +
    +
    + + +
    + setDatabase(e.target.value)} + placeholder="production_db" + className="h-10 bg-zinc-900/50 border-white/5 focus:border-blue-500/50 transition-all text-xs font-mono" + /> +
    + + )} + +
    {/* Advanced Settings (Oracle/MSSQL) */} - {(type === 'oracle' || type === 'mssql') && ( + {(type === "oracle" || type === "mssql") && (
    - {sslMode !== 'disable' && ( + {sslMode !== "disable" && (
    @@ -487,7 +563,7 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection, on className="w-full rounded-md bg-zinc-900/50 border border-white/5 focus:border-emerald-500/50 text-xs font-mono text-zinc-300 p-2 resize-none placeholder:text-zinc-600" />
    - {(sslMode === 'verify-ca' || sslMode === 'verify-full') && ( + {(sslMode === "verify-ca" || sslMode === "verify-full") && ( <>
    @@ -537,7 +613,7 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection, on {showSSH && ( @@ -586,31 +662,31 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection, on
    - {sshAuthMethod === 'password' ? ( + {sshAuthMethod === "password" ? (
    -
    +
    {testResult.success ? ( ) : ( @@ -705,12 +783,17 @@ export function ConnectionModal({ isOpen, onClose, onConnect, editConnection, on Testing...
    ) : ( - 'Test Connection' + "Test Connection" )}
    - + setTableName(e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '_'))} + onChange={(e) => setTableName(e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, "_"))} className="bg-zinc-900 border-white/5 focus-visible:ring-blue-500/50" />
    @@ -160,9 +146,9 @@ export function CreateTableModal({ isOpen, onClose, onTableCreated }: CreateTabl
    - - diff --git a/src/components/DataCharts.tsx b/src/components/DataCharts.tsx index 672a27a1..0e977bdc 100644 --- a/src/components/DataCharts.tsx +++ b/src/components/DataCharts.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useMemo, useRef, useCallback } from 'react'; +import React, { useState, useMemo, useRef, useCallback } from "react"; import { BarChart, Bar, @@ -20,9 +20,9 @@ import { Legend, Cell, ResponsiveContainer, -} from 'recharts'; -import { QueryResult } from '@/lib/types'; -import { cn } from '@/lib/utils'; +} from "recharts"; +import { QueryResult } from "@/lib/types"; +import { cn } from "@/lib/utils"; import { BarChart3, LineChart as LineChartIcon, @@ -40,43 +40,37 @@ import { Save, FolderOpen, X, -} from 'lucide-react'; -import { Button } from '@/components/ui/button'; +} from "lucide-react"; +import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { storage } from '@/lib/storage'; +} from "@/components/ui/dropdown-menu"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { storage } from "@/lib/storage"; // Chart colors matching CSS variables const CHART_COLORS = [ - 'hsl(217, 91%, 60%)', // Blue - 'hsl(142, 71%, 45%)', // Green - 'hsl(38, 92%, 50%)', // Amber - 'hsl(270, 91%, 65%)', // Purple - 'hsl(330, 81%, 60%)', // Pink - 'hsl(199, 89%, 48%)', // Cyan - 'hsl(24, 95%, 53%)', // Orange - 'hsl(162, 63%, 41%)', // Teal + "hsl(217, 91%, 60%)", // Blue + "hsl(142, 71%, 45%)", // Green + "hsl(38, 92%, 50%)", // Amber + "hsl(270, 91%, 65%)", // Purple + "hsl(330, 81%, 60%)", // Pink + "hsl(199, 89%, 48%)", // Cyan + "hsl(24, 95%, 53%)", // Orange + "hsl(162, 63%, 41%)", // Teal ]; -type ChartType = 'bar' | 'line' | 'pie' | 'area' | 'scatter' | 'histogram' | 'stacked-bar' | 'stacked-area'; +type ChartType = "bar" | "line" | "pie" | "area" | "scatter" | "histogram" | "stacked-bar" | "stacked-area"; -export type AggregationType = 'none' | 'sum' | 'avg' | 'count' | 'min' | 'max'; -export type DateGrouping = 'hour' | 'day' | 'week' | 'month' | 'year'; +export type AggregationType = "none" | "sum" | "avg" | "count" | "min" | "max"; +export type DateGrouping = "hour" | "day" | "week" | "month" | "year"; export interface FieldAnalysis { name: string; - type: 'numeric' | 'categorical' | 'date' | 'unknown'; + type: "numeric" | "categorical" | "date" | "unknown"; uniqueValues: number; hasNulls: boolean; sample: unknown; @@ -97,29 +91,30 @@ interface DataChartsProps { } export function analyzeField(name: string, values: unknown[]): FieldAnalysis { - const nonNullValues = values.filter(v => v !== null && v !== undefined); + const nonNullValues = values.filter((v) => v !== null && v !== undefined); const uniqueValues = new Set(nonNullValues).size; const sample = nonNullValues[0]; // Check if numeric - const numericCount = nonNullValues.filter(v => typeof v === 'number' || (typeof v === 'string' && !isNaN(Number(v)))).length; + const numericCount = nonNullValues.filter( + (v) => typeof v === "number" || (typeof v === "string" && !isNaN(Number(v))), + ).length; const isNumeric = numericCount > nonNullValues.length * 0.8; // Check if date const datePatterns = [ - /^\d{4}-\d{2}-\d{2}/, // ISO date + /^\d{4}-\d{2}-\d{2}/, // ISO date /^\d{2}\/\d{2}\/\d{4}/, // US date /^\d{2}\.\d{2}\.\d{4}/, // EU date ]; - const isDate = nonNullValues.some(v => - (typeof v === 'string' && datePatterns.some(p => p.test(v))) || - v instanceof Date + const isDate = nonNullValues.some( + (v) => (typeof v === "string" && datePatterns.some((p) => p.test(v))) || v instanceof Date, ); - let type: FieldAnalysis['type'] = 'unknown'; - if (isDate) type = 'date'; - else if (isNumeric) type = 'numeric'; - else if (uniqueValues <= 50) type = 'categorical'; + let type: FieldAnalysis["type"] = "unknown"; + if (isDate) type = "date"; + else if (isNumeric) type = "numeric"; + else if (uniqueValues <= 50) type = "categorical"; return { name, @@ -137,9 +132,9 @@ export function analyzeData(result: QueryResult | null): DataAnalysis { numericFields: [], categoricalFields: [], dateFields: [], - suggestedChartType: 'bar', + suggestedChartType: "bar", isVisualizable: false, - reason: 'No data to visualize', + reason: "No data to visualize", }; } @@ -149,20 +144,23 @@ export function analyzeData(result: QueryResult | null): DataAnalysis { numericFields: [], categoricalFields: [], dateFields: [], - suggestedChartType: 'bar', + suggestedChartType: "bar", isVisualizable: false, - reason: 'Need at least 2 rows for visualization', + reason: "Need at least 2 rows for visualization", }; } const fieldNames = result.fields || Object.keys(result.rows[0]); - const fields = fieldNames.map(name => - analyzeField(name, result.rows.map(row => row[name])) + const fields = fieldNames.map((name) => + analyzeField( + name, + result.rows.map((row) => row[name]), + ), ); - const numericFields = fields.filter(f => f.type === 'numeric').map(f => f.name); - const categoricalFields = fields.filter(f => f.type === 'categorical').map(f => f.name); - const dateFields = fields.filter(f => f.type === 'date').map(f => f.name); + const numericFields = fields.filter((f) => f.type === "numeric").map((f) => f.name); + const categoricalFields = fields.filter((f) => f.type === "categorical").map((f) => f.name); + const dateFields = fields.filter((f) => f.type === "date").map((f) => f.name); if (numericFields.length === 0) { return { @@ -170,23 +168,23 @@ export function analyzeData(result: QueryResult | null): DataAnalysis { numericFields, categoricalFields, dateFields, - suggestedChartType: 'bar', + suggestedChartType: "bar", isVisualizable: false, - reason: 'No numeric fields found for Y-axis', + reason: "No numeric fields found for Y-axis", }; } // Suggest chart type based on data - let suggestedChartType: ChartType = 'bar'; + let suggestedChartType: ChartType = "bar"; if (dateFields.length > 0) { - suggestedChartType = 'line'; // Time series → line chart + suggestedChartType = "line"; // Time series → line chart } else if (numericFields.length >= 2 && categoricalFields.length === 0) { - suggestedChartType = 'scatter'; // 2+ numeric, no categorical → scatter + suggestedChartType = "scatter"; // 2+ numeric, no categorical → scatter } else if (categoricalFields.length > 0 && result.rows.length <= 10) { - suggestedChartType = 'pie'; // Few categories → pie chart + suggestedChartType = "pie"; // Few categories → pie chart } else if (categoricalFields.length > 0) { - suggestedChartType = 'bar'; // Many categories → bar chart + suggestedChartType = "bar"; // Many categories → bar chart } return { @@ -201,10 +199,10 @@ export function analyzeData(result: QueryResult | null): DataAnalysis { export function formatNumber(value: number): string { if (Math.abs(value) >= 1000000) { - return (value / 1000000).toFixed(1) + 'M'; + return (value / 1000000).toFixed(1) + "M"; } if (Math.abs(value) >= 1000) { - return (value / 1000).toFixed(1) + 'K'; + return (value / 1000).toFixed(1) + "K"; } return value.toLocaleString(); } @@ -235,7 +233,10 @@ const CustomTooltip = ({ active, payload, label }: TooltipProps) => { }; // Histogram bin calculation -export function computeHistogramBins(values: number[], buckets: number): { range: string; count: number; min: number; max: number }[] { +export function computeHistogramBins( + values: number[], + buckets: number, +): { range: string; count: number; min: number; max: number }[] { if (values.length === 0) return []; const min = Math.min(...values); const max = Math.max(...values); @@ -247,7 +248,7 @@ export function computeHistogramBins(values: number[], buckets: number): { range min: min + i * binWidth, max: min + (i + 1) * binWidth, })); - values.forEach(v => { + values.forEach((v) => { let idx = Math.floor((v - min) / binWidth); if (idx >= buckets) idx = buckets - 1; bins[idx].count++; @@ -260,13 +261,13 @@ export function aggregateData( rows: Record[], groupByField: string, metrics: { field: string; aggregation: AggregationType }[], - dateGrouping?: DateGrouping + dateGrouping?: DateGrouping, ): Record[] { - if (metrics.every(m => m.aggregation === 'none')) return rows; + if (metrics.every((m) => m.aggregation === "none")) return rows; const groups = new Map[]>(); - rows.forEach(row => { - let key = String(row[groupByField] ?? ''); + rows.forEach((row) => { + let key = String(row[groupByField] ?? ""); if (dateGrouping && key) { key = groupByDate(key, dateGrouping); } @@ -277,14 +278,25 @@ export function aggregateData( return Array.from(groups.entries()).map(([key, groupRows]) => { const result: Record = { [groupByField]: key }; metrics.forEach(({ field, aggregation }) => { - const values = groupRows.map(r => Number(r[field]) || 0); + const values = groupRows.map((r) => Number(r[field]) || 0); switch (aggregation) { - case 'sum': result[field] = values.reduce((a, b) => a + b, 0); break; - case 'avg': result[field] = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; break; - case 'count': result[field] = values.length; break; - case 'min': result[field] = Math.min(...values); break; - case 'max': result[field] = Math.max(...values); break; - default: result[field] = values[0]; + case "sum": + result[field] = values.reduce((a, b) => a + b, 0); + break; + case "avg": + result[field] = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; + break; + case "count": + result[field] = values.length; + break; + case "min": + result[field] = Math.min(...values); + break; + case "max": + result[field] = Math.max(...values); + break; + default: + result[field] = values[0]; } }); return result; @@ -295,12 +307,21 @@ export function groupByDate(dateStr: string, grouping: DateGrouping): string { const date = new Date(dateStr); if (isNaN(date.getTime())) return dateStr; switch (grouping) { - case 'hour': return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')} ${String(date.getHours()).padStart(2,'0')}:00`; - case 'day': return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`; - case 'week': { const d = new Date(date); d.setDate(d.getDate() - d.getDay()); return `W${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; } - case 'month': return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}`; - case 'year': return `${date.getFullYear()}`; - default: return dateStr; + case "hour": + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:00`; + case "day": + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; + case "week": { + const d = new Date(date); + d.setDate(d.getDate() - d.getDay()); + return `W${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; + } + case "month": + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`; + case "year": + return `${date.getFullYear()}`; + default: + return dateStr; } } @@ -309,31 +330,43 @@ export function DataCharts({ result }: DataChartsProps) { const analysis = useMemo(() => analyzeData(result), [result]); const [chartType, setChartType] = useState(analysis.suggestedChartType); - const [xAxis, setXAxis] = useState(''); + const [xAxis, setXAxis] = useState(""); const [yAxis, setYAxis] = useState([]); - const [scatterY, setScatterY] = useState(''); + const [scatterY, setScatterY] = useState(""); const [histogramBuckets, setHistogramBuckets] = useState(10); - const [aggregation, setAggregation] = useState('none'); - const [dateGrouping, setDateGrouping] = useState(''); + const [aggregation, setAggregation] = useState("none"); + const [dateGrouping, setDateGrouping] = useState(""); // Saved charts state - const [savedCharts, setSavedCharts] = useState<{ id: string; name: string; chartType: ChartType; xAxis: string; yAxis: string[]; aggregation: AggregationType; dateGrouping: string }[]>([]); + const [savedCharts, setSavedCharts] = useState< + { + id: string; + name: string; + chartType: ChartType; + xAxis: string; + yAxis: string[]; + aggregation: AggregationType; + dateGrouping: string; + }[] + >([]); const [showSaveDialog, setShowSaveDialog] = useState(false); - const [saveName, setSaveName] = useState(''); + const [saveName, setSaveName] = useState(""); // Load saved charts from storage React.useEffect(() => { const charts = storage.getSavedCharts(); if (charts.length > 0) { - setSavedCharts(charts.map(c => ({ - id: c.id, - name: c.name, - chartType: c.chartType as ChartType, - xAxis: c.xAxis, - yAxis: c.yAxis, - aggregation: (c.aggregation || 'none') as AggregationType, - dateGrouping: c.dateGrouping || '', - }))); + setSavedCharts( + charts.map((c) => ({ + id: c.id, + name: c.name, + chartType: c.chartType as ChartType, + xAxis: c.xAxis, + yAxis: c.yAxis, + aggregation: (c.aggregation || "none") as AggregationType, + dateGrouping: c.dateGrouping || "", + })), + ); } }, []); @@ -342,7 +375,7 @@ export function DataCharts({ result }: DataChartsProps) { if (analysis.isVisualizable) { setChartType(analysis.suggestedChartType); - const defaultX = analysis.categoricalFields[0] || analysis.dateFields[0] || analysis.fields[0]?.name || ''; + const defaultX = analysis.categoricalFields[0] || analysis.dateFields[0] || analysis.fields[0]?.name || ""; setXAxis(defaultX); if (analysis.numericFields.length > 0) { @@ -358,38 +391,38 @@ export function DataCharts({ result }: DataChartsProps) { if (!result?.rows) return []; // Histogram: special data preparation - if (chartType === 'histogram' && yAxis.length > 0) { - const values = result.rows.map(r => Number(r[yAxis[0]]) || 0).filter(v => !isNaN(v)); + if (chartType === "histogram" && yAxis.length > 0) { + const values = result.rows.map((r) => Number(r[yAxis[0]]) || 0).filter((v) => !isNaN(v)); return computeHistogramBins(values, histogramBuckets); } // Scatter: needs both axes as numeric - if (chartType === 'scatter') { + if (chartType === "scatter") { if (!xAxis || !scatterY) return []; - return result.rows.map(row => ({ - [xAxis]: typeof row[xAxis] === 'number' ? row[xAxis] : Number(row[xAxis]) || 0, - [scatterY]: typeof row[scatterY] === 'number' ? row[scatterY] : Number(row[scatterY]) || 0, + return result.rows.map((row) => ({ + [xAxis]: typeof row[xAxis] === "number" ? row[xAxis] : Number(row[xAxis]) || 0, + [scatterY]: typeof row[scatterY] === "number" ? row[scatterY] : Number(row[scatterY]) || 0, })); } if (!xAxis) return []; - const baseData = result.rows.map(row => { + const baseData = result.rows.map((row) => { const dataPoint: Record = { [xAxis]: row[xAxis] }; - yAxis.forEach(field => { + yAxis.forEach((field) => { const value = row[field]; - dataPoint[field] = typeof value === 'number' ? value : Number(value) || 0; + dataPoint[field] = typeof value === "number" ? value : Number(value) || 0; }); return dataPoint; }); // Apply aggregation if set - if (aggregation !== 'none' && yAxis.length > 0) { + if (aggregation !== "none" && yAxis.length > 0) { return aggregateData( baseData, xAxis, - yAxis.map(f => ({ field: f, aggregation })), - dateGrouping || undefined + yAxis.map((f) => ({ field: f, aggregation })), + dateGrouping || undefined, ); } @@ -398,8 +431,8 @@ export function DataCharts({ result }: DataChartsProps) { return aggregateData( baseData, xAxis, - yAxis.map(f => ({ field: f, aggregation: 'sum' })), - dateGrouping + yAxis.map((f) => ({ field: f, aggregation: "sum" })), + dateGrouping, ); } @@ -416,7 +449,7 @@ export function DataCharts({ result }: DataChartsProps) { xAxis, yAxis: [...yAxis], aggregation, - dateGrouping: dateGrouping || '', + dateGrouping: dateGrouping || "", }; const updated = [...savedCharts, newChart]; setSavedCharts(updated); @@ -431,52 +464,58 @@ export function DataCharts({ result }: DataChartsProps) { createdAt: new Date(), }); setShowSaveDialog(false); - setSaveName(''); + setSaveName(""); }, [saveName, chartType, xAxis, yAxis, aggregation, dateGrouping, savedCharts]); // Load saved chart config - const loadSavedChart = useCallback((chart: typeof savedCharts[0]) => { + const loadSavedChart = useCallback((chart: (typeof savedCharts)[0]) => { setChartType(chart.chartType); setXAxis(chart.xAxis); setYAxis(chart.yAxis); setAggregation(chart.aggregation); - setDateGrouping((chart.dateGrouping || '') as DateGrouping | ''); + setDateGrouping((chart.dateGrouping || "") as DateGrouping | ""); }, []); // Delete saved chart - const deleteSavedChart = useCallback((id: string) => { - const updated = savedCharts.filter(c => c.id !== id); - setSavedCharts(updated); - storage.deleteChart(id); - }, [savedCharts]); + const deleteSavedChart = useCallback( + (id: string) => { + const updated = savedCharts.filter((c) => c.id !== id); + setSavedCharts(updated); + storage.deleteChart(id); + }, + [savedCharts], + ); - const exportChart = useCallback(async (format: 'png' | 'svg') => { + const exportChart = useCallback(async (format: "png" | "svg") => { if (!chartRef.current) return; - if (format === 'png') { + if (format === "png") { try { // Dynamic import for html2canvas - const html2canvasModule = await import('html2canvas'); - const html2canvas = html2canvasModule.default as (element: HTMLElement, options?: { backgroundColor?: string; scale?: number }) => Promise; + const html2canvasModule = await import("html2canvas"); + const html2canvas = html2canvasModule.default as ( + element: HTMLElement, + options?: { backgroundColor?: string; scale?: number }, + ) => Promise; const canvas = await html2canvas(chartRef.current, { - backgroundColor: '#080808', + backgroundColor: "#080808", scale: 2, }); - const link = document.createElement('a'); + const link = document.createElement("a"); link.download = `chart_${Date.now()}.png`; - link.href = canvas.toDataURL('image/png'); + link.href = canvas.toDataURL("image/png"); link.click(); } catch (error) { - console.error('Failed to export PNG:', error); + console.error("Failed to export PNG:", error); } } else { // SVG export - find the SVG element - const svgElement = chartRef.current.querySelector('svg'); + const svgElement = chartRef.current.querySelector("svg"); if (svgElement) { const svgData = new XMLSerializer().serializeToString(svgElement); - const blob = new Blob([svgData], { type: 'image/svg+xml' }); + const blob = new Blob([svgData], { type: "image/svg+xml" }); const url = URL.createObjectURL(blob); - const link = document.createElement('a'); + const link = document.createElement("a"); link.download = `chart_${Date.now()}.svg`; link.href = url; link.click(); @@ -486,9 +525,9 @@ export function DataCharts({ result }: DataChartsProps) { }, []); const toggleYAxis = (field: string) => { - setYAxis(prev => { + setYAxis((prev) => { if (prev.includes(field)) { - return prev.filter(f => f !== field); + return prev.filter((f) => f !== field); } return [...prev, field]; }); @@ -506,22 +545,26 @@ export function DataCharts({ result }: DataChartsProps) { } const chartTypes: { type: ChartType; icon: React.ReactNode; label: string }[] = [ - { type: 'bar', icon: , label: 'Bar' }, - { type: 'line', icon: , label: 'Line' }, - { type: 'pie', icon: , label: 'Pie' }, - { type: 'area', icon: , label: 'Area' }, - { type: 'scatter', icon: , label: 'Scatter' }, - { type: 'histogram', icon: , label: 'Histogram' }, - { type: 'stacked-bar', icon: , label: 'Stacked' }, - { type: 'stacked-area', icon: , label: 'Stack Area' }, + { type: "bar", icon: , label: "Bar" }, + { type: "line", icon: , label: "Line" }, + { type: "pie", icon: , label: "Pie" }, + { type: "area", icon: , label: "Area" }, + { type: "scatter", icon: , label: "Scatter" }, + { type: "histogram", icon: , label: "Histogram" }, + { type: "stacked-bar", icon: , label: "Stacked" }, + { type: "stacked-area", icon: , label: "Stack Area" }, ]; - const getFieldIcon = (type: FieldAnalysis['type']) => { + const getFieldIcon = (type: FieldAnalysis["type"]) => { switch (type) { - case 'numeric': return ; - case 'date': return ; - case 'categorical': return ; - default: return ; + case "numeric": + return ; + case "date": + return ; + case "categorical": + return ; + default: + return ; } }; @@ -537,9 +580,7 @@ export function DataCharts({ result }: DataChartsProps) { onClick={() => setChartType(type)} className={cn( "flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all", - chartType === type - ? "bg-blue-600 text-white" - : "text-zinc-500 hover:text-zinc-300 hover:bg-white/5" + chartType === type ? "bg-blue-600 text-white" : "text-zinc-500 hover:text-zinc-300 hover:bg-white/5", )} title={label} > @@ -552,7 +593,7 @@ export function DataCharts({ result }: DataChartsProps) {
    {/* X-Axis Selector */} - {chartType !== 'pie' && ( + {chartType !== "pie" && (
    X-Axis @@ -613,16 +649,20 @@ export function DataCharts({ result }: DataChartsProps) { - {analysis.numericFields.filter(f => f !== xAxis).map(field => ( - {field} - ))} + {analysis.numericFields + .filter((f) => f !== xAxis) + .map((field) => ( + + {field} + + ))}
    )} {/* Histogram buckets */} - {chartType === 'histogram' && ( + {chartType === "histogram" && (
    Buckets @@ -639,7 +681,7 @@ export function DataCharts({ result }: DataChartsProps) { )} {/* Aggregation */} - {chartType !== 'scatter' && chartType !== 'histogram' && chartType !== 'pie' && ( + {chartType !== "scatter" && chartType !== "histogram" && chartType !== "pie" && (
    Agg @@ -656,17 +700,24 @@ export function DataCharts({ result }: DataChartsProps) { )} {/* Date Grouping */} - {analysis.dateFields.length > 0 && chartType !== 'scatter' && chartType !== 'histogram' && ( + {analysis.dateFields.length > 0 && chartType !== "scatter" && chartType !== "histogram" && (
    Group - setDateGrouping(v === "none" ? "" : (v as DateGrouping))} + > - None - {(['hour', 'day', 'week', 'month', 'year'] as const).map(g => ( - {g} + + None + + {(["hour", "day", "week", "month", "year"] as const).map((g) => ( + + {g} + ))} @@ -684,16 +735,30 @@ export function DataCharts({ result }: DataChartsProps) { placeholder="Chart name..." value={saveName} onChange={(e) => setSaveName(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSaveChart()} + onKeyDown={(e) => e.key === "Enter" && handleSaveChart()} className="h-7 px-2 text-xs bg-white/5 border border-white/10 rounded text-zinc-300 focus:outline-none focus:border-blue-500" autoFocus /> - - + +
    ) : (
    - {savedCharts.length > 0 && ( @@ -704,10 +769,21 @@ export function DataCharts({ result }: DataChartsProps) { - {savedCharts.map(chart => ( - - loadSavedChart(chart)}>{chart.name} ({chart.chartType}) - @@ -726,10 +802,10 @@ export function DataCharts({ result }: DataChartsProps) { - exportChart('png')} className="text-xs cursor-pointer"> + exportChart("png")} className="text-xs cursor-pointer"> Export as PNG - exportChart('svg')} className="text-xs cursor-pointer"> + exportChart("svg")} className="text-xs cursor-pointer"> Export as SVG @@ -744,20 +820,11 @@ export function DataCharts({ result }: DataChartsProps) {
    ) : ( - {chartType === 'bar' ? ( + {chartType === "bar" ? ( - - + + } /> {yAxis.map((field, index) => ( @@ -769,20 +836,11 @@ export function DataCharts({ result }: DataChartsProps) { /> ))} - ) : chartType === 'line' ? ( + ) : chartType === "line" ? ( - - + + } /> {yAxis.map((field, index) => ( @@ -797,20 +855,11 @@ export function DataCharts({ result }: DataChartsProps) { /> ))} - ) : chartType === 'area' ? ( + ) : chartType === "area" ? ( - - + + } /> {yAxis.map((field, index) => ( @@ -825,88 +874,54 @@ export function DataCharts({ result }: DataChartsProps) { /> ))} - ) : chartType === 'scatter' ? ( + ) : chartType === "scatter" ? ( - } cursor={{ strokeDasharray: '3 3' }} /> - + } cursor={{ strokeDasharray: "3 3" }} /> + - ) : chartType === 'histogram' ? ( + ) : chartType === "histogram" ? ( - + } /> - ) : chartType === 'stacked-bar' ? ( + ) : chartType === "stacked-bar" ? ( - - + + } /> {yAxis.map((field, index) => ( - + ))} - ) : chartType === 'stacked-area' ? ( + ) : chartType === "stacked-area" ? ( - - + + } /> {yAxis.map((field, index) => ( @@ -931,13 +946,10 @@ export function DataCharts({ result }: DataChartsProps) { cy="50%" outerRadius="70%" label={({ name, percent }) => `${name} (${(percent * 100).toFixed(0)}%)`} - labelLine={{ stroke: '#444' }} + labelLine={{ stroke: "#444" }} > {chartData.slice(0, 10).map((_entry, index) => ( - + ))} } /> @@ -950,12 +962,16 @@ export function DataCharts({ result }: DataChartsProps) { {/* Footer Stats */}
    - Rows: {result?.rows.length || 0} - Fields: {analysis.fields.length} - Numeric: {analysis.numericFields.length} - {chartType === 'pie' && chartData.length > 10 && ( - Showing top 10 values - )} + + Rows: {result?.rows.length || 0} + + + Fields: {analysis.fields.length} + + + Numeric: {analysis.numericFields.length} + + {chartType === "pie" && chartData.length > 10 && Showing top 10 values}
    ); diff --git a/src/components/DataImportModal.tsx b/src/components/DataImportModal.tsx index 4722ffa3..01d3a7f4 100644 --- a/src/components/DataImportModal.tsx +++ b/src/components/DataImportModal.tsx @@ -1,15 +1,10 @@ "use client"; -import React, { useState, useCallback, useRef, useMemo } from 'react'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { cn } from '@/lib/utils'; +import React, { useState, useCallback, useRef, useMemo } from "react"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; import { Upload, FileSpreadsheet, @@ -21,8 +16,8 @@ import { ArrowRight, Loader2, X, -} from 'lucide-react'; -import type { TableSchema } from '@/lib/types'; +} from "lucide-react"; +import type { TableSchema } from "@/lib/types"; interface DataImportModalProps { isOpen: boolean; @@ -38,16 +33,16 @@ export interface ParsedData { totalRows: number; } -type ImportStep = 'upload' | 'preview' | 'configure' | 'ready'; +type ImportStep = "upload" | "preview" | "configure" | "ready"; export function parseCSV(text: string): ParsedData { - const lines = text.split(/\r?\n/).filter(line => line.trim()); + const lines = text.split(/\r?\n/).filter((line) => line.trim()); if (lines.length === 0) return { headers: [], rows: [], totalRows: 0 }; // Parse CSV with basic quote handling const parseLine = (line: string): string[] => { const result: string[] = []; - let current = ''; + let current = ""; let inQuotes = false; for (let i = 0; i < line.length; i++) { @@ -61,9 +56,9 @@ export function parseCSV(text: string): ParsedData { } else { inQuotes = false; } - } else if (ch === ',' && !inQuotes) { + } else if (ch === "," && !inQuotes) { result.push(current.trim()); - current = ''; + current = ""; } else { current += ch; } @@ -73,7 +68,7 @@ export function parseCSV(text: string): ParsedData { }; const headers = parseLine(lines[0]); - const rows = lines.slice(1).map(line => parseLine(line)); + const rows = lines.slice(1).map((line) => parseLine(line)); return { headers, rows, totalRows: rows.length }; } @@ -82,35 +77,37 @@ export function parseJSON(text: string): ParsedData { const arr = Array.isArray(data) ? data : [data]; if (arr.length === 0) return { headers: [], rows: [], totalRows: 0 }; - const headers = [...new Set(arr.flatMap(obj => Object.keys(obj)))]; - const rows = arr.map(obj => headers.map(h => { - const val = obj[h]; - if (val === null || val === undefined) return ''; - if (typeof val === 'object') return JSON.stringify(val); - return String(val); - })); + const headers = [...new Set(arr.flatMap((obj) => Object.keys(obj)))]; + const rows = arr.map((obj) => + headers.map((h) => { + const val = obj[h]; + if (val === null || val === undefined) return ""; + if (typeof val === "object") return JSON.stringify(val); + return String(val); + }), + ); return { headers, rows, totalRows: rows.length }; } export function inferSqlType(values: string[]): string { - const nonEmpty = values.filter(v => v !== '' && v !== null); - if (nonEmpty.length === 0) return 'TEXT'; + const nonEmpty = values.filter((v) => v !== "" && v !== null); + if (nonEmpty.length === 0) return "TEXT"; - const allIntegers = nonEmpty.every(v => /^-?\d+$/.test(v)); - if (allIntegers) return 'INTEGER'; + const allIntegers = nonEmpty.every((v) => /^-?\d+$/.test(v)); + if (allIntegers) return "INTEGER"; - const allNumbers = nonEmpty.every(v => /^-?\d+(\.\d+)?$/.test(v)); - if (allNumbers) return 'NUMERIC'; + const allNumbers = nonEmpty.every((v) => /^-?\d+(\.\d+)?$/.test(v)); + if (allNumbers) return "NUMERIC"; - const allBooleans = nonEmpty.every(v => /^(true|false|0|1)$/i.test(v)); - if (allBooleans) return 'BOOLEAN'; + const allBooleans = nonEmpty.every((v) => /^(true|false|0|1)$/i.test(v)); + if (allBooleans) return "BOOLEAN"; - return 'TEXT'; + return "TEXT"; } export function escapeSQL(value: string): string { - if (value === '' || value === 'null' || value === 'NULL') return 'NULL'; + if (value === "" || value === "null" || value === "NULL") return "NULL"; return `'${value.replace(/'/g, "''")}'`; } @@ -121,71 +118,69 @@ export function generateImportSQL( newTableName: string, columnMapping: Record, ): string { - if (!parsedData) return ''; + if (!parsedData) return ""; - const tableName = createNewTable ? (newTableName || 'imported_data') : targetTable; - if (!tableName) return ''; + const tableName = createNewTable ? newTableName || "imported_data" : targetTable; + if (!tableName) return ""; const statements: string[] = []; // CREATE TABLE if new if (createNewTable) { - const colDefs = parsedData.headers.map(h => { - const colValues = parsedData.rows.slice(0, 100).map(r => r[parsedData.headers.indexOf(h)]); + const colDefs = parsedData.headers.map((h) => { + const colValues = parsedData.rows.slice(0, 100).map((r) => r[parsedData.headers.indexOf(h)]); const sqlType = inferSqlType(colValues); const colName = columnMapping[h] || h; return ` ${colName} ${sqlType}`; }); - statements.push(`CREATE TABLE ${tableName} (\n${colDefs.join(',\n')}\n);`); + statements.push(`CREATE TABLE ${tableName} (\n${colDefs.join(",\n")}\n);`); } // INSERT statements (batch in groups of 100) - const mappedHeaders = parsedData.headers.map(h => columnMapping[h] || h); + const mappedHeaders = parsedData.headers.map((h) => columnMapping[h] || h); const batchSize = 100; for (let i = 0; i < parsedData.rows.length; i += batchSize) { const batch = parsedData.rows.slice(i, i + batchSize); - const valueRows = batch.map(row => { + const valueRows = batch.map((row) => { const values = row.map((val, idx) => { - const sqlType = inferSqlType(parsedData.rows.slice(0, 100).map(r => r[idx])); - if (val === '' || val === 'NULL' || val === 'null') return 'NULL'; - if (sqlType === 'INTEGER' || sqlType === 'NUMERIC' || sqlType === 'BOOLEAN') { - if (sqlType === 'BOOLEAN') return val.toLowerCase() === 'true' || val === '1' ? 'TRUE' : 'FALSE'; + const sqlType = inferSqlType(parsedData.rows.slice(0, 100).map((r) => r[idx])); + if (val === "" || val === "NULL" || val === "null") return "NULL"; + if (sqlType === "INTEGER" || sqlType === "NUMERIC" || sqlType === "BOOLEAN") { + if (sqlType === "BOOLEAN") return val.toLowerCase() === "true" || val === "1" ? "TRUE" : "FALSE"; return val; } return escapeSQL(val); }); - return ` (${values.join(', ')})`; + return ` (${values.join(", ")})`; }); - statements.push( - `INSERT INTO ${tableName} (${mappedHeaders.join(', ')})\nVALUES\n${valueRows.join(',\n')};` - ); + statements.push(`INSERT INTO ${tableName} (${mappedHeaders.join(", ")})\nVALUES\n${valueRows.join(",\n")};`); } - return statements.join('\n\n'); + return statements.join("\n\n"); } export function DataImportModal({ isOpen, onClose, onImport, tables, databaseType }: DataImportModalProps) { - const [step, setStep] = useState('upload'); + const [step, setStep] = useState("upload"); const [parsedData, setParsedData] = useState(null); - const [fileName, setFileName] = useState(''); - const [fileType, setFileType] = useState<'csv' | 'json'>('csv'); - const [targetTable, setTargetTable] = useState(''); + const [fileName, setFileName] = useState(""); + const [fileType, setFileType] = useState<"csv" | "json">("csv"); + const [targetTable, setTargetTable] = useState(""); const [createNewTable, setCreateNewTable] = useState(false); - const [newTableName, setNewTableName] = useState(''); + const [newTableName, setNewTableName] = useState(""); const [columnMapping, setColumnMapping] = useState>({}); const [error, setError] = useState(null); const [isImporting, setIsImporting] = useState(false); const fileInputRef = useRef(null); const resetState = useCallback(() => { - setStep('upload'); + setStep("upload"); setParsedData(null); - setFileName(''); - setTargetTable(''); + setFileName(""); + setTargetTable(""); setCreateNewTable(false); - setNewTableName(''); + setNewTableName(""); setColumnMapping({}); setError(null); setIsImporting(false); @@ -200,9 +195,9 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp setError(null); setFileName(file.name); - const ext = file.name.split('.').pop()?.toLowerCase(); - const isJSON = ext === 'json'; - setFileType(isJSON ? 'json' : 'csv'); + const ext = file.name.split(".").pop()?.toLowerCase(); + const isJSON = ext === "json"; + setFileType(isJSON ? "json" : "csv"); const reader = new FileReader(); reader.onload = (e) => { @@ -211,28 +206,33 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp const data = isJSON ? parseJSON(text) : parseCSV(text); if (data.headers.length === 0) { - setError('No data found in file'); + setError("No data found in file"); return; } setParsedData(data); // Auto-map columns 1:1 const mapping: Record = {}; - data.headers.forEach(h => { mapping[h] = h; }); + data.headers.forEach((h) => { + mapping[h] = h; + }); setColumnMapping(mapping); - setStep('preview'); + setStep("preview"); } catch (err) { - setError(`Failed to parse file: ${err instanceof Error ? err.message : 'Unknown error'}`); + setError(`Failed to parse file: ${err instanceof Error ? err.message : "Unknown error"}`); } }; reader.readAsText(file); }, []); - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault(); - const file = e.dataTransfer.files[0]; - if (file) handleFileSelect(file); - }, [handleFileSelect]); + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + if (file) handleFileSelect(file); + }, + [handleFileSelect], + ); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); @@ -259,35 +259,43 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp Import Data - {fileName && ( - - {fileName} - - )} + {fileName && {fileName}} {/* Step Indicator */}
    - {(['upload', 'preview', 'configure', 'ready'] as ImportStep[]).map((s, idx) => ( + {(["upload", "preview", "configure", "ready"] as ImportStep[]).map((s, idx) => ( -
    -
    - {idx < ['upload', 'preview', 'configure', 'ready'].indexOf(step) ? ( +
    +
    + {idx < ["upload", "preview", "configure", "ready"].indexOf(step) ? ( ) : ( idx + 1 )}
    - {s === 'upload' ? 'Upload' : s === 'preview' ? 'Preview' : s === 'configure' ? 'Configure' : 'Import'} + + {s === "upload" ? "Upload" : s === "preview" ? "Preview" : s === "configure" ? "Configure" : "Import"} +
    {idx < 3 && } @@ -296,7 +304,7 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp
    {/* Step 1: Upload */} - {step === 'upload' && ( + {step === "upload" && (
    -

    - Drop a file here or click to browse -

    -

    - Supports CSV and JSON files -

    +

    Drop a file here or click to browse

    +

    Supports CSV and JSON files

    @@ -342,11 +346,11 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp )} {/* Step 2: Preview */} - {step === 'preview' && parsedData && ( + {step === "preview" && parsedData && (
    - {fileType === 'json' ? ( + {fileType === "json" ? ( ) : ( @@ -362,7 +366,9 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp variant="ghost" size="sm" className="h-7 text-xs text-zinc-500" - onClick={() => { resetState(); }} + onClick={() => { + resetState(); + }} > Reset @@ -373,8 +379,11 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp - {parsedData.headers.map(h => ( - ))} @@ -384,7 +393,10 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp {parsedData.rows.slice(0, 10).map((row, idx) => ( {row.map((cell, cidx) => ( - ))} @@ -403,7 +415,7 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp @@ -412,7 +424,7 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp )} {/* Step 3: Configure */} - {step === 'configure' && parsedData && ( + {step === "configure" && parsedData && (
    {/* Target Table */}
    @@ -421,7 +433,9 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp
    @@ -477,13 +495,16 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp Target Column
    - {parsedData.headers.map(header => ( -
    + {parsedData.headers.map((header) => ( +
    {header} setColumnMapping(prev => ({ ...prev, [header]: e.target.value }))} + value={columnMapping[header] || ""} + onChange={(e) => setColumnMapping((prev) => ({ ...prev, [header]: e.target.value }))} className="h-7 text-xs bg-[#111] border-white/10" placeholder={header} /> @@ -498,14 +519,14 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp variant="ghost" size="sm" className="h-8 text-xs text-zinc-500" - onClick={() => setStep('preview')} + onClick={() => setStep("preview")} > Back @@ -566,9 +585,13 @@ export function DataImportModal({ isOpen, onClose, onImport, tables, databaseTyp disabled={isImporting} > {isImporting ? ( - <> Importing... + <> + Importing... + ) : ( - <> Execute Import + <> + Execute Import + )}
    diff --git a/src/components/DataProfiler.tsx b/src/components/DataProfiler.tsx index e91b8bcc..92950df9 100644 --- a/src/components/DataProfiler.tsx +++ b/src/components/DataProfiler.tsx @@ -1,10 +1,10 @@ "use client"; -import { useState, useEffect, useMemo } from 'react'; -import { Loader2, BarChart3, X, Hash, AlertCircle, Sparkles, Lock } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { TableSchema, DatabaseConnection } from '@/lib/types'; -import { detectSensitiveColumns, maskValue } from '@/lib/data-masking'; +import { useState, useEffect, useMemo } from "react"; +import { Loader2, BarChart3, X, Hash, AlertCircle, Sparkles, Lock } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { TableSchema, DatabaseConnection } from "@/lib/types"; +import { detectSensitiveColumns, maskValue } from "@/lib/data-masking"; interface ColumnProfile { name: string; @@ -52,14 +52,14 @@ export function DataProfiler({ }: DataProfilerProps) { const [isLoading, setIsLoading] = useState(false); const [profile, setProfile] = useState(null); - const [aiSummary, setAiSummary] = useState(''); + const [aiSummary, setAiSummary] = useState(""); const [isAiLoading, setIsAiLoading] = useState(false); const [error, setError] = useState(null); // Detect sensitive columns for masking sample values in profiler const sensitiveColumnNames = useMemo(() => { if (!tableSchema?.columns) return new Map(); - return detectSensitiveColumns(tableSchema.columns.map(c => c.name)); + return detectSensitiveColumns(tableSchema.columns.map((c) => c.name)); }, [tableSchema]); useEffect(() => { @@ -68,10 +68,10 @@ export function DataProfiler({ } return () => { setProfile(null); - setAiSummary(''); + setAiSummary(""); setError(null); }; - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen, tableName]); const fetchProfile = async () => { @@ -87,16 +87,16 @@ export function DataProfiler({ data = await onProfile({ connectionId: connection.id, tableName }); } else { // Default: existing fetch behavior - const columns = tableSchema.columns?.map(c => c.name) || []; - const response = await fetch('/api/db/profile', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const columns = tableSchema.columns?.map((c) => c.name) || []; + const response = await fetch("/api/db/profile", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ connection, tableName, columns }), }); if (!response.ok) { const err = await response.json(); - throw new Error(err.error || 'Profile failed'); + throw new Error(err.error || "Profile failed"); } data = await response.json(); @@ -107,7 +107,7 @@ export function DataProfiler({ // Trigger AI summary fetchAiSummary(data); } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); + setError(err instanceof Error ? err.message : "Unknown error"); } finally { setIsLoading(false); } @@ -116,11 +116,14 @@ export function DataProfiler({ const fetchAiSummary = async (data: ProfileData) => { setIsAiLoading(true); try { - const profileSummary = data.columns.map(c => - `${c.name}: ${c.nullPercent}% null, ${c.distinctCount} distinct, min=${c.minValue || 'N/A'}, max=${c.maxValue || 'N/A'}` - ).join('\n'); + const profileSummary = data.columns + .map( + (c) => + `${c.name}: ${c.nullPercent}% null, ${c.distinctCount} distinct, min=${c.minValue || "N/A"}, max=${c.maxValue || "N/A"}`, + ) + .join("\n"); - const fullSchemaContext = `Table: ${tableName} (${data.totalRows} rows)\n\nColumn Profiles:\n${profileSummary}\n\nSchema:\n${schemaContext || ''}`; + const fullSchemaContext = `Table: ${tableName} (${data.totalRows} rows)\n\nColumn Profiles:\n${profileSummary}\n\nSchema:\n${schemaContext || ""}`; if (onDescribeSchema) { // Platform adapter: use callback instead of fetch @@ -128,13 +131,13 @@ export function DataProfiler({ setAiSummary(result); } else { // Default: existing fetch behavior - const response = await fetch('/api/ai/describe-schema', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const response = await fetch("/api/ai/describe-schema", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ schemaContext: fullSchemaContext, databaseType, - mode: 'table', + mode: "table", }), }); @@ -143,7 +146,7 @@ export function DataProfiler({ const reader = response.body?.getReader(); if (!reader) return; - let full = ''; + let full = ""; while (true) { const { done, value } = await reader.read(); if (done) break; @@ -208,7 +211,8 @@ export function DataProfiler({

    {profile.columns.length > 0 ? Math.round(profile.columns.reduce((sum, c) => sum + c.nullPercent, 0) / profile.columns.length) - : 0}% + : 0} + %

    @@ -222,16 +226,14 @@ export function DataProfiler({
    {col.name} - {col.type && ( - {col.type} - )} + {col.type && {col.type}} {sensitiveColumnNames.has(col.name) && ( - + + + )}
    - - {col.distinctCount.toLocaleString()} distinct - + {col.distinctCount.toLocaleString()} distinct {col.error ? ( @@ -244,47 +246,57 @@ export function DataProfiler({
    50 ? "bg-red-500" : - col.nullPercent > 20 ? "bg-amber-500" : - "bg-emerald-500" + col.nullPercent > 50 + ? "bg-red-500" + : col.nullPercent > 20 + ? "bg-amber-500" + : "bg-emerald-500", )} style={{ width: `${100 - col.nullPercent}%` }} />
    - 50 ? "text-red-400" : - col.nullPercent > 20 ? "text-amber-400" : - "text-emerald-400" - )}> + 50 + ? "text-red-400" + : col.nullPercent > 20 + ? "text-amber-400" + : "text-emerald-400", + )} + > {col.nullPercent}% null {/* Min/Max */}
    - {col.minValue && (() => { - const rule = sensitiveColumnNames.get(col.name); - const display = rule - ? maskValue(col.minValue, rule) - : col.minValue.substring(0, 30); - return ( - - min: {display} - - ); - })()} - {col.maxValue && (() => { - const rule = sensitiveColumnNames.get(col.name); - const display = rule - ? maskValue(col.maxValue, rule) - : col.maxValue.substring(0, 30); - return ( - - max: {display} - - ); - })()} + {col.minValue && + (() => { + const rule = sensitiveColumnNames.get(col.name); + const display = rule ? maskValue(col.minValue, rule) : col.minValue.substring(0, 30); + return ( + + min:{" "} + + {display} + + + ); + })()} + {col.maxValue && + (() => { + const rule = sensitiveColumnNames.get(col.name); + const display = rule ? maskValue(col.maxValue, rule) : col.maxValue.substring(0, 30); + return ( + + max:{" "} + + {display} + + + ); + })()}
    {/* Sample Values */} @@ -292,11 +304,15 @@ export function DataProfiler({
    {col.sampleValues.map((val, i) => { const rule = sensitiveColumnNames.get(col.name); - const display = rule - ? maskValue(val, rule) - : val.substring(0, 20); + const display = rule ? maskValue(val, rule) : val.substring(0, 20); return ( - + {display} ); @@ -314,15 +330,11 @@ export function DataProfiler({
    - - AI Analysis - + AI Analysis {isAiLoading && }
    {aiSummary && ( -
    - {aiSummary} -
    +
    {aiSummary}
    )}
    )} diff --git a/src/components/DatabaseDocs.tsx b/src/components/DatabaseDocs.tsx index 8768faef..414697f0 100644 --- a/src/components/DatabaseDocs.tsx +++ b/src/components/DatabaseDocs.tsx @@ -1,9 +1,9 @@ "use client"; -import React, { useState } from 'react'; -import { FileText, Loader2, Search, Sparkles, Download } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { TableSchema } from '@/lib/types'; +import React, { useState } from "react"; +import { FileText, Loader2, Search, Sparkles, Download } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { TableSchema } from "@/lib/types"; interface DatabaseDocsProps { schema: TableSchema[]; @@ -12,56 +12,70 @@ interface DatabaseDocsProps { } export function DatabaseDocs({ schema, schemaContext, databaseType }: DatabaseDocsProps) { - const [search, setSearch] = useState(''); - const [aiDocs, setAiDocs] = useState(''); + const [search, setSearch] = useState(""); + const [aiDocs, setAiDocs] = useState(""); const [isAiLoading, setIsAiLoading] = useState(false); const [error, setError] = useState(null); - const filteredSchema = schema.filter(t => - t.name.toLowerCase().includes(search.toLowerCase()) || - t.columns?.some(c => c.name.toLowerCase().includes(search.toLowerCase())) + const filteredSchema = schema.filter( + (t) => + t.name.toLowerCase().includes(search.toLowerCase()) || + t.columns?.some((c) => c.name.toLowerCase().includes(search.toLowerCase())), ); const generateAiDocs = async () => { setIsAiLoading(true); setError(null); - setAiDocs(''); + setAiDocs(""); try { - let filteredSchemaStr = ''; + let filteredSchemaStr = ""; if (schemaContext) { try { const tables = JSON.parse(schemaContext); - filteredSchemaStr = tables.slice(0, 50).map((t: { name: string; rowCount?: number; columns?: { name: string; type: string; isPrimary?: boolean; isNullable?: boolean }[] }) => { - const cols = t.columns?.map(c => - `${c.name} (${c.type}${c.isPrimary ? ', PK' : ''}${c.isNullable === false ? ', NOT NULL' : ''})` - ).join(', ') || ''; - return `Table: ${t.name} (${t.rowCount || 0} rows)\nColumns: ${cols}`; - }).join('\n\n'); + filteredSchemaStr = tables + .slice(0, 50) + .map( + (t: { + name: string; + rowCount?: number; + columns?: { name: string; type: string; isPrimary?: boolean; isNullable?: boolean }[]; + }) => { + const cols = + t.columns + ?.map( + (c) => + `${c.name} (${c.type}${c.isPrimary ? ", PK" : ""}${c.isNullable === false ? ", NOT NULL" : ""})`, + ) + .join(", ") || ""; + return `Table: ${t.name} (${t.rowCount || 0} rows)\nColumns: ${cols}`; + }, + ) + .join("\n\n"); } catch { filteredSchemaStr = schemaContext.substring(0, 5000); } } - const response = await fetch('/api/ai/describe-schema', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const response = await fetch("/api/ai/describe-schema", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ schemaContext: filteredSchemaStr, databaseType, - mode: 'full', + mode: "full", }), }); if (!response.ok) { const err = await response.json(); - throw new Error(err.error || 'Documentation generation failed'); + throw new Error(err.error || "Documentation generation failed"); } const reader = response.body?.getReader(); - if (!reader) throw new Error('No reader'); + if (!reader) throw new Error("No reader"); - let full = ''; + let full = ""; while (true) { const { done, value } = await reader.read(); if (done) break; @@ -69,7 +83,7 @@ export function DatabaseDocs({ schema, schemaContext, databaseType }: DatabaseDo setAiDocs(full); } } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); + setError(err instanceof Error ? err.message : "Unknown error"); } finally { setIsAiLoading(false); } @@ -77,7 +91,7 @@ export function DatabaseDocs({ schema, schemaContext, databaseType }: DatabaseDo const exportMarkdown = () => { let md = `# Database Documentation\n\n`; - md += `**Type:** ${databaseType || 'Unknown'}\n`; + md += `**Type:** ${databaseType || "Unknown"}\n`; md += `**Tables:** ${schema.length}\n\n`; if (aiDocs) { @@ -93,37 +107,61 @@ export function DatabaseDocs({ schema, schemaContext, databaseType }: DatabaseDo if (table.columns && table.columns.length > 0) { md += `| Column | Type | Primary | Nullable |\n|--------|------|---------|----------|\n`; for (const col of table.columns) { - md += `| ${col.name} | ${col.type} | ${col.isPrimary ? 'Yes' : ''} | ${col.nullable !== false ? 'Yes' : 'No'} |\n`; + md += `| ${col.name} | ${col.type} | ${col.isPrimary ? "Yes" : ""} | ${col.nullable !== false ? "Yes" : "No"} |\n`; } - md += '\n'; + md += "\n"; } } - const blob = new Blob([md], { type: 'text/markdown' }); + const blob = new Blob([md], { type: "text/markdown" }); const url = URL.createObjectURL(blob); - const a = document.createElement('a'); + const a = document.createElement("a"); a.href = url; - a.download = 'database-docs.md'; + a.download = "database-docs.md"; a.click(); URL.revokeObjectURL(url); }; // Simple markdown rendering for AI docs const renderMarkdown = (text: string) => { - return text.split('\n').map((line, i) => { - if (line.startsWith('## ')) return

    {line.slice(3)}

    ; - if (line.startsWith('### ')) return

    {line.slice(4)}

    ; - if (line.startsWith('- ')) { + return text.split("\n").map((line, i) => { + if (line.startsWith("## ")) + return ( +

    + {line.slice(3)} +

    + ); + if (line.startsWith("### ")) + return ( +

    + {line.slice(4)} +

    + ); + if (line.startsWith("- ")) { const content = line.slice(2).replace(/\*\*(.*?)\*\*/g, '$1'); - return
  • ; + return ( +
  • + ); } if (line.match(/^\d+\.\s/)) { const content = line.replace(/\*\*(.*?)\*\*/g, '$1'); - return
  • ; + return ( +
  • + ); } if (line.trim()) { const content = line.replace(/\*\*(.*?)\*\*/g, '$1'); - return

    ; + return ( +

    + ); } return

    ; }); @@ -137,9 +175,7 @@ export function DatabaseDocs({ schema, schemaContext, databaseType }: DatabaseDo
    - - Database Docs - + Database Docs {schema.length} tables
    @@ -148,13 +184,15 @@ export function DatabaseDocs({ schema, schemaContext, databaseType }: DatabaseDo disabled={isAiLoading} className={cn( "flex items-center gap-1 px-2.5 py-1 rounded-lg text-xs font-medium transition-colors", - isAiLoading - ? "bg-teal-600/20 text-teal-400 cursor-wait" - : "bg-teal-600 hover:bg-teal-500 text-white" + isAiLoading ? "bg-teal-600/20 text-teal-400 cursor-wait" : "bg-teal-600 hover:bg-teal-500 text-white", )} > - {isAiLoading ? : } - {aiDocs ? 'Regenerate' : 'AI Describe'} + {isAiLoading ? ( + + ) : ( + + )} + {aiDocs ? "Regenerate" : "AI Describe"}
  • - {table.columns.map(col => ( + {table.columns.map((col) => ( - + ))} diff --git a/src/components/MaskingSettings.tsx b/src/components/MaskingSettings.tsx index e9b0cf92..a69abe62 100644 --- a/src/components/MaskingSettings.tsx +++ b/src/components/MaskingSettings.tsx @@ -1,35 +1,15 @@ -'use client'; +"use client"; -import React, { useState, useCallback } from 'react'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { Switch } from '@/components/ui/switch'; -import { Badge } from '@/components/ui/badge'; -import { Input } from '@/components/ui/input'; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogFooter, -} from '@/components/ui/dialog'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { - Shield, - Plus, - Pencil, - Trash2, - RotateCcw, - Save, - Lock, -} from 'lucide-react'; -import { toast } from 'sonner'; +import React, { useState, useCallback } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Shield, Plus, Pencil, Trash2, RotateCcw, Save, Lock } from "lucide-react"; +import { toast } from "sonner"; import { type MaskingConfig, type MaskingPattern, @@ -39,9 +19,20 @@ import { getPreviewMasked, loadMaskingConfig, saveMaskingConfig, -} from '@/lib/data-masking'; +} from "@/lib/data-masking"; -const ALL_MASK_TYPES: MaskType[] = ['email', 'phone', 'card', 'ssn', 'full', 'partial', 'ip', 'date', 'financial', 'custom']; +const ALL_MASK_TYPES: MaskType[] = [ + "email", + "phone", + "card", + "ssn", + "full", + "partial", + "ip", + "date", + "financial", + "custom", +]; export function MaskingSettings() { const [config, setConfig] = useState(() => loadMaskingConfig()); @@ -50,39 +41,35 @@ export function MaskingSettings() { const [isNewPattern, setIsNewPattern] = useState(false); // Edit dialog state - const [editName, setEditName] = useState(''); - const [editMaskType, setEditMaskType] = useState('full'); - const [editColumnPatterns, setEditColumnPatterns] = useState(''); - const [editCustomMask, setEditCustomMask] = useState(''); + const [editName, setEditName] = useState(""); + const [editMaskType, setEditMaskType] = useState("full"); + const [editColumnPatterns, setEditColumnPatterns] = useState(""); + const [editCustomMask, setEditCustomMask] = useState(""); const handleSave = useCallback(() => { saveMaskingConfig(config); - toast.success('Masking configuration saved'); + toast.success("Masking configuration saved"); }, [config]); const handleReset = useCallback(() => { setConfig(DEFAULT_MASKING_CONFIG); saveMaskingConfig(DEFAULT_MASKING_CONFIG); - toast.success('Masking configuration reset to defaults'); + toast.success("Masking configuration reset to defaults"); }, []); const toggleGlobal = useCallback((enabled: boolean) => { - setConfig(prev => ({ ...prev, enabled })); + setConfig((prev) => ({ ...prev, enabled })); }, []); const togglePatternEnabled = useCallback((patternId: string, enabled: boolean) => { - setConfig(prev => ({ + setConfig((prev) => ({ ...prev, - patterns: prev.patterns.map(p => p.id === patternId ? { ...p, enabled } : p), + patterns: prev.patterns.map((p) => (p.id === patternId ? { ...p, enabled } : p)), })); }, []); - const updateRoleSetting = useCallback(( - role: 'admin' | 'user', - key: 'canToggle' | 'canReveal', - value: boolean - ) => { - setConfig(prev => ({ + const updateRoleSetting = useCallback((role: "admin" | "user", key: "canToggle" | "canReveal", value: boolean) => { + setConfig((prev) => ({ ...prev, roleSettings: { ...prev.roleSettings, @@ -95,34 +82,34 @@ export function MaskingSettings() { setEditingPattern(pattern); setEditName(pattern.name); setEditMaskType(pattern.maskType); - setEditColumnPatterns(pattern.columnPatterns.join('\n')); - setEditCustomMask(pattern.customMask || ''); + setEditColumnPatterns(pattern.columnPatterns.join("\n")); + setEditCustomMask(pattern.customMask || ""); setIsNewPattern(false); setIsDialogOpen(true); }, []); const openNewDialog = useCallback(() => { setEditingPattern(null); - setEditName(''); - setEditMaskType('full'); - setEditColumnPatterns(''); - setEditCustomMask(''); + setEditName(""); + setEditMaskType("full"); + setEditColumnPatterns(""); + setEditCustomMask(""); setIsNewPattern(true); setIsDialogOpen(true); }, []); const handleDialogSave = useCallback(() => { const patterns = editColumnPatterns - .split('\n') - .map(s => s.trim()) + .split("\n") + .map((s) => s.trim()) .filter(Boolean); if (!editName.trim()) { - toast.error('Pattern name is required'); + toast.error("Pattern name is required"); return; } if (patterns.length === 0) { - toast.error('At least one column pattern is required'); + toast.error("At least one column pattern is required"); return; } @@ -134,25 +121,25 @@ export function MaskingSettings() { maskType: editMaskType, enabled: true, isBuiltin: false, - customMask: editMaskType === 'custom' ? editCustomMask : undefined, + customMask: editMaskType === "custom" ? editCustomMask : undefined, }; - setConfig(prev => ({ + setConfig((prev) => ({ ...prev, patterns: [...prev.patterns, newPattern], })); } else if (editingPattern) { - setConfig(prev => ({ + setConfig((prev) => ({ ...prev, - patterns: prev.patterns.map(p => + patterns: prev.patterns.map((p) => p.id === editingPattern.id ? { ...p, name: editName.trim(), maskType: editMaskType, columnPatterns: patterns, - customMask: editMaskType === 'custom' ? editCustomMask : undefined, + customMask: editMaskType === "custom" ? editCustomMask : undefined, } - : p + : p, ), })); } @@ -161,9 +148,9 @@ export function MaskingSettings() { }, [editName, editMaskType, editColumnPatterns, editCustomMask, isNewPattern, editingPattern]); const deletePattern = useCallback((patternId: string) => { - setConfig(prev => ({ + setConfig((prev) => ({ ...prev, - patterns: prev.patterns.filter(p => p.id !== patternId), + patterns: prev.patterns.filter((p) => p.id !== patternId), })); }, []); @@ -186,10 +173,7 @@ export function MaskingSettings() { When enabled, sensitive columns are automatically detected and masked

    - + {/* Role Permissions */} @@ -199,20 +183,22 @@ export function MaskingSettings() { {/* Admin Row */}
    - Admin + + Admin +
    @@ -221,20 +207,22 @@ export function MaskingSettings() { {/* User Row */}
    - User + + User +
    @@ -254,36 +242,32 @@ export function MaskingSettings() {
    - {config.patterns.map(pattern => ( + {config.patterns.map((pattern) => (
    - togglePatternEnabled(pattern.id, v)} - /> + togglePatternEnabled(pattern.id, v)} />
    {pattern.name} {pattern.isBuiltin && ( - builtin + + builtin + )} - {pattern.maskType} + + {pattern.maskType} +

    - {pattern.columnPatterns.join(', ')} + {pattern.columnPatterns.join(", ")}

    - {!pattern.isBuiltin && ( @@ -307,19 +291,22 @@ export function MaskingSettings() {

    Preview

    - {config.patterns.filter(p => p.enabled).slice(0, 5).map(pattern => { - const preview = MASK_TYPE_PREVIEWS[pattern.maskType]; - const masked = getPreviewMasked(pattern.maskType, pattern.customMask); - return ( -
    - - {pattern.name}: - {preview.sample} - - {masked} -
    - ); - })} + {config.patterns + .filter((p) => p.enabled) + .slice(0, 5) + .map((pattern) => { + const preview = MASK_TYPE_PREVIEWS[pattern.maskType]; + const masked = getPreviewMasked(pattern.maskType, pattern.customMask); + return ( +
    + + {pattern.name}: + {preview.sample} + + {masked} +
    + ); + })}
    @@ -341,18 +328,12 @@ export function MaskingSettings() { - - {isNewPattern ? 'Add Masking Pattern' : 'Edit Masking Pattern'} - + {isNewPattern ? "Add Masking Pattern" : "Edit Masking Pattern"}
    - setEditName(e.target.value)} - placeholder="Pattern name" - /> + setEditName(e.target.value)} placeholder="Pattern name" />
    @@ -361,7 +342,7 @@ export function MaskingSettings() { - {ALL_MASK_TYPES.map(t => ( + {ALL_MASK_TYPES.map((t) => ( {t} — {MASK_TYPE_PREVIEWS[t].label} @@ -369,7 +350,7 @@ export function MaskingSettings() {
    - {editMaskType === 'custom' && ( + {editMaskType === "custom" && (

    Preview:

    - {getPreviewMasked(editMaskType, editMaskType === 'custom' ? editCustomMask : undefined)} + {getPreviewMasked(editMaskType, editMaskType === "custom" ? editCustomMask : undefined)}

    diff --git a/src/components/MobileNav.tsx b/src/components/MobileNav.tsx index 6c2a919b..327aef7d 100644 --- a/src/components/MobileNav.tsx +++ b/src/components/MobileNav.tsx @@ -1,20 +1,20 @@ "use client"; -import React from 'react'; -import { Database, Terminal, Table as TableIcon } from 'lucide-react'; -import { cn } from '@/lib/utils'; +import React from "react"; +import { Database, Terminal, Table as TableIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; interface MobileNavProps { - activeTab: 'database' | 'schema' | 'editor'; - onTabChange: (tab: 'database' | 'schema' | 'editor') => void; + activeTab: "database" | "schema" | "editor"; + onTabChange: (tab: "database" | "schema" | "editor") => void; hasResult?: boolean; } export function MobileNav({ activeTab, onTabChange }: MobileNavProps) { const tabs = [ - { id: 'database', label: 'DB', icon: Database }, - { id: 'schema', label: 'Schema', icon: TableIcon }, - { id: 'editor', label: 'SQL', icon: Terminal }, + { id: "database", label: "DB", icon: Database }, + { id: "schema", label: "Schema", icon: TableIcon }, + { id: "editor", label: "SQL", icon: Terminal }, ] as const; return ( @@ -29,19 +29,19 @@ export function MobileNav({ activeTab, onTabChange }: MobileNavProps) { onClick={() => onTabChange(tab.id)} className={cn( "flex flex-col items-center gap-1 transition-all duration-200 relative", - isActive ? "text-blue-400" : "text-zinc-500" + isActive ? "text-blue-400" : "text-zinc-500", )} > -
    +
    {tab.label} - {isActive && ( -
    - )} + {isActive &&
    } ); })} diff --git a/src/components/NL2SQLPanel.tsx b/src/components/NL2SQLPanel.tsx index 155bfedd..a3186365 100644 --- a/src/components/NL2SQLPanel.tsx +++ b/src/components/NL2SQLPanel.tsx @@ -1,11 +1,11 @@ "use client"; -import { useState, useRef, useEffect, type FormEvent } from 'react'; -import { Send, Loader2, Sparkles, X, Play, MessageSquare, Trash2 } from 'lucide-react'; -import { cn } from '@/lib/utils'; +import { useState, useRef, useEffect, type FormEvent } from "react"; +import { Send, Loader2, Sparkles, X, Play, MessageSquare, Trash2 } from "lucide-react"; +import { cn } from "@/lib/utils"; interface ConversationMessage { - role: 'user' | 'assistant'; + role: "user" | "assistant"; content: string; query?: string; // Extracted SQL/JSON query from assistant response } @@ -19,7 +19,11 @@ interface NL2SQLPanelProps { databaseType?: string; queryLanguage?: string; /** Optional API adapter: when provided, bypasses the built-in /api/ai/nl2sql fetch. */ - onNL2SQL?: (params: { prompt: string; schemaContext: string; conversationHistory?: { role: string; content: string }[] }) => Promise; + onNL2SQL?: (params: { + prompt: string; + schemaContext: string; + conversationHistory?: { role: string; content: string }[]; + }) => Promise; } function extractCodeBlock(text: string): string | null { @@ -38,7 +42,7 @@ export function NL2SQLPanel({ queryLanguage, onNL2SQL, }: NL2SQLPanelProps) { - const [question, setQuestion] = useState(''); + const [question, setQuestion] = useState(""); const [isLoading, setIsLoading] = useState(false); const [messages, setMessages] = useState([]); const [error, setError] = useState(null); @@ -46,7 +50,7 @@ export function NL2SQLPanel({ const inputRef = useRef(null); useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); useEffect(() => { @@ -57,34 +61,46 @@ export function NL2SQLPanel({ if (e) e.preventDefault(); if (!question.trim() || isLoading) return; - const userMsg: ConversationMessage = { role: 'user', content: question.trim() }; - setMessages(prev => [...prev, userMsg]); - setQuestion(''); + const userMsg: ConversationMessage = { role: "user", content: question.trim() }; + setMessages((prev) => [...prev, userMsg]); + setQuestion(""); setIsLoading(true); setError(null); try { // Build filtered schema context (top 100 tables) - let filteredSchema = ''; + let filteredSchema = ""; if (schemaContext) { try { const tables = JSON.parse(schemaContext); const sorted = [...tables] .sort((a: { rowCount?: number }, b: { rowCount?: number }) => (b.rowCount || 0) - (a.rowCount || 0)) .slice(0, 100); - filteredSchema = sorted.map((t: { name: string; rowCount?: number; columns?: { name: string; type: string; isPrimary?: boolean }[] }) => { - const cols = t.columns?.slice(0, 10).map(c => `${c.name} (${c.type}${c.isPrimary ? ', PK' : ''})`).join(', ') || ''; - return `Table: ${t.name} (${t.rowCount || 0} rows)\nColumns: ${cols}`; - }).join('\n\n'); + filteredSchema = sorted + .map( + (t: { + name: string; + rowCount?: number; + columns?: { name: string; type: string; isPrimary?: boolean }[]; + }) => { + const cols = + t.columns + ?.slice(0, 10) + .map((c) => `${c.name} (${c.type}${c.isPrimary ? ", PK" : ""})`) + .join(", ") || ""; + return `Table: ${t.name} (${t.rowCount || 0} rows)\nColumns: ${cols}`; + }, + ) + .join("\n\n"); } catch { filteredSchema = schemaContext.substring(0, 3000); } } // Build conversation history (exclude current question) - const history = messages.map(m => ({ role: m.role, content: m.content })); + const history = messages.map((m) => ({ role: m.role, content: m.content })); - let fullResponse = ''; + let fullResponse = ""; if (onNL2SQL) { // Platform adapter: use callback instead of fetch @@ -95,9 +111,9 @@ export function NL2SQLPanel({ }); } else { // Default: existing fetch behavior - const response = await fetch('/api/ai/nl2sql', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, + const response = await fetch("/api/ai/nl2sql", { + method: "POST", + headers: { "Content-Type": "application/json" }, body: JSON.stringify({ question: question.trim(), schemaContext: filteredSchema, @@ -109,11 +125,11 @@ export function NL2SQLPanel({ if (!response.ok) { const errData = await response.json(); - throw new Error(errData.error || 'Request failed'); + throw new Error(errData.error || "Request failed"); } const reader = response.body?.getReader(); - if (!reader) throw new Error('No reader'); + if (!reader) throw new Error("No reader"); while (true) { const { done, value } = await reader.read(); @@ -124,13 +140,13 @@ export function NL2SQLPanel({ const extractedQuery = extractCodeBlock(fullResponse); const assistantMsg: ConversationMessage = { - role: 'assistant', + role: "assistant", content: fullResponse, query: extractedQuery || undefined, }; - setMessages(prev => [...prev, assistantMsg]); + setMessages((prev) => [...prev, assistantMsg]); } catch (err) { - const msg = err instanceof Error ? err.message : 'Unknown error'; + const msg = err instanceof Error ? err.message : "Unknown error"; setError(msg); } finally { setIsLoading(false); @@ -152,12 +168,10 @@ export function NL2SQLPanel({
    - - Natural Language Query - + Natural Language Query {messages.length > 0 && ( - {messages.filter(m => m.role === 'user').length} questions + {messages.filter((m) => m.role === "user").length} questions )}
    @@ -186,21 +200,21 @@ export function NL2SQLPanel({

    Ask a question in plain English

    -

    - e.g. "Show me the top 10 employees by salary" -

    +

    e.g. "Show me the top 10 employees by salary"

    )} {messages.map((msg, i) => ( -
    -
    - {msg.role === 'user' ? ( +
    +
    + {msg.role === "user" ? (

    {msg.content}

    ) : (
    @@ -227,9 +241,9 @@ export function NL2SQLPanel({
    )} {/* Show explanation text (non-code parts) */} - {msg.content.replace(/```[\s\S]*?```/g, '').trim() && ( + {msg.content.replace(/```[\s\S]*?```/g, "").trim() && (

    - {msg.content.replace(/```[\s\S]*?```/g, '').trim()} + {msg.content.replace(/```[\s\S]*?```/g, "").trim()}

    )}
    @@ -272,7 +286,11 @@ export function NL2SQLPanel({ disabled={isLoading || !question.trim()} className="bg-violet-600 hover:bg-violet-500 disabled:opacity-50 px-3 py-2 rounded-lg text-white text-xs font-medium transition-colors flex items-center gap-1.5" > - {isLoading ? : } + {isLoading ? ( + + ) : ( + + )}
    diff --git a/src/components/PivotTable.tsx b/src/components/PivotTable.tsx index cde9922a..5857dbb7 100644 --- a/src/components/PivotTable.tsx +++ b/src/components/PivotTable.tsx @@ -1,34 +1,39 @@ "use client"; -import React, { useState, useMemo, useCallback, useEffect } from 'react'; -import { Columns3, GripVertical, ArrowRight } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { QueryResult } from '@/lib/types'; +import React, { useState, useMemo, useCallback, useEffect } from "react"; +import { Columns3, GripVertical, ArrowRight } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { QueryResult } from "@/lib/types"; interface PivotTableProps { result: QueryResult | null; onLoadQuery?: (query: string) => void; } -type AggFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'; +type AggFunction = "count" | "sum" | "avg" | "min" | "max"; const AGG_LABELS: Record = { - count: 'COUNT', - sum: 'SUM', - avg: 'AVG', - min: 'MIN', - max: 'MAX', + count: "COUNT", + sum: "SUM", + avg: "AVG", + min: "MIN", + max: "MAX", }; export function aggregate(values: unknown[], fn: AggFunction): string { - const nums = values.map(v => Number(v)).filter(n => !isNaN(n)); + const nums = values.map((v) => Number(v)).filter((n) => !isNaN(n)); switch (fn) { - case 'count': return String(values.length); - case 'sum': return nums.length ? nums.reduce((a, b) => a + b, 0).toFixed(2) : '0'; - case 'avg': return nums.length ? (nums.reduce((a, b) => a + b, 0) / nums.length).toFixed(2) : '0'; - case 'min': return nums.length ? String(Math.min(...nums)) : '-'; - case 'max': return nums.length ? String(Math.max(...nums)) : '-'; + case "count": + return String(values.length); + case "sum": + return nums.length ? nums.reduce((a, b) => a + b, 0).toFixed(2) : "0"; + case "avg": + return nums.length ? (nums.reduce((a, b) => a + b, 0) / nums.length).toFixed(2) : "0"; + case "min": + return nums.length ? String(Math.min(...nums)) : "-"; + case "max": + return nums.length ? String(Math.max(...nums)) : "-"; } } @@ -36,26 +41,26 @@ export function PivotTable({ result, onLoadQuery }: PivotTableProps) { const [rowField, setRowField] = useState(null); const [colField, setColField] = useState(null); const [valueField, setValueField] = useState(null); - const [aggFunction, setAggFunction] = useState('count'); + const [aggFunction, setAggFunction] = useState("count"); const fields = result?.fields || []; const rows = useMemo(() => result?.rows || [], [result?.rows]); // Auto-detect fields on first render useEffect(() => { if (fields.length >= 2 && !rowField) { - const strCol = fields.find(f => { + const strCol = fields.find((f) => { const sample = rows[0]?.[f]; - return typeof sample === 'string'; + return typeof sample === "string"; }); if (strCol) setRowField(strCol); - const numCol = fields.find(f => { + const numCol = fields.find((f) => { const sample = rows[0]?.[f]; - return typeof sample === 'number'; + return typeof sample === "number"; }); if (numCol) setValueField(numCol); } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [fields.length]); // Compute pivot data @@ -67,8 +72,8 @@ export function PivotTable({ result, onLoadQuery }: PivotTableProps) { const colValues = new Set(); for (const row of rows) { - const rowKey = String(row[rowField] ?? 'NULL'); - const colKey = colField ? String(row[colField] ?? 'NULL') : '__all__'; + const rowKey = String(row[rowField] ?? "NULL"); + const colKey = colField ? String(row[colField] ?? "NULL") : "__all__"; const value = valueField ? row[valueField] : 1; if (colField) colValues.add(colKey); @@ -79,7 +84,7 @@ export function PivotTable({ result, onLoadQuery }: PivotTableProps) { colMap.get(colKey)!.push(value); } - const colKeys = colField ? Array.from(colValues).sort() : ['__all__']; + const colKeys = colField ? Array.from(colValues).sort() : ["__all__"]; // Build pivot rows const pivotRows: { rowKey: string; values: Map }[] = []; @@ -100,7 +105,7 @@ export function PivotTable({ result, onLoadQuery }: PivotTableProps) { // Generate SQL const generateSQL = useCallback(() => { - if (!rowField) return ''; + if (!rowField) return ""; const select: string[] = [`"${rowField}"`]; const groupBy: string[] = [`"${rowField}"`]; @@ -108,18 +113,18 @@ export function PivotTable({ result, onLoadQuery }: PivotTableProps) { // Use CASE WHEN for pivot columns const colKeys = pivotData?.colKeys || []; for (const ck of colKeys) { - if (ck === '__all__') continue; - const valExpr = valueField ? `"${valueField}"` : '1'; + if (ck === "__all__") continue; + const valExpr = valueField ? `"${valueField}"` : "1"; select.push( - `${AGG_LABELS[aggFunction]}(CASE WHEN "${colField}" = '${ck.replace(/'/g, "''")}' THEN ${valExpr} END) AS "${ck}"` + `${AGG_LABELS[aggFunction]}(CASE WHEN "${colField}" = '${ck.replace(/'/g, "''")}' THEN ${valExpr} END) AS "${ck}"`, ); } } else { - const valExpr = valueField ? `"${valueField}"` : '*'; + const valExpr = valueField ? `"${valueField}"` : "*"; select.push(`${AGG_LABELS[aggFunction]}(${valExpr}) AS "${aggFunction}_value"`); } - return `SELECT\n ${select.join(',\n ')}\nFROM your_table\nGROUP BY ${groupBy.join(', ')}\nORDER BY ${groupBy.join(', ')};`; + return `SELECT\n ${select.join(",\n ")}\nFROM your_table\nGROUP BY ${groupBy.join(", ")}\nORDER BY ${groupBy.join(", ")};`; }, [rowField, colField, valueField, aggFunction, pivotData]); if (!result || rows.length === 0) { @@ -140,12 +145,16 @@ export function PivotTable({ result, onLoadQuery }: PivotTableProps) {
    Rows:
    @@ -153,12 +162,18 @@ export function PivotTable({ result, onLoadQuery }: PivotTableProps) {
    Columns:
    @@ -166,18 +181,24 @@ export function PivotTable({ result, onLoadQuery }: PivotTableProps) {
    Values:
    {/* Aggregation */}
    - {(Object.keys(AGG_LABELS) as AggFunction[]).map(fn => ( + {(Object.keys(AGG_LABELS) as AggFunction[]).map((fn) => (
    - {pivotData.colKeys.map(ck => ( - ))} @@ -226,12 +250,10 @@ export function PivotTable({ result, onLoadQuery }: PivotTableProps) { {pivotData.pivotRows.map((row, i) => ( - - {pivotData.colKeys.map(ck => ( + + {pivotData.colKeys.map((ck) => ( ))} @@ -249,7 +271,8 @@ export function PivotTable({ result, onLoadQuery }: PivotTableProps) { {/* Status */} {pivotData && (
    - {pivotData.pivotRows.length} groups • {pivotData.colKeys.length} columns • {AGG_LABELS[aggFunction]} aggregation + {pivotData.pivotRows.length} groups • {pivotData.colKeys.length} columns • {AGG_LABELS[aggFunction]}{" "} + aggregation
    )} diff --git a/src/components/QueryEditor.tsx b/src/components/QueryEditor.tsx index 77248b37..6624e1fd 100644 --- a/src/components/QueryEditor.tsx +++ b/src/components/QueryEditor.tsx @@ -1,18 +1,18 @@ "use client"; -import React, { useRef, useEffect, useState, useMemo, forwardRef, useImperativeHandle, useCallback } from 'react'; -import Editor, { useMonaco } from '@monaco-editor/react'; -import type * as Monaco from 'monaco-editor'; -import { Zap, Sparkles, Send, X, Loader2, AlignLeft, Trash2, Copy, Play, Hash } from 'lucide-react'; -import { cn } from '@/lib/utils'; -import { Button } from '@/components/ui/button'; -import { motion, AnimatePresence } from 'framer-motion'; -import { format } from 'sql-formatter'; -import { registerSQLCompletionProvider } from '@/lib/editor/sql-completions'; -import type { SchemaCompletionCache, SchemaColumnItem } from '@/lib/editor/sql-completions'; -import { registerMongoDBCompletionProvider } from '@/lib/editor/mongodb-completions'; -import { registerLibreDBLanguage } from '@/lib/editor/libredb-language'; -import { useAiChat } from '@/hooks/use-ai-chat'; +import React, { useRef, useEffect, useState, useMemo, forwardRef, useImperativeHandle, useCallback } from "react"; +import Editor, { useMonaco } from "@monaco-editor/react"; +import type * as Monaco from "monaco-editor"; +import { Zap, Sparkles, Send, X, Loader2, AlignLeft, Trash2, Copy, Play, Hash } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { motion, AnimatePresence } from "framer-motion"; +import { format } from "sql-formatter"; +import { registerSQLCompletionProvider } from "@/lib/editor/sql-completions"; +import type { SchemaCompletionCache, SchemaColumnItem } from "@/lib/editor/sql-completions"; +import { registerMongoDBCompletionProvider } from "@/lib/editor/mongodb-completions"; +import { registerLibreDBLanguage } from "@/lib/editor/libredb-language"; +import { useAiChat } from "@/hooks/use-ai-chat"; export interface QueryEditorRef { getSelectedText: () => string; @@ -32,13 +32,17 @@ interface QueryEditorProps { /** Called when content changes in real-time. Use sparingly as it triggers on every keystroke. */ onContentChange?: (val: string) => void; onExplain?: () => void; - language?: 'sql' | 'json' | 'libredb'; + language?: "sql" | "json" | "libredb"; tables?: string[]; databaseType?: string; schemaContext?: string; - capabilities?: import('@/lib/db/types').ProviderCapabilities; + capabilities?: import("@/lib/db/types").ProviderCapabilities; /** Optional API adapter: when provided, bypasses the built-in /api/ai/chat fetch. */ - onAiChat?: (params: { prompt: string; schemaContext: string; history: { role: string; content: string }[] }) => Promise; + onAiChat?: (params: { + prompt: string; + schemaContext: string; + history: { role: string; content: string }[]; + }) => Promise; } interface ParsedTable { @@ -56,22 +60,22 @@ const getEditorOptions = (showLineNumbers: boolean) => ({ minimap: { enabled: false }, fontSize: 13, fontFamily: '"JetBrains Mono", "Fira Code", Menlo, Monaco, Consolas, monospace', - lineNumbers: showLineNumbers ? ('on' as const) : ('off' as const), + lineNumbers: showLineNumbers ? ("on" as const) : ("off" as const), roundedSelection: true, scrollBeyondLastLine: false, readOnly: false, automaticLayout: true, padding: { top: 12 }, - cursorSmoothCaretAnimation: 'on' as const, - cursorBlinking: 'smooth' as const, + cursorSmoothCaretAnimation: "on" as const, + cursorBlinking: "smooth" as const, smoothScrolling: true, contextmenu: true, - renderLineHighlight: 'all' as const, + renderLineHighlight: "all" as const, bracketPairColorization: { enabled: true }, guides: { indentation: true }, scrollbar: { - vertical: 'visible' as const, - horizontal: 'visible' as const, + vertical: "visible" as const, + horizontal: "visible" as const, verticalScrollbarSize: 8, horizontalScrollbarSize: 8, }, @@ -80,491 +84,497 @@ const getEditorOptions = (showLineNumbers: boolean) => ({ quickSuggestions: { other: true, comments: false, - strings: true + strings: true, }, parameterHints: { - enabled: true - } + enabled: true, + }, }); -export const QueryEditor = forwardRef(({ - value, - onChange, - onContentChange, - onExplain, - language = 'sql', - tables = [], - databaseType, - schemaContext, - capabilities, - onAiChat, -}, ref) => { - const monaco = useMonaco(); - const editorRef = useRef(null); - const [hasSelection, setHasSelection] = useState(false); - - // Line numbers toggle state (persisted in localStorage) - const [showLineNumbers, setShowLineNumbers] = useState(() => { - if (typeof window !== 'undefined') { - const saved = localStorage.getItem('editor-line-numbers'); - return saved !== null ? saved === 'true' : true; // default: true - } - return true; - }); - - // Track last synced value to detect external changes - const lastSyncedValueRef = useRef(value); - const isInternalChangeRef = useRef(false); - - // Sync editor content when value prop changes externally (e.g., tab switch) - useEffect(() => { - if (editorRef.current && value !== lastSyncedValueRef.current) { - const currentEditorValue = editorRef.current.getValue(); - // Only update if the new value is different from current editor content - // This prevents unnecessary updates when we're the source of the change - if (value !== currentEditorValue) { - isInternalChangeRef.current = true; - editorRef.current.setValue(value); - lastSyncedValueRef.current = value; - isInternalChangeRef.current = false; +export const QueryEditor = forwardRef( + ( + { + value, + onChange, + onContentChange, + onExplain, + language = "sql", + tables = [], + databaseType, + schemaContext, + capabilities, + onAiChat, + }, + ref, + ) => { + const monaco = useMonaco(); + const editorRef = useRef(null); + const [hasSelection, setHasSelection] = useState(false); + + // Line numbers toggle state (persisted in localStorage) + const [showLineNumbers, setShowLineNumbers] = useState(() => { + if (typeof window !== "undefined") { + const saved = localStorage.getItem("editor-line-numbers"); + return saved !== null ? saved === "true" : true; // default: true } - } - }, [value]); - - // Update editor options when line numbers toggle changes - useEffect(() => { - if (editorRef.current) { - editorRef.current.updateOptions({ lineNumbers: showLineNumbers ? 'on' : 'off' }); - } - }, [showLineNumbers]); - - // Persist line numbers preference to localStorage - useEffect(() => { - if (typeof window !== 'undefined') { - localStorage.setItem('editor-line-numbers', String(showLineNumbers)); - } - }, [showLineNumbers]); - - const parsedSchema = useMemo((): ParsedTable[] => { - if (!schemaContext) return []; - try { - return JSON.parse(schemaContext); - } catch (e) { - console.error('Failed to parse schema context for editor:', e); - return []; - } - }, [schemaContext]); - - // Pre-compute schema-based completion items for faster lookups - const schemaCompletionCache = useMemo((): SchemaCompletionCache => { - const tableItems: SchemaCompletionCache['tableItems'] = []; - const columnMap = new Map(); - const allColumns = new Map(); - - parsedSchema.forEach((table) => { - const tableLower = table.name.toLowerCase(); - tableItems.push({ - label: table.name, - labelLower: tableLower, - rowCount: table.rowCount || 0, - columnNames: table.columns?.map((c) => c.name).join(', ') || '' - }); - - const tableColumns: SchemaColumnItem[] = []; - table.columns?.forEach((col) => { - const colItem: SchemaColumnItem = { - label: col.name, - labelLower: col.name.toLowerCase(), - type: col.type, - isPrimary: col.isPrimary || false, - tableName: table.name - }; - tableColumns.push(colItem); + return true; + }); - // Only store first occurrence for global column suggestions - if (!allColumns.has(col.name)) { - allColumns.set(col.name, colItem); + // Track last synced value to detect external changes + const lastSyncedValueRef = useRef(value); + const isInternalChangeRef = useRef(false); + + // Sync editor content when value prop changes externally (e.g., tab switch) + useEffect(() => { + if (editorRef.current && value !== lastSyncedValueRef.current) { + const currentEditorValue = editorRef.current.getValue(); + // Only update if the new value is different from current editor content + // This prevents unnecessary updates when we're the source of the change + if (value !== currentEditorValue) { + isInternalChangeRef.current = true; + editorRef.current.setValue(value); + lastSyncedValueRef.current = value; + isInternalChangeRef.current = false; } - }); - columnMap.set(tableLower, tableColumns); - }); + } + }, [value]); - return { tableItems, columnMap, allColumns }; - }, [parsedSchema]); - - const handleFormat = () => { - if (!editorRef.current) return; - const currentValue = editorRef.current.getValue(); - if (!currentValue) return; - - try { - let formatted: string; - if (language === 'json') { - // JSON formatting for MongoDB queries - const parsed = JSON.parse(currentValue); - formatted = JSON.stringify(parsed, null, 2); - } else if (language === 'sql') { - formatted = format(currentValue, { - language: 'postgresql', - keywordCase: 'upper', - dataTypeCase: 'upper', - indentStyle: 'tabularLeft', - logicalOperatorNewline: 'before', - expressionWidth: 100, - tabWidth: 2, - linesBetweenQueries: 2, - }); - } else { - return; + // Update editor options when line numbers toggle changes + useEffect(() => { + if (editorRef.current) { + editorRef.current.updateOptions({ lineNumbers: showLineNumbers ? "on" : "off" }); + } + }, [showLineNumbers]); + + // Persist line numbers preference to localStorage + useEffect(() => { + if (typeof window !== "undefined") { + localStorage.setItem("editor-line-numbers", String(showLineNumbers)); } - editorRef.current.setValue(formatted); - lastSyncedValueRef.current = formatted; - onChange?.(formatted); - } catch (e) { - console.error('Formatting failed:', e); - } - }; - - const getSelectedText = () => { - if (!editorRef.current) return ''; - const selection = editorRef.current.getSelection(); - const model = editorRef.current.getModel(); - if (!selection || !model) return ''; - return model.getValueInRange(selection); - }; - - const getEffectiveQuery = () => { - const editorValue = editorRef.current?.getValue() || ''; - if (!editorRef.current || !monaco) return { query: editorValue, range: null }; - - const model = editorRef.current.getModel(); - if (!model) return { query: editorValue, range: null }; - - // 1. Check for explicit selection - const selection = editorRef.current.getSelection(); - if (selection) { - const selectedText = model.getValueInRange(selection); - if (selectedText && selectedText.trim().length > 0) { - return { query: selectedText, range: selection }; + }, [showLineNumbers]); + + const parsedSchema = useMemo((): ParsedTable[] => { + if (!schemaContext) return []; + try { + return JSON.parse(schemaContext); + } catch (e) { + console.error("Failed to parse schema context for editor:", e); + return []; } - } - - // 2. If no selection, try to find the current statement (between semicolons) - if (language === 'sql') { - const position = editorRef.current.getPosition(); - if (position) { - const fullText = model.getValue(); - const cursorOffset = model.getOffsetAt(position); - - // Find boundaries of the current statement - let startOffset = fullText.lastIndexOf(';', cursorOffset - 1); - let endOffset = fullText.indexOf(';', cursorOffset); - - if (startOffset === -1) startOffset = 0; - else startOffset += 1; // skip the semicolon - - if (endOffset === -1) endOffset = fullText.length; - - const statement = fullText.substring(startOffset, endOffset).trim(); - if (statement.length > 0) { - const startPos = model.getPositionAt(startOffset); - const endPos = model.getPositionAt(endOffset); - const range = new monaco.Range(startPos.lineNumber, startPos.column, endPos.lineNumber, endPos.column); - return { query: statement, range }; + }, [schemaContext]); + + // Pre-compute schema-based completion items for faster lookups + const schemaCompletionCache = useMemo((): SchemaCompletionCache => { + const tableItems: SchemaCompletionCache["tableItems"] = []; + const columnMap = new Map(); + const allColumns = new Map(); + + parsedSchema.forEach((table) => { + const tableLower = table.name.toLowerCase(); + tableItems.push({ + label: table.name, + labelLower: tableLower, + rowCount: table.rowCount || 0, + columnNames: table.columns?.map((c) => c.name).join(", ") || "", + }); + + const tableColumns: SchemaColumnItem[] = []; + table.columns?.forEach((col) => { + const colItem: SchemaColumnItem = { + label: col.name, + labelLower: col.name.toLowerCase(), + type: col.type, + isPrimary: col.isPrimary || false, + tableName: table.name, + }; + tableColumns.push(colItem); + + // Only store first occurrence for global column suggestions + if (!allColumns.has(col.name)) { + allColumns.set(col.name, colItem); + } + }); + columnMap.set(tableLower, tableColumns); + }); + + return { tableItems, columnMap, allColumns }; + }, [parsedSchema]); + + const handleFormat = () => { + if (!editorRef.current) return; + const currentValue = editorRef.current.getValue(); + if (!currentValue) return; + + try { + let formatted: string; + if (language === "json") { + // JSON formatting for MongoDB queries + const parsed = JSON.parse(currentValue); + formatted = JSON.stringify(parsed, null, 2); + } else if (language === "sql") { + formatted = format(currentValue, { + language: "postgresql", + keywordCase: "upper", + dataTypeCase: "upper", + indentStyle: "tabularLeft", + logicalOperatorNewline: "before", + expressionWidth: 100, + tabWidth: 2, + linesBetweenQueries: 2, + }); + } else { + return; } + editorRef.current.setValue(formatted); + lastSyncedValueRef.current = formatted; + onChange?.(formatted); + } catch (e) { + console.error("Formatting failed:", e); } - } - - return { query: editorValue, range: null }; - }; - - // Track active highlight timeout to prevent race conditions - const highlightTimeoutRef = useRef(null); - const activeDecorationsRef = useRef([]); - - const flashHighlight = (range: Monaco.Range | null) => { - if (!editorRef.current || !monaco || !range) return; - - // Clear any existing highlight first - if (highlightTimeoutRef.current) { - clearTimeout(highlightTimeoutRef.current); - highlightTimeoutRef.current = null; - } - if (activeDecorationsRef.current.length > 0 && editorRef.current) { - editorRef.current.deltaDecorations(activeDecorationsRef.current, []); - activeDecorationsRef.current = []; - } - - // Create new decoration - const decorations = editorRef.current.deltaDecorations([], [ - { - range: range, - options: { - isWholeLine: false, - className: 'executed-query-highlight', - inlineClassName: 'executed-query-inline-highlight' + }; + + const getSelectedText = () => { + if (!editorRef.current) return ""; + const selection = editorRef.current.getSelection(); + const model = editorRef.current.getModel(); + if (!selection || !model) return ""; + return model.getValueInRange(selection); + }; + + const getEffectiveQuery = () => { + const editorValue = editorRef.current?.getValue() || ""; + if (!editorRef.current || !monaco) return { query: editorValue, range: null }; + + const model = editorRef.current.getModel(); + if (!model) return { query: editorValue, range: null }; + + // 1. Check for explicit selection + const selection = editorRef.current.getSelection(); + if (selection) { + const selectedText = model.getValueInRange(selection); + if (selectedText && selectedText.trim().length > 0) { + return { query: selectedText, range: selection }; } } - ]); - activeDecorationsRef.current = decorations; - // Schedule removal with ref tracking for safe cleanup - highlightTimeoutRef.current = setTimeout(() => { - if (editorRef.current && activeDecorationsRef.current.length > 0) { - editorRef.current.deltaDecorations(activeDecorationsRef.current, []); - activeDecorationsRef.current = []; + // 2. If no selection, try to find the current statement (between semicolons) + if (language === "sql") { + const position = editorRef.current.getPosition(); + if (position) { + const fullText = model.getValue(); + const cursorOffset = model.getOffsetAt(position); + + // Find boundaries of the current statement + let startOffset = fullText.lastIndexOf(";", cursorOffset - 1); + let endOffset = fullText.indexOf(";", cursorOffset); + + if (startOffset === -1) startOffset = 0; + else startOffset += 1; // skip the semicolon + + if (endOffset === -1) endOffset = fullText.length; + + const statement = fullText.substring(startOffset, endOffset).trim(); + if (statement.length > 0) { + const startPos = model.getPositionAt(startOffset); + const endPos = model.getPositionAt(endOffset); + const range = new monaco.Range(startPos.lineNumber, startPos.column, endPos.lineNumber, endPos.column); + return { query: statement, range }; + } + } } - highlightTimeoutRef.current = null; - }, 1000); - }; - // Cleanup highlight timeout on unmount - useEffect(() => { - return () => { + return { query: editorValue, range: null }; + }; + + // Track active highlight timeout to prevent race conditions + const highlightTimeoutRef = useRef(null); + const activeDecorationsRef = useRef([]); + + const flashHighlight = (range: Monaco.Range | null) => { + if (!editorRef.current || !monaco || !range) return; + + // Clear any existing highlight first if (highlightTimeoutRef.current) { clearTimeout(highlightTimeoutRef.current); + highlightTimeoutRef.current = null; + } + if (activeDecorationsRef.current.length > 0 && editorRef.current) { + editorRef.current.deltaDecorations(activeDecorationsRef.current, []); + activeDecorationsRef.current = []; } + + // Create new decoration + const decorations = editorRef.current.deltaDecorations( + [], + [ + { + range: range, + options: { + isWholeLine: false, + className: "executed-query-highlight", + inlineClassName: "executed-query-inline-highlight", + }, + }, + ], + ); + activeDecorationsRef.current = decorations; + + // Schedule removal with ref tracking for safe cleanup + highlightTimeoutRef.current = setTimeout(() => { + if (editorRef.current && activeDecorationsRef.current.length > 0) { + editorRef.current.deltaDecorations(activeDecorationsRef.current, []); + activeDecorationsRef.current = []; + } + highlightTimeoutRef.current = null; + }, 1000); }; - }, []); - - // AI Chat hook (must be before useImperativeHandle that references showAi/setShowAi) - const getEditorValue = useCallback(() => editorRef.current?.getValue() || '', []); - const setEditorValueForAi = useCallback((val: string) => { - if (editorRef.current) { - editorRef.current.setValue(val); - lastSyncedValueRef.current = val; - } - }, []); - - const { - showAi, - setShowAi, - aiPrompt, - setAiPrompt, - isAiLoading, - aiError, - setAiError, - aiConversationHistory, - setAiConversationHistory, - handleAiSubmit, - } = useAiChat({ - parsedSchema, - schemaContext, - databaseType, - getEditorValue, - setEditorValue: setEditorValueForAi, - onChange, - onAiChat, - }); - - useImperativeHandle(ref, () => ({ - getSelectedText, - getEffectiveQuery: () => getEffectiveQuery().query, - getValue: () => editorRef.current?.getValue() || '', - setValue: (newValue: string) => { + + // Cleanup highlight timeout on unmount + useEffect(() => { + return () => { + if (highlightTimeoutRef.current) { + clearTimeout(highlightTimeoutRef.current); + } + }; + }, []); + + // AI Chat hook (must be before useImperativeHandle that references showAi/setShowAi) + const getEditorValue = useCallback(() => editorRef.current?.getValue() || "", []); + const setEditorValueForAi = useCallback((val: string) => { if (editorRef.current) { - editorRef.current.setValue(newValue); - lastSyncedValueRef.current = newValue; + editorRef.current.setValue(val); + lastSyncedValueRef.current = val; } - }, - focus: () => editorRef.current?.focus(), - format: handleFormat, - toggleAi: () => setShowAi(!showAi), - })); - - const handleCopy = () => { - const textToCopy = getSelectedText() || editorRef.current?.getValue() || ''; - navigator.clipboard.writeText(textToCopy); - }; - - const handleClear = () => { - if (editorRef.current) { - editorRef.current.setValue(''); - lastSyncedValueRef.current = ''; - onChange?.(''); - } - }; - - // Store original console.error for cleanup - const originalConsoleErrorRef = useRef(null); - - // Cleanup console.error override on unmount - useEffect(() => { - return () => { - if (originalConsoleErrorRef.current) { - console.error = originalConsoleErrorRef.current; - originalConsoleErrorRef.current = null; + }, []); + + const { + showAi, + setShowAi, + aiPrompt, + setAiPrompt, + isAiLoading, + aiError, + setAiError, + aiConversationHistory, + setAiConversationHistory, + handleAiSubmit, + } = useAiChat({ + parsedSchema, + schemaContext, + databaseType, + getEditorValue, + setEditorValue: setEditorValueForAi, + onChange, + onAiChat, + }); + + useImperativeHandle(ref, () => ({ + getSelectedText, + getEffectiveQuery: () => getEffectiveQuery().query, + getValue: () => editorRef.current?.getValue() || "", + setValue: (newValue: string) => { + if (editorRef.current) { + editorRef.current.setValue(newValue); + lastSyncedValueRef.current = newValue; + } + }, + focus: () => editorRef.current?.focus(), + format: handleFormat, + toggleAi: () => setShowAi(!showAi), + })); + + const handleCopy = () => { + const textToCopy = getSelectedText() || editorRef.current?.getValue() || ""; + navigator.clipboard.writeText(textToCopy); + }; + + const handleClear = () => { + if (editorRef.current) { + editorRef.current.setValue(""); + lastSyncedValueRef.current = ""; + onChange?.(""); } }; - }, []); - - const handleBeforeMount = (monacoInstance: typeof Monaco) => { - // Register the LibreDB command language (idempotent) so its tabs highlight - // correctly instead of being treated as JSON. - registerLibreDBLanguage(monacoInstance); - - // Suppress Monaco's "Canceled" errors in console (with cleanup tracking) - if (!originalConsoleErrorRef.current) { - originalConsoleErrorRef.current = console.error; - const originalConsoleError = console.error; - console.error = (...args: unknown[]) => { - const message = args[0]?.toString?.() || ''; - if (message.includes('Canceled') || message.includes('ERR Canceled')) { - return; // Suppress Monaco cancellation errors + + // Store original console.error for cleanup + const originalConsoleErrorRef = useRef(null); + + // Cleanup console.error override on unmount + useEffect(() => { + return () => { + if (originalConsoleErrorRef.current) { + console.error = originalConsoleErrorRef.current; + originalConsoleErrorRef.current = null; } - originalConsoleError.apply(console, args as Parameters); }; - } - - monacoInstance.editor.defineTheme('db-dark', { - base: 'vs-dark', - inherit: true, - rules: [ - { token: 'keyword', foreground: '569cd6', fontStyle: 'bold' }, - { token: 'function', foreground: 'dcdcaa' }, - { token: 'string', foreground: 'ce9178' }, - { token: 'number', foreground: 'b5cea8' }, - { token: 'comment', foreground: '6a9955' }, - { token: 'operator', foreground: 'd4d4d4' }, - { token: 'identifier', foreground: '9cdcfe' }, - ], - colors: { - 'editor.background': '#050505', - 'editor.foreground': '#d4d4d4', - 'editorCursor.foreground': '#569cd6', - 'editor.lineHighlightBackground': '#111111', - 'editorLineNumber.foreground': '#333333', - 'editorLineNumber.activeForeground': '#666666', - 'editor.selectionBackground': '#264f78', - 'editor.inactiveSelectionBackground': '#3a3d41', - 'editorIndentGuide.background': '#1a1a1a', - 'editorIndentGuide.activeBackground': '#333333', + }, []); + + const handleBeforeMount = (monacoInstance: typeof Monaco) => { + // Register the LibreDB command language (idempotent) so its tabs highlight + // correctly instead of being treated as JSON. + registerLibreDBLanguage(monacoInstance); + + // Suppress Monaco's "Canceled" errors in console (with cleanup tracking) + if (!originalConsoleErrorRef.current) { + originalConsoleErrorRef.current = console.error; + const originalConsoleError = console.error; + console.error = (...args: unknown[]) => { + const message = args[0]?.toString?.() || ""; + if (message.includes("Canceled") || message.includes("ERR Canceled")) { + return; // Suppress Monaco cancellation errors + } + originalConsoleError.apply(console, args as Parameters); + }; } - }); - }; - - // SQL completion provider - useEffect(() => { - if (monaco && language === 'sql') { - const disposable = registerSQLCompletionProvider(monaco, schemaCompletionCache); - return () => disposable.dispose(); - } - }, [monaco, language, schemaCompletionCache]); - - // MongoDB JSON completion provider - useEffect(() => { - if (monaco && language === 'json') { - const disposable = registerMongoDBCompletionProvider(monaco, schemaCompletionCache); - return () => disposable.dispose(); - } - }, [monaco, language, schemaCompletionCache]); - - const handleEditorChange = (val: string | undefined) => { - const newValue = val || ''; - // Only call onContentChange if provided (for real-time sync scenarios) - // This avoids the performance hit of updating parent state on every keystroke - onContentChange?.(newValue); - }; - - // Sync to parent on blur (when user leaves the editor) - const handleEditorBlur = () => { - if (editorRef.current) { - const currentValue = editorRef.current.getValue(); - lastSyncedValueRef.current = currentValue; - onChange?.(currentValue); - } - }; - - const handleExecute = () => { - // Sync current content to parent before executing - if (editorRef.current) { - const currentValue = editorRef.current.getValue(); - lastSyncedValueRef.current = currentValue; - onChange?.(currentValue); - } - - const { query, range } = getEffectiveQuery(); - flashHighlight(range); - const event = new CustomEvent('execute-query', { detail: { query } }); - window.dispatchEvent(event); - }; - - - return ( -
    - {/* Dynamic Pro Toolbar - Hidden on mobile */} -
    - {hasSelection && ( + + monacoInstance.editor.defineTheme("db-dark", { + base: "vs-dark", + inherit: true, + rules: [ + { token: "keyword", foreground: "569cd6", fontStyle: "bold" }, + { token: "function", foreground: "dcdcaa" }, + { token: "string", foreground: "ce9178" }, + { token: "number", foreground: "b5cea8" }, + { token: "comment", foreground: "6a9955" }, + { token: "operator", foreground: "d4d4d4" }, + { token: "identifier", foreground: "9cdcfe" }, + ], + colors: { + "editor.background": "#050505", + "editor.foreground": "#d4d4d4", + "editorCursor.foreground": "#569cd6", + "editor.lineHighlightBackground": "#111111", + "editorLineNumber.foreground": "#333333", + "editorLineNumber.activeForeground": "#666666", + "editor.selectionBackground": "#264f78", + "editor.inactiveSelectionBackground": "#3a3d41", + "editorIndentGuide.background": "#1a1a1a", + "editorIndentGuide.activeBackground": "#333333", + }, + }); + }; + + // SQL completion provider + useEffect(() => { + if (monaco && language === "sql") { + const disposable = registerSQLCompletionProvider(monaco, schemaCompletionCache); + return () => disposable.dispose(); + } + }, [monaco, language, schemaCompletionCache]); + + // MongoDB JSON completion provider + useEffect(() => { + if (monaco && language === "json") { + const disposable = registerMongoDBCompletionProvider(monaco, schemaCompletionCache); + return () => disposable.dispose(); + } + }, [monaco, language, schemaCompletionCache]); + + const handleEditorChange = (val: string | undefined) => { + const newValue = val || ""; + // Only call onContentChange if provided (for real-time sync scenarios) + // This avoids the performance hit of updating parent state on every keystroke + onContentChange?.(newValue); + }; + + // Sync to parent on blur (when user leaves the editor) + const handleEditorBlur = () => { + if (editorRef.current) { + const currentValue = editorRef.current.getValue(); + lastSyncedValueRef.current = currentValue; + onChange?.(currentValue); + } + }; + + const handleExecute = () => { + // Sync current content to parent before executing + if (editorRef.current) { + const currentValue = editorRef.current.getValue(); + lastSyncedValueRef.current = currentValue; + onChange?.(currentValue); + } + + const { query, range } = getEffectiveQuery(); + flashHighlight(range); + const event = new CustomEvent("execute-query", { detail: { query } }); + window.dispatchEvent(event); + }; + + return ( +
    + {/* Dynamic Pro Toolbar - Hidden on mobile */} +
    + {hasSelection && ( + + )} + + {(language === "sql" || language === "json") && ( + + )} + - )} - {(language === 'sql' || language === 'json') && ( + +
    + + + + - )} - - - - - -
    - - - - -
    +
    {onExplain && capabilities?.supportsExplain && ( @@ -583,9 +593,9 @@ export const QueryEditor = forwardRef(({
    - {/* Floating AI Input */} - - {showAi && ( + {/* Floating AI Input */} + + {showAi && ( (({ Context: {tables.length} tables
    -
    +
    - - {aiError && ( - -
    -
    - -
    -
    -

    AI Error

    -

    {aiError}

    -
    - + + {aiError && ( + +
    +
    +
    - - )} - - -
    - +
    +

    AI Error

    +

    {aiError}

    +
    + +
    + + )} + + +
    (({
    - )} - - -
    -
    } - onMount={(editor, monaco) => { - editorRef.current = editor; - - // Sync to parent when editor loses focus - editor.onDidBlurEditorText(() => { - handleEditorBlur(); - }); - - editor.onDidChangeCursorSelection(() => { - const selection = editor.getSelection(); - setHasSelection(selection ? !selection.isEmpty() : false); - }); - - // Add custom keyboard shortcut - editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => { - handleExecute(); - }); - - // Add format shortcut - editor.addCommand(monaco.KeyMod.Alt | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, () => { - handleFormat(); - }); - - // Context Menu Actions - editor.addAction({ - id: 'run-query', - label: 'Run Query', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], - contextMenuGroupId: 'navigation', - contextMenuOrder: 1, - run: () => handleExecute() - }); - - if (onExplain) { + )} + + +
    + + +
    + } + onMount={(editor, monaco) => { + editorRef.current = editor; + + // Sync to parent when editor loses focus + editor.onDidBlurEditorText(() => { + handleEditorBlur(); + }); + + editor.onDidChangeCursorSelection(() => { + const selection = editor.getSelection(); + setHasSelection(selection ? !selection.isEmpty() : false); + }); + + // Add custom keyboard shortcut + editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => { + handleExecute(); + }); + + // Add format shortcut + editor.addCommand(monaco.KeyMod.Alt | monaco.KeyMod.Shift | monaco.KeyCode.KeyF, () => { + handleFormat(); + }); + + // Context Menu Actions editor.addAction({ - id: 'explain-query', - label: 'Explain Plan', - contextMenuGroupId: 'navigation', - contextMenuOrder: 2, - run: () => onExplain() + id: "run-query", + label: "Run Query", + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter], + contextMenuGroupId: "navigation", + contextMenuOrder: 1, + run: () => handleExecute(), }); - } - editor.addAction({ - id: 'format-sql', - label: 'Format SQL', - keybindings: [monaco.KeyMod.Alt | monaco.KeyMod.Shift | monaco.KeyCode.KeyF], - contextMenuGroupId: 'modification', - contextMenuOrder: 1, - run: () => handleFormat() - }); - }} - options={getEditorOptions(showLineNumbers)} - /> + if (onExplain) { + editor.addAction({ + id: "explain-query", + label: "Explain Plan", + contextMenuGroupId: "navigation", + contextMenuOrder: 2, + run: () => onExplain(), + }); + } + editor.addAction({ + id: "format-sql", + label: "Format SQL", + keybindings: [monaco.KeyMod.Alt | monaco.KeyMod.Shift | monaco.KeyCode.KeyF], + contextMenuGroupId: "modification", + contextMenuOrder: 1, + run: () => handleFormat(), + }); + }} + options={getEditorOptions(showLineNumbers)} + /> +
    -
    - ); -}); + ); + }, +); -QueryEditor.displayName = 'QueryEditor'; +QueryEditor.displayName = "QueryEditor"; diff --git a/src/components/QueryHistory.tsx b/src/components/QueryHistory.tsx index 99705617..5cafd7d9 100644 --- a/src/components/QueryHistory.tsx +++ b/src/components/QueryHistory.tsx @@ -1,18 +1,25 @@ "use client"; -import React, { useState, useEffect, useMemo } from 'react'; -import { storage } from '@/lib/storage'; -import { QueryHistoryItem } from '@/lib/types'; -import { - CheckCircle2, AlertCircle, - RotateCcw, Trash2, Search, Download, - ArrowUpDown, Hash, - Database, History as HistoryIcon, X -} from 'lucide-react'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { cn } from '@/lib/utils'; -import { format } from 'date-fns'; +import React, { useState, useEffect, useMemo } from "react"; +import { storage } from "@/lib/storage"; +import { QueryHistoryItem } from "@/lib/types"; +import { + CheckCircle2, + AlertCircle, + RotateCcw, + Trash2, + Search, + Download, + ArrowUpDown, + Hash, + Database, + History as HistoryIcon, + X, +} from "lucide-react"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { cn } from "@/lib/utils"; +import { format } from "date-fns"; import { DropdownMenu, DropdownMenuContent, @@ -26,16 +33,16 @@ interface QueryHistoryProps { refreshTrigger?: number; } -type SortField = 'executedAt' | 'executionTime' | 'rowCount'; -type SortOrder = 'asc' | 'desc'; +type SortField = "executedAt" | "executionTime" | "rowCount"; +type SortOrder = "asc" | "desc"; export function QueryHistory({ onSelectQuery, activeConnectionId, refreshTrigger }: QueryHistoryProps) { const [history, setHistory] = useState([]); - const [search, setSearch] = useState(''); - const [filterStatus, setFilterStatus] = useState<'all' | 'success' | 'error'>('all'); + const [search, setSearch] = useState(""); + const [filterStatus, setFilterStatus] = useState<"all" | "success" | "error">("all"); const [isGlobal, setIsGlobal] = useState(false); - const [sortField, setSortField] = useState('executedAt'); - const [sortOrder, setSortOrder] = useState('desc'); + const [sortField, setSortField] = useState("executedAt"); + const [sortOrder, setSortOrder] = useState("desc"); // Refresh history when refreshTrigger changes (replaces key-based re-mount) useEffect(() => { @@ -43,32 +50,35 @@ export function QueryHistory({ onSelectQuery, activeConnectionId, refreshTrigger }, [refreshTrigger]); const filteredHistory = useMemo(() => { - return history.filter(item => { - const matchesSearch = item.query.toLowerCase().includes(search.toLowerCase()) || - item.connectionName?.toLowerCase().includes(search.toLowerCase()) || - item.tabName?.toLowerCase().includes(search.toLowerCase()); - const matchesStatus = filterStatus === 'all' || item.status === filterStatus; - const matchesConnection = isGlobal || !activeConnectionId || item.connectionId === activeConnectionId; - return matchesSearch && matchesStatus && matchesConnection; - }).sort((a, b) => { - let valA: number = 0; - let valB: number = 0; - - if (sortField === 'executedAt') { - valA = a.executedAt ? new Date(a.executedAt).getTime() : 0; - valB = b.executedAt ? new Date(b.executedAt).getTime() : 0; - } else { - valA = (a[sortField] as number) || 0; - valB = (b[sortField] as number) || 0; - } + return history + .filter((item) => { + const matchesSearch = + item.query.toLowerCase().includes(search.toLowerCase()) || + item.connectionName?.toLowerCase().includes(search.toLowerCase()) || + item.tabName?.toLowerCase().includes(search.toLowerCase()); + const matchesStatus = filterStatus === "all" || item.status === filterStatus; + const matchesConnection = isGlobal || !activeConnectionId || item.connectionId === activeConnectionId; + return matchesSearch && matchesStatus && matchesConnection; + }) + .sort((a, b) => { + let valA: number = 0; + let valB: number = 0; + + if (sortField === "executedAt") { + valA = a.executedAt ? new Date(a.executedAt).getTime() : 0; + valB = b.executedAt ? new Date(b.executedAt).getTime() : 0; + } else { + valA = (a[sortField] as number) || 0; + valB = (b[sortField] as number) || 0; + } - if (sortOrder === 'asc') return valA > valB ? 1 : -1; - return valA < valB ? 1 : -1; - }); + if (sortOrder === "asc") return valA > valB ? 1 : -1; + return valA < valB ? 1 : -1; + }); }, [history, search, filterStatus, isGlobal, activeConnectionId, sortField, sortOrder]); const handleClearHistory = () => { - if (confirm('Are you sure you want to clear all history?')) { + if (confirm("Are you sure you want to clear all history?")) { storage.clearHistory(); setHistory([]); } @@ -76,40 +86,42 @@ export function QueryHistory({ onSelectQuery, activeConnectionId, refreshTrigger const handleSort = (field: SortField) => { if (sortField === field) { - setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); } else { setSortField(field); - setSortOrder('desc'); + setSortOrder("desc"); } }; - const exportHistory = (format: 'csv' | 'json') => { - let content = ''; - let mimeType = ''; + const exportHistory = (format: "csv" | "json") => { + let content = ""; + let mimeType = ""; const fileName = `query_history_${new Date().getTime()}.${format}`; - if (format === 'csv') { - const headers = ['Executed At', 'Status', 'Connection', 'Tab', 'Execution Time (ms)', 'Rows', 'Query', 'Error']; - const rows = filteredHistory.map(item => [ - item.executedAt, - item.status, - item.connectionName || item.connectionId, - item.tabName || '', - item.executionTime, - item.rowCount || 0, - `"${item.query.replace(/"/g, '""')}"`, - `"${(item.errorMessage || '').replace(/"/g, '""')}"` - ].join(',')); - content = [headers.join(','), ...rows].join('\n'); - mimeType = 'text/csv'; + if (format === "csv") { + const headers = ["Executed At", "Status", "Connection", "Tab", "Execution Time (ms)", "Rows", "Query", "Error"]; + const rows = filteredHistory.map((item) => + [ + item.executedAt, + item.status, + item.connectionName || item.connectionId, + item.tabName || "", + item.executionTime, + item.rowCount || 0, + `"${item.query.replace(/"/g, '""')}"`, + `"${(item.errorMessage || "").replace(/"/g, '""')}"`, + ].join(","), + ); + content = [headers.join(","), ...rows].join("\n"); + mimeType = "text/csv"; } else { content = JSON.stringify(filteredHistory, null, 2); - mimeType = 'application/json'; + mimeType = "application/json"; } const blob = new Blob([content], { type: mimeType }); const url = URL.createObjectURL(blob); - const link = document.createElement('a'); + const link = document.createElement("a"); link.href = url; link.download = fileName; link.click(); @@ -125,68 +137,70 @@ export function QueryHistory({ onSelectQuery, activeConnectionId, refreshTrigger
    -

    - Query History -

    -

    - Showing {filteredHistory.length} executions -

    +

    Query History

    +

    Showing {filteredHistory.length} executions

    - +
    - - exportHistory('csv')} className="text-xs cursor-pointer"> + exportHistory("csv")} className="text-xs cursor-pointer"> Export as CSV - exportHistory('json')} className="text-xs cursor-pointer"> + exportHistory("json")} className="text-xs cursor-pointer"> Export as JSON -
    - +
    - setSearch(e.target.value)} className="pl-9 h-9 bg-white/5 border-white/10 text-xs focus:ring-emerald-500/20 rounded-lg" /> {search && ( - )}
    - +
    - {(['all', 'success', 'error'] as const).map((status) => ( + {(["all", "success", "error"] as const).map((status) => (
    - - - @@ -257,13 +303,13 @@ export function QueryHistory({ onSelectQuery, activeConnectionId, refreshTrigger {filteredHistory.map((item) => ( - @@ -288,11 +334,11 @@ export function QueryHistory({ onSelectQuery, activeConnectionId, refreshTrigger
    - {item.connectionName || 'Unknown'} + {item.connectionName || "Unknown"}
    - {item.tabName || 'Default Tab'} + {item.tabName || "Default Tab"}
    @@ -309,22 +355,24 @@ export function QueryHistory({ onSelectQuery, activeConnectionId, refreshTrigger
    + {parsedData.headers.map((h) => ( + {h}
    + {cell || NULL}
    {col.name} {col.type} {col.isPrimary && PK} {col.nullable !== false ? 'Yes' : 'No'}{col.nullable !== false ? "Yes" : "No"}
    {rowField} - {ck === '__all__' ? `${AGG_LABELS[aggFunction]}(${valueField || '*'})` : ck} + {pivotData.colKeys.map((ck) => ( + + {ck === "__all__" ? `${AGG_LABELS[aggFunction]}(${valueField || "*"})` : ck}
    - {row.rowKey} - {row.rowKey} - {row.values.get(ck) || '0'} + {row.values.get(ck) || "0"}
    Status handleSort('executedAt')}> + handleSort("executedAt")} + >
    Executed At - +
    Source SQL Query handleSort('executionTime')}> + handleSort("executionTime")} + >
    Duration - +
    handleSort('rowCount')}> + handleSort("rowCount")} + >
    Rows - +
    - {item.status === 'success' ? ( + {item.status === "success" ? (
    @@ -277,10 +323,10 @@ export function QueryHistory({ onSelectQuery, activeConnectionId, refreshTrigger
    - {item.executedAt ? format(new Date(item.executedAt), 'MMM d, HH:mm:ss') : '-'} + {item.executedAt ? format(new Date(item.executedAt), "MMM d, HH:mm:ss") : "-"} - {item.executedAt ? format(new Date(item.executedAt), 'yyyy') : ''} + {item.executedAt ? format(new Date(item.executedAt), "yyyy") : ""}
    - 500 ? "text-amber-400 bg-amber-400/10" : "text-zinc-400 bg-white/5" - )}> + 500 ? "text-amber-400 bg-amber-400/10" : "text-zinc-400 bg-white/5", + )} + > {item.executionTime}ms - {item.rowCount != null ? item.rowCount.toLocaleString() : '-'} + {item.rowCount != null ? item.rowCount.toLocaleString() : "-"} - @@ -286,5 +328,5 @@ export function isDangerousQuery(query: string): boolean { if (/^\s*DELETE\b/i.test(normalized) && !/\bWHERE\b/.test(normalized)) return true; if (/\bUPDATE\b[\s\S]*?\bSET\b/i.test(normalized) && !/\bWHERE\b/.test(normalized)) return true; - return patterns.some(p => p.test(query)); + return patterns.some((p) => p.test(query)); } diff --git a/src/components/ResultsGrid.tsx b/src/components/ResultsGrid.tsx index f2fa9bfe..d3d9ec1b 100644 --- a/src/components/ResultsGrid.tsx +++ b/src/components/ResultsGrid.tsx @@ -1,7 +1,7 @@ "use client"; -import React, { useMemo, useState, useRef, useCallback, useEffect } from 'react'; -import { QueryResult } from '@/lib/types'; +import React, { useMemo, useState, useRef, useCallback, useEffect } from "react"; +import { QueryResult } from "@/lib/types"; import { flexRender, getCoreRowModel, @@ -9,17 +9,10 @@ import { getSortedRowModel, SortingState, ColumnDef, -} from '@tanstack/react-table'; -import { useVirtualizer } from '@tanstack/react-virtual'; -import { cn } from '@/lib/utils'; -import { - ArrowUpDown, - ArrowUp, - ArrowDown, - Eye, - Filter, - Lock, -} from 'lucide-react'; +} from "@tanstack/react-table"; +import { useVirtualizer } from "@tanstack/react-virtual"; +import { cn } from "@/lib/utils"; +import { ArrowUpDown, ArrowUp, ArrowDown, Eye, Filter, Lock } from "lucide-react"; import { type MaskingConfig, detectSensitiveColumnsFromConfig, @@ -28,11 +21,11 @@ import { canToggleMasking, canReveal, loadMaskingConfig, -} from '@/lib/data-masking'; -import { ResultCard } from '@/components/results-grid/ResultCard'; -import { RowDetailSheet } from '@/components/results-grid/RowDetailSheet'; -import { StatsBar, LoadMoreFooter } from '@/components/results-grid/StatsBar'; -import { formatCellValue } from '@/components/results-grid/utils'; +} from "@/lib/data-masking"; +import { ResultCard } from "@/components/results-grid/ResultCard"; +import { RowDetailSheet } from "@/components/results-grid/RowDetailSheet"; +import { StatsBar, LoadMoreFooter } from "@/components/results-grid/StatsBar"; +import { formatCellValue } from "@/components/results-grid/utils"; export interface CellChange { rowIndex: number; @@ -59,11 +52,11 @@ interface ResultsGridProps { // Detect primary column (first text-like column that's not an ID) function detectPrimaryColumn(fields: string[], rows: Record[]): string { - const preferredNames = ['name', 'title', 'label', 'username', 'email', 'description']; + const preferredNames = ["name", "title", "label", "username", "email", "description"]; for (const name of preferredNames) { - if (fields.some(f => f.toLowerCase().includes(name))) { - return fields.find(f => f.toLowerCase().includes(name))!; + if (fields.some((f) => f.toLowerCase().includes(name))) { + return fields.find((f) => f.toLowerCase().includes(name))!; } } @@ -71,7 +64,7 @@ function detectPrimaryColumn(fields: string[], rows: Record[]): if (rows.length > 0) { for (const field of fields) { const value = rows[0][field]; - if (typeof value === 'string' && !field.toLowerCase().includes('id')) { + if (typeof value === "string" && !field.toLowerCase().includes("id")) { return field; } } @@ -82,7 +75,7 @@ function detectPrimaryColumn(fields: string[], rows: Record[]): // Get ID column if exists function detectIdColumn(fields: string[]): string | null { - return fields.find(f => f.toLowerCase() === 'id' || f.toLowerCase().endsWith('_id')) || null; + return fields.find((f) => f.toLowerCase() === "id" || f.toLowerCase().endsWith("_id")) || null; } export function ResultsGrid({ @@ -100,9 +93,9 @@ export function ResultsGrid({ onApplyChanges, }: ResultsGridProps) { const [sorting, setSorting] = useState([]); - const [editingCell, setEditingCell] = useState<{ rowIndex: number, columnId: string } | null>(null); + const [editingCell, setEditingCell] = useState<{ rowIndex: number; columnId: string } | null>(null); const [editValue, setEditValue] = useState(""); - const [viewMode, setViewMode] = useState<'card' | 'table'>('card'); + const [viewMode, setViewMode] = useState<"card" | "table">("card"); const [selectedRow, setSelectedRow] = useState<{ row: Record; index: number } | null>(null); const [columnFilters, setColumnFilters] = useState>(new Map()); const [activeFilterCol, setActiveFilterCol] = useState(null); @@ -122,7 +115,7 @@ export function ResultsGrid({ // Config-based sensitive column detection const sensitiveColumns = useMemo( () => detectSensitiveColumnsFromConfig(result.fields, resolvedConfig), - [result.fields, resolvedConfig] + [result.fields, resolvedConfig], ); const hasSensitive = sensitiveColumns.size > 0; @@ -134,9 +127,9 @@ export function ResultsGrid({ // Per-cell reveal with auto-hide const revealCell = useCallback((key: string) => { - setRevealedCells(prev => new Set(prev).add(key)); + setRevealedCells((prev) => new Set(prev).add(key)); setTimeout(() => { - setRevealedCells(prev => { + setRevealedCells((prev) => { const next = new Set(prev); next.delete(key); return next; @@ -144,23 +137,17 @@ export function ResultsGrid({ }, 10000); }, []); - const primaryColumn = useMemo( - () => detectPrimaryColumn(result.fields, result.rows), - [result.fields, result.rows] - ); + const primaryColumn = useMemo(() => detectPrimaryColumn(result.fields, result.rows), [result.fields, result.rows]); - const idColumn = useMemo( - () => detectIdColumn(result.fields), - [result.fields] - ); + const idColumn = useMemo(() => detectIdColumn(result.fields), [result.fields]); // Filter rows based on column filters const filteredRows = useMemo(() => { if (columnFilters.size === 0) return result.rows; - return result.rows.filter(row => { + return result.rows.filter((row) => { for (const [col, filterVal] of columnFilters) { if (!filterVal) continue; - const cellVal = String(row[col] ?? '').toLowerCase(); + const cellVal = String(row[col] ?? "").toLowerCase(); if (!cellVal.includes(filterVal.toLowerCase())) return false; } return true; @@ -176,9 +163,12 @@ export function ResultsGrid({ }, [columnFilters]); // Check if a cell has a pending change - const getCellChange = useCallback((rowIndex: number, columnId: string): CellChange | undefined => { - return pendingChanges?.find(c => c.rowIndex === rowIndex && c.columnId === columnId); - }, [pendingChanges]); + const getCellChange = useCallback( + (rowIndex: number, columnId: string): CellChange | undefined => { + return pendingChanges?.find((c) => c.rowIndex === rowIndex && c.columnId === columnId); + }, + [pendingChanges], + ); const handleClearFilters = useCallback(() => { setColumnFilters(new Map()); @@ -186,7 +176,7 @@ export function ResultsGrid({ }, []); const columns = useMemo>[]>(() => { - return result.fields.map(field => ({ + return result.fields.map((field) => ({ accessorKey: field, header: ({ column }) => { const hasFilter = columnFilters.has(field) && !!columnFilters.get(field); @@ -199,7 +189,9 @@ export function ResultsGrid({ > {field} {isSensitive && ( - + + + )}
    {column.getIsSorted() === "asc" ? ( @@ -214,9 +206,14 @@ export function ResultsGrid({