From dd47cc79856f0c6eefbbdd1b46f9b3672ce60c9f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 30 Mar 2026 12:49:09 +0000 Subject: [PATCH 1/2] Extract pivot engine and React components into standalone npm workspace packages - Add `@utils-foo/pivot-engine`: zero-dependency headless pivot engine with aggregators, sorters, PivotEngine class, and a plugin API for custom aggregation types (`registerAggregator`) - Add `@utils-foo/pivot-react`: React component library (PivotTable, PivotGrid, ConfigPanel, DataInput, FilterModal) with PapaParse CSV support - Set up npm workspaces with Vite aliases and tsconfig paths for dev-time source resolution (no separate build step) - Add 110 unit tests for engine (sorters, aggregators, PivotEngine) - Reduce `src/tools/pivot-table/index.tsx` to a thin wrapper around PivotTable https://claude.ai/code/session_01Gq6D8TLHT6eW3Gh8ZzFWa3 --- package-lock.json | 2671 ++++++++++++++--- package.json | 11 +- packages/pivot-engine/package.json | 18 + .../src/__tests__/PivotEngine.test.ts | 326 ++ .../src/__tests__/aggregators.test.ts | 366 +++ .../src/__tests__/sorters.test.ts | 174 ++ .../pivot-engine/src}/engine/PivotEngine.ts | 47 +- .../pivot-engine/src}/engine/aggregators.ts | 105 +- .../pivot-engine/src/engine/analyzeData.ts | 60 + .../pivot-engine/src}/engine/sorters.ts | 5 +- packages/pivot-engine/src/index.ts | 60 + .../pivot-engine/src}/types.ts | 43 +- packages/pivot-engine/tsconfig.json | 19 + packages/pivot-engine/vitest.config.ts | 14 + packages/pivot-react/package.json | 18 + .../src}/components/ConfigPanel.tsx | 97 +- .../pivot-react/src}/components/DataInput.tsx | 5 +- .../src}/components/FilterModal.tsx | 38 +- .../pivot-react/src}/components/PivotGrid.tsx | 18 +- .../pivot-react/src/components/PivotTable.tsx | 132 + .../pivot-react/src/hooks/usePivotData.ts | 26 + packages/pivot-react/src/index.ts | 21 + packages/pivot-react/src/lib/parseCsv.ts | 38 + packages/pivot-react/src/lib/utils.ts | 6 + packages/pivot-react/src/ui/Button.tsx | 31 + packages/pivot-react/src/ui/Card.tsx | 40 + packages/pivot-react/src/ui/Checkbox.tsx | 43 + packages/pivot-react/src/ui/EmptyState.tsx | 31 + packages/pivot-react/src/ui/Modal.tsx | 66 + packages/pivot-react/src/ui/Toggle.tsx | 38 + packages/pivot-react/tsconfig.json | 24 + src/tools/pivot-table/hooks/usePivotData.ts | 89 - src/tools/pivot-table/index.tsx | 173 +- tsconfig.json | 9 +- vite.config.ts | 8 + 35 files changed, 4115 insertions(+), 755 deletions(-) create mode 100644 packages/pivot-engine/package.json create mode 100644 packages/pivot-engine/src/__tests__/PivotEngine.test.ts create mode 100644 packages/pivot-engine/src/__tests__/aggregators.test.ts create mode 100644 packages/pivot-engine/src/__tests__/sorters.test.ts rename {src/tools/pivot-table => packages/pivot-engine/src}/engine/PivotEngine.ts (92%) rename {src/tools/pivot-table => packages/pivot-engine/src}/engine/aggregators.ts (69%) create mode 100644 packages/pivot-engine/src/engine/analyzeData.ts rename {src/tools/pivot-table => packages/pivot-engine/src}/engine/sorters.ts (97%) create mode 100644 packages/pivot-engine/src/index.ts rename {src/tools/pivot-table => packages/pivot-engine/src}/types.ts (80%) create mode 100644 packages/pivot-engine/tsconfig.json create mode 100644 packages/pivot-engine/vitest.config.ts create mode 100644 packages/pivot-react/package.json rename {src/tools/pivot-table => packages/pivot-react/src}/components/ConfigPanel.tsx (86%) rename {src/tools/pivot-table => packages/pivot-react/src}/components/DataInput.tsx (93%) rename {src/tools/pivot-table => packages/pivot-react/src}/components/FilterModal.tsx (80%) rename {src/tools/pivot-table => packages/pivot-react/src}/components/PivotGrid.tsx (95%) create mode 100644 packages/pivot-react/src/components/PivotTable.tsx create mode 100644 packages/pivot-react/src/hooks/usePivotData.ts create mode 100644 packages/pivot-react/src/index.ts create mode 100644 packages/pivot-react/src/lib/parseCsv.ts create mode 100644 packages/pivot-react/src/lib/utils.ts create mode 100644 packages/pivot-react/src/ui/Button.tsx create mode 100644 packages/pivot-react/src/ui/Card.tsx create mode 100644 packages/pivot-react/src/ui/Checkbox.tsx create mode 100644 packages/pivot-react/src/ui/EmptyState.tsx create mode 100644 packages/pivot-react/src/ui/Modal.tsx create mode 100644 packages/pivot-react/src/ui/Toggle.tsx create mode 100644 packages/pivot-react/tsconfig.json delete mode 100644 src/tools/pivot-table/hooks/usePivotData.ts diff --git a/package-lock.json b/package-lock.json index 5ef9da3..afbd076 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "utils.foo", "version": "1.0.0", + "workspaces": [ + "packages/*" + ], "dependencies": { "@codemirror/commands": "^6.10.3", "@codemirror/lang-json": "^6.0.2", @@ -20,6 +23,8 @@ "@types/js-yaml": "^4.0.9", "@types/papaparse": "^5.5.2", "@types/qrcode": "^1.5.6", + "@utils-foo/pivot-engine": "*", + "@utils-foo/pivot-react": "*", "beautiful-mermaid": "^1.1.3", "clsx": "^2.1.1", "codemirror": "^6.0.2", @@ -57,6 +62,20 @@ "vite": "^8.0.3" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -306,6 +325,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@codemirror/autocomplete": { "version": "6.20.1", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", @@ -763,188 +792,743 @@ "tslib": "^2.4.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.3", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.1", - "levn": "^0.4.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, @@ -1172,65 +1756,263 @@ "@lezer/lr": "^1.0.0" } }, - "node_modules/@lezer/xml": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", - "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "node_modules/@lezer/xml": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", + "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@lezer/yaml": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", - "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.4.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "license": "MIT" - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-android-arm64": { + "node_modules/@rolldown/binding-openharmony-arm64": { "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", "cpu": [ "arm64" ], @@ -1238,16 +2020,33 @@ "license": "MIT", "optional": true, "os": [ - "android" + "openharmony" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-darwin-arm64": { + "node_modules/@rolldown/binding-wasm32-wasi": { "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", "cpu": [ "arm64" ], @@ -1255,16 +2054,16 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-darwin-x64": { + "node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", "cpu": [ "x64" ], @@ -1272,16 +2071,93 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "win32" ], "engines": { "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", "cpu": [ "x64" ], @@ -1289,153 +2165,209 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", "cpu": [ - "arm" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", "cpu": [ - "arm64" + "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", "cpu": [ - "arm64" + "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", "cpu": [ - "ppc64" + "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "cpu": [ - "s390x" + "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "openbsd" + ] }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", "cpu": [ "arm64" ], @@ -1444,49 +2376,40 @@ "optional": true, "os": [ "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", "cpu": [ - "wasm32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } + "os": [ + "win32" + ] }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", "cpu": [ "x64" ], @@ -1495,17 +2418,21 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.7", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", - "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@tailwindcss/node": { "version": "4.2.2", @@ -1640,9 +2567,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1660,9 +2584,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1680,9 +2601,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1700,9 +2618,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1802,6 +2717,24 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/diff": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", @@ -2126,6 +3059,14 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@utils-foo/pivot-engine": { + "resolved": "packages/pivot-engine", + "link": true + }, + "node_modules/@utils-foo/pivot-react": { + "resolved": "packages/pivot-react", + "link": true + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", @@ -2152,6 +3093,155 @@ } } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2222,6 +3312,35 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.27", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", @@ -2339,6 +3458,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -2369,6 +3498,33 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -2516,6 +3672,16 @@ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2548,6 +3714,13 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/echarts": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", @@ -2623,6 +3796,55 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2814,6 +4036,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2824,6 +4056,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2948,6 +4190,23 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", @@ -2995,6 +4254,28 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3008,6 +4289,39 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -3015,6 +4329,16 @@ "dev": true, "license": "ISC" }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -3032,6 +4356,13 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3091,6 +4422,76 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/javascript-natural-sort": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", @@ -3367,9 +4768,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3391,9 +4789,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3415,9 +4810,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3439,9 +4831,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -3513,6 +4902,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3542,6 +4938,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/marked": { "version": "17.0.5", "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.5.tgz", @@ -3593,6 +5030,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3692,6 +5139,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/papaparse": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", @@ -3732,6 +5186,47 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3949,6 +5444,51 @@ "dev": true, "license": "MIT" }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -4006,6 +5546,26 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/size-sensor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.3.tgz", @@ -4034,6 +5594,20 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -4048,18 +5622,68 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/strnum": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", @@ -4078,6 +5702,19 @@ "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "license": "MIT" }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tailwind-merge": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", @@ -4109,12 +5746,41 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tiny-emitter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -4132,6 +5798,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -4338,6 +6034,252 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", @@ -4366,6 +6308,23 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -4390,6 +6349,25 @@ "node": ">=8" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -4540,6 +6518,27 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", "license": "0BSD" + }, + "packages/pivot-engine": { + "name": "@utils-foo/pivot-engine", + "version": "0.1.0", + "devDependencies": { + "@vitest/coverage-v8": "^3.2.0", + "vitest": "^3.2.0" + } + }, + "packages/pivot-react": { + "name": "@utils-foo/pivot-react", + "version": "0.1.0", + "dependencies": { + "@types/papaparse": "^5.5.2", + "@utils-foo/pivot-engine": "*", + "papaparse": "^5.5.3" + }, + "peerDependencies": { + "lucide-react": "^1.0.0", + "react": "^19.0.0" + } } } } diff --git a/package.json b/package.json index 5df8e8c..7568018 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,17 @@ "private": true, "version": "1.0.0", "type": "module", + "workspaces": [ + "packages/*" + ], "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", - "lint": "eslint src", - "lint:fix": "eslint src --fix" + "lint": "eslint src packages", + "lint:fix": "eslint src packages --fix", + "test": "npm run test --workspace=packages/pivot-engine", + "test:watch": "npm run test:watch --workspace=packages/pivot-engine" }, "dependencies": { "@codemirror/commands": "^6.10.3", @@ -23,6 +28,8 @@ "@types/js-yaml": "^4.0.9", "@types/papaparse": "^5.5.2", "@types/qrcode": "^1.5.6", + "@utils-foo/pivot-engine": "*", + "@utils-foo/pivot-react": "*", "beautiful-mermaid": "^1.1.3", "clsx": "^2.1.1", "codemirror": "^6.0.2", diff --git a/packages/pivot-engine/package.json b/packages/pivot-engine/package.json new file mode 100644 index 0000000..9af6059 --- /dev/null +++ b/packages/pivot-engine/package.json @@ -0,0 +1,18 @@ +{ + "name": "@utils-foo/pivot-engine", + "private": true, + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" + }, + "devDependencies": { + "@vitest/coverage-v8": "^3.2.0", + "vitest": "^3.2.0" + } +} diff --git a/packages/pivot-engine/src/__tests__/PivotEngine.test.ts b/packages/pivot-engine/src/__tests__/PivotEngine.test.ts new file mode 100644 index 0000000..a33da4f --- /dev/null +++ b/packages/pivot-engine/src/__tests__/PivotEngine.test.ts @@ -0,0 +1,326 @@ +import { describe, it, expect } from 'vitest' +import { PivotEngine, getHeatmapColor } from '../engine/PivotEngine' +import { flattenKey, compositeKey } from '../engine/sorters' +import type { PivotConfig, DataRecord } from '../types' + +// ─── Fixtures ───────────────────────────────────────────────────────────────── + +const RECORDS: DataRecord[] = [ + { region: 'North', product: 'A', sales: 100, units: 10 }, + { region: 'North', product: 'B', sales: 200, units: 20 }, + { region: 'South', product: 'A', sales: 150, units: 15 }, + { region: 'South', product: 'B', sales: 300, units: 30 }, +] + +const BASE_CONFIG: PivotConfig = { + rows: ['region'], + cols: ['product'], + values: [{ field: 'sales', aggregation: 'sum' }], + filters: [], + rowOrder: 'key_asc', + colOrder: 'key_asc', + heatmap: 'none', + showRowTotals: true, + showColTotals: true, + showGrandTotal: true, +} + +function getCell( + result: ReturnType, + rowKey: string[], + colKey: string[] +): number | null { + const key = compositeKey(flattenKey(rowKey), flattenKey(colKey)) + return result.cells.get(key)?.values[0] ?? null +} + +// ─── Basic 2D Pivot ─────────────────────────────────────────────────────────── + +describe('PivotEngine — basic 2D pivot', () => { + const engine = new PivotEngine(RECORDS, BASE_CONFIG) + const result = engine.getResult() + + it('produces correct rowKeys (sorted ascending)', () => { + expect(result.rowKeys).toEqual([['North'], ['South']]) + }) + + it('produces correct colKeys (sorted ascending)', () => { + expect(result.colKeys).toEqual([['A'], ['B']]) + }) + + it('computes correct cell values', () => { + expect(getCell(result, ['North'], ['A'])).toBe(100) + expect(getCell(result, ['North'], ['B'])).toBe(200) + expect(getCell(result, ['South'], ['A'])).toBe(150) + expect(getCell(result, ['South'], ['B'])).toBe(300) + }) + + it('computes correct row totals', () => { + expect(result.rowTotals.get(flattenKey(['North']))?.values[0]).toBe(300) + expect(result.rowTotals.get(flattenKey(['South']))?.values[0]).toBe(450) + }) + + it('computes correct column totals', () => { + expect(result.colTotals.get(flattenKey(['A']))?.values[0]).toBe(250) + expect(result.colTotals.get(flattenKey(['B']))?.values[0]).toBe(500) + }) + + it('computes correct grand total', () => { + expect(result.grandTotal.values[0]).toBe(750) + }) + + it('isEmpty is false', () => { + expect(result.isEmpty).toBe(false) + }) +}) + +// ─── Rows Only ──────────────────────────────────────────────────────────────── + +describe('PivotEngine — rows only (no cols)', () => { + const config: PivotConfig = { ...BASE_CONFIG, cols: [] } + const result = new PivotEngine(RECORDS, config).getResult() + + it('has no colKeys', () => { + expect(result.colKeys).toEqual([]) + }) + + it('has rowTotals populated', () => { + expect(result.rowTotals.get(flattenKey(['North']))?.values[0]).toBe(300) + }) + + it('cells map is empty', () => { + expect(result.cells.size).toBe(0) + }) +}) + +// ─── Cols Only ──────────────────────────────────────────────────────────────── + +describe('PivotEngine — cols only (no rows)', () => { + const config: PivotConfig = { ...BASE_CONFIG, rows: [] } + const result = new PivotEngine(RECORDS, config).getResult() + + it('has no rowKeys', () => { + expect(result.rowKeys).toEqual([]) + }) + + it('has colTotals populated', () => { + expect(result.colTotals.get(flattenKey(['A']))?.values[0]).toBe(250) + }) + + it('cells map is empty', () => { + expect(result.cells.size).toBe(0) + }) +}) + +// ─── Empty Records ──────────────────────────────────────────────────────────── + +describe('PivotEngine — empty records', () => { + const result = new PivotEngine([], BASE_CONFIG).getResult() + + it('isEmpty is true', () => { + expect(result.isEmpty).toBe(true) + }) + + it('has no rowKeys or colKeys', () => { + expect(result.rowKeys).toEqual([]) + expect(result.colKeys).toEqual([]) + }) +}) + +// ─── Multiple Values ────────────────────────────────────────────────────────── + +describe('PivotEngine — multiple value configs', () => { + const config: PivotConfig = { + ...BASE_CONFIG, + values: [ + { field: 'sales', aggregation: 'sum' }, + { field: 'units', aggregation: 'sum' }, + ], + } + const result = new PivotEngine(RECORDS, config).getResult() + + it('produces two values per cell', () => { + const cell = result.cells.get(compositeKey(flattenKey(['North']), flattenKey(['A']))) + expect(cell?.values).toHaveLength(2) + expect(cell?.values[0]).toBe(100) + expect(cell?.values[1]).toBe(10) + }) + + it('grand total has two values', () => { + expect(result.grandTotal.values[0]).toBe(750) + expect(result.grandTotal.values[1]).toBe(75) + }) +}) + +// ─── Filters ────────────────────────────────────────────────────────────────── + +describe('PivotEngine — filters', () => { + const config: PivotConfig = { + ...BASE_CONFIG, + filters: [{ field: 'region', excludedValues: new Set(['South']) }], + } + const result = new PivotEngine(RECORDS, config).getResult() + + it('excludes South rows', () => { + expect(result.rowKeys).toEqual([['North']]) + }) + + it('adjusts grand total', () => { + expect(result.grandTotal.values[0]).toBe(300) // only North + }) + + it('excludes South cells', () => { + expect(getCell(result, ['South'], ['A'])).toBeNull() + }) +}) + +// ─── Sort Orders ────────────────────────────────────────────────────────────── + +describe('PivotEngine — sort orders', () => { + it('key_desc sorts rows Z→A', () => { + const config: PivotConfig = { ...BASE_CONFIG, rowOrder: 'key_desc' } + const result = new PivotEngine(RECORDS, config).getResult() + expect(result.rowKeys[0]).toEqual(['South']) + expect(result.rowKeys[1]).toEqual(['North']) + }) + + it('value_desc sorts rows by descending total', () => { + const config: PivotConfig = { ...BASE_CONFIG, rowOrder: 'value_desc' } + const result = new PivotEngine(RECORDS, config).getResult() + // South total=450, North total=300 → South first when descending + expect(result.rowKeys[0]).toEqual(['South']) + }) + + it('value_asc sorts rows by ascending total', () => { + const config: PivotConfig = { ...BASE_CONFIG, rowOrder: 'value_asc' } + const result = new PivotEngine(RECORDS, config).getResult() + expect(result.rowKeys[0]).toEqual(['North']) + }) +}) + +// ─── Derived Aggregations (pct) ─────────────────────────────────────────────── + +describe('PivotEngine — pctTotal', () => { + const config: PivotConfig = { + ...BASE_CONFIG, + values: [{ field: 'sales', aggregation: 'pctTotal' }], + } + const result = new PivotEngine(RECORDS, config).getResult() + + it('cell values are fractions summing to 1', () => { + const sum = Array.from(result.cells.values()) + .reduce((acc, cell) => acc + (cell.values[0] ?? 0), 0) + expect(sum).toBeCloseTo(1.0, 5) + }) + + it('grand total shows 1.0 (100%)', () => { + expect(result.grandTotal.values[0]).toBeCloseTo(1.0, 5) + }) + + it('cells are formatted as percentages', () => { + // North/A = 100/750 + const cell = result.cells.get(compositeKey(flattenKey(['North']), flattenKey(['A']))) + expect(cell?.formatted[0]).toMatch(/%$/) + }) +}) + +describe('PivotEngine — pctRow', () => { + const config: PivotConfig = { + ...BASE_CONFIG, + values: [{ field: 'sales', aggregation: 'pctRow' }], + } + const result = new PivotEngine(RECORDS, config).getResult() + + it('each row sums to 1.0', () => { + for (const rowKey of result.rowKeys) { + const rowSum = result.colKeys.reduce((acc, colKey) => { + const cell = result.cells.get(compositeKey(flattenKey(rowKey), flattenKey(colKey))) + return acc + (cell?.values[0] ?? 0) + }, 0) + expect(rowSum).toBeCloseTo(1.0, 5) + } + }) + + it('row total shows 1.0', () => { + for (const rowKey of result.rowKeys) { + const rowTotal = result.rowTotals.get(flattenKey(rowKey)) + expect(rowTotal?.values[0]).toBeCloseTo(1.0, 5) + } + }) +}) + +describe('PivotEngine — pctCol', () => { + const config: PivotConfig = { + ...BASE_CONFIG, + values: [{ field: 'sales', aggregation: 'pctCol' }], + } + const result = new PivotEngine(RECORDS, config).getResult() + + it('each column sums to 1.0', () => { + for (const colKey of result.colKeys) { + const colSum = result.rowKeys.reduce((acc, rowKey) => { + const cell = result.cells.get(compositeKey(flattenKey(rowKey), flattenKey(colKey))) + return acc + (cell?.values[0] ?? 0) + }, 0) + expect(colSum).toBeCloseTo(1.0, 5) + } + }) +}) + +// ─── Heatmap Utilities ──────────────────────────────────────────────────────── + +describe('PivotEngine.getValueRange', () => { + const result = new PivotEngine(RECORDS, BASE_CONFIG).getResult() + + it('returns correct min and max across all cells', () => { + const range = PivotEngine.getValueRange(result, 0) + expect(range?.min).toBe(100) + expect(range?.max).toBe(300) + }) +}) + +describe('PivotEngine.getRowValueRange', () => { + const result = new PivotEngine(RECORDS, BASE_CONFIG).getResult() + + it('returns correct range within a row', () => { + const range = PivotEngine.getRowValueRange(result, ['North'], result.colKeys, 0) + expect(range?.min).toBe(100) + expect(range?.max).toBe(200) + }) +}) + +describe('PivotEngine.getColValueRange', () => { + const result = new PivotEngine(RECORDS, BASE_CONFIG).getResult() + + it('returns correct range within a column', () => { + const range = PivotEngine.getColValueRange(result, ['A'], result.rowKeys, 0) + expect(range?.min).toBe(100) + expect(range?.max).toBe(150) + }) +}) + +// ─── getHeatmapColor ────────────────────────────────────────────────────────── + +describe('getHeatmapColor', () => { + it('returns undefined for null value', () => { + expect(getHeatmapColor(null, { min: 0, max: 100 })).toBeUndefined() + }) + + it('returns undefined for null range', () => { + expect(getHeatmapColor(50, null)).toBeUndefined() + }) + + it('returns single-value color when min === max', () => { + expect(getHeatmapColor(5, { min: 5, max: 5 })).toBe('rgba(59, 130, 246, 0.3)') + }) + + it('returns low alpha for minimum value', () => { + const color = getHeatmapColor(0, { min: 0, max: 100 }) + expect(color).toBe('rgba(59, 130, 246, 0.10)') + }) + + it('returns high alpha for maximum value', () => { + const color = getHeatmapColor(100, { min: 0, max: 100 }) + expect(color).toBe('rgba(59, 130, 246, 0.60)') + }) +}) diff --git a/packages/pivot-engine/src/__tests__/aggregators.test.ts b/packages/pivot-engine/src/__tests__/aggregators.test.ts new file mode 100644 index 0000000..d7a6a5b --- /dev/null +++ b/packages/pivot-engine/src/__tests__/aggregators.test.ts @@ -0,0 +1,366 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { + createAggregator, + registerAggregator, + getRegisteredPlugins, + formatNumber, + calculatePercentage, + getAllAggregations, +} from '../engine/aggregators' +import type { Aggregator } from '../types' + +// ─── CountAggregator ────────────────────────────────────────────────────────── + +describe('count aggregator', () => { + it('counts each push', () => { + const a = createAggregator('count') + a.push('x'); a.push(1); a.push(null) + expect(a.value()).toBe(3) + }) + + it('starts at 0', () => { + expect(createAggregator('count').value()).toBe(0) + }) + + it('clone is independent', () => { + const a = createAggregator('count') + a.push('x') + const b = a.clone() + b.push('y') + expect(a.value()).toBe(1) + expect(b.value()).toBe(2) + }) +}) + +// ─── CountUniqueAggregator ──────────────────────────────────────────────────── + +describe('countUnique aggregator', () => { + it('counts unique values', () => { + const a = createAggregator('countUnique') + a.push('x'); a.push('x'); a.push('y') + expect(a.value()).toBe(2) + }) + + it('treats null as a distinct value', () => { + const a = createAggregator('countUnique') + a.push(null); a.push(null) + expect(a.value()).toBe(1) + }) + + it('clone is independent', () => { + const a = createAggregator('countUnique') + a.push('a') + const b = a.clone() + b.push('b') + expect(a.value()).toBe(1) + expect(b.value()).toBe(2) + }) +}) + +// ─── SumAggregator ──────────────────────────────────────────────────────────── + +describe('sum aggregator', () => { + it('returns null when no valid values', () => { + const a = createAggregator('sum') + expect(a.value()).toBeNull() + }) + + it('ignores non-numeric strings', () => { + const a = createAggregator('sum') + a.push('hello'); a.push(null) + expect(a.value()).toBeNull() + }) + + it('ignores NaN and Infinity', () => { + const a = createAggregator('sum') + a.push(NaN); a.push(Infinity) + expect(a.value()).toBeNull() + }) + + it('sums numbers', () => { + const a = createAggregator('sum') + a.push(1); a.push(2); a.push(3) + expect(a.value()).toBe(6) + }) + + it('sums numeric strings', () => { + const a = createAggregator('sum') + a.push('10'); a.push('20') + expect(a.value()).toBe(30) + }) + + it('clone is independent', () => { + const a = createAggregator('sum') + a.push(5) + const b = a.clone() + b.push(10) + expect(a.value()).toBe(5) + expect(b.value()).toBe(15) + }) +}) + +// ─── AverageAggregator ──────────────────────────────────────────────────────── + +describe('average aggregator', () => { + it('returns null when empty', () => { + expect(createAggregator('average').value()).toBeNull() + }) + + it('computes mean correctly', () => { + const a = createAggregator('average') + a.push(2); a.push(4); a.push(6) + expect(a.value()).toBe(4) + }) + + it('handles single value', () => { + const a = createAggregator('average') + a.push(7) + expect(a.value()).toBe(7) + }) + + it('ignores non-numeric values', () => { + const a = createAggregator('average') + a.push('bad'); a.push(10) + expect(a.value()).toBe(10) + }) + + it('Welford stability — large values', () => { + const a = createAggregator('average') + // These would overflow naive sum+count with large numbers + a.push(1e15); a.push(1e15 + 2) + expect(a.value()).toBeCloseTo(1e15 + 1, 5) + }) +}) + +// ─── MedianAggregator ───────────────────────────────────────────────────────── + +describe('median aggregator', () => { + it('returns null when empty', () => { + expect(createAggregator('median').value()).toBeNull() + }) + + it('returns the middle value for odd count', () => { + const a = createAggregator('median') + a.push(3); a.push(1); a.push(2) + expect(a.value()).toBe(2) + }) + + it('averages two middle values for even count', () => { + const a = createAggregator('median') + a.push(1); a.push(2); a.push(3); a.push(4) + expect(a.value()).toBe(2.5) + }) + + it('handles single value', () => { + const a = createAggregator('median') + a.push(42) + expect(a.value()).toBe(42) + }) +}) + +// ─── MinAggregator / MaxAggregator ──────────────────────────────────────────── + +describe('min aggregator', () => { + it('returns null when empty', () => { + expect(createAggregator('min').value()).toBeNull() + }) + + it('returns the minimum', () => { + const a = createAggregator('min') + a.push(5); a.push(1); a.push(3) + expect(a.value()).toBe(1) + }) + + it('handles negative values', () => { + const a = createAggregator('min') + a.push(-5); a.push(-10); a.push(0) + expect(a.value()).toBe(-10) + }) +}) + +describe('max aggregator', () => { + it('returns null when empty', () => { + expect(createAggregator('max').value()).toBeNull() + }) + + it('returns the maximum', () => { + const a = createAggregator('max') + a.push(5); a.push(1); a.push(3) + expect(a.value()).toBe(5) + }) +}) + +// ─── StdevAggregator ────────────────────────────────────────────────────────── + +describe('stdev aggregator', () => { + it('returns null for 0 values', () => { + expect(createAggregator('stdev').value()).toBeNull() + }) + + it('returns null for 1 value', () => { + const a = createAggregator('stdev') + a.push(5) + expect(a.value()).toBeNull() + }) + + it('computes sample stdev correctly', () => { + const a = createAggregator('stdev') + // Dataset: 2, 4, 4, 4, 5, 5, 7, 9 → sample stdev = sqrt(32/7) ≈ 2.138 + ;[2, 4, 4, 4, 5, 5, 7, 9].forEach((v) => a.push(v)) + expect(a.value()).toBeCloseTo(Math.sqrt(32 / 7), 5) + }) +}) + +// ─── SumOverSumAggregator ───────────────────────────────────────────────────── + +describe('sumOverSum aggregator', () => { + it('returns null when nothing pushed', () => { + expect(createAggregator('sumOverSum').value()).toBeNull() + }) + + it('returns null when sum2 is 0', () => { + const a = createAggregator('sumOverSum') + a.push(10, 0) + expect(a.value()).toBeNull() + }) + + it('computes ratio correctly', () => { + const a = createAggregator('sumOverSum') + a.push(10, 2); a.push(20, 8) + // sum1=30, sum2=10 → 30/10 = 3 + expect(a.value()).toBe(3) + }) +}) + +// ─── Plugin API ─────────────────────────────────────────────────────────────── + +describe('registerAggregator plugin API', () => { + beforeEach(() => { + // Clear any plugins registered in previous test runs + // (the registry is module-level state, but since tests run + // in the same process, we use unique names to avoid conflicts) + }) + + it('registers and creates a custom aggregator', () => { + registerAggregator({ + type: '__test_double__', + label: 'Double', + factory: () => { + let sum = 0 + const agg: Aggregator = { + push(v: unknown) { sum += Number(v) * 2 }, + value() { return sum }, + clone() { + const s = sum + return { + push(v: unknown) { sum += Number(v) * 2 }, + value() { return s }, + clone() { return agg.clone() }, + } + }, + } + return agg + }, + }) + + const a = createAggregator('__test_double__') + a.push(5) + expect(a.value()).toBe(10) + }) + + it('throws when trying to override a built-in type', () => { + expect(() => + registerAggregator({ type: 'sum', label: 'Bad', factory: () => createAggregator('count') }) + ).toThrow('[pivot-engine] Cannot override built-in aggregation type: "sum"') + }) + + it('throws createAggregator for completely unknown type', () => { + expect(() => createAggregator('__nonexistent_xyz__')).toThrow( + '[pivot-engine] Unknown aggregation type: "__nonexistent_xyz__"' + ) + }) + + it('getRegisteredPlugins returns registered plugin', () => { + // Plugin registered in earlier test is visible + const plugins = getRegisteredPlugins() + expect(plugins.has('__test_double__')).toBe(true) + }) + + it('getAllAggregations includes built-ins and custom plugins', () => { + const all = getAllAggregations() + const types = all.map((a) => a.type) + expect(types).toContain('sum') + expect(types).toContain('count') + expect(types).toContain('__test_double__') + }) +}) + +// ─── formatNumber ───────────────────────────────────────────────────────────── + +describe('formatNumber', () => { + it('returns — for null', () => { + expect(formatNumber(null, 'sum')).toBe('—') + }) + + it('formats count/countUnique as integer', () => { + expect(formatNumber(42, 'count')).toBe('42') + expect(formatNumber(42, 'countUnique')).toBe('42') + }) + + it('formats average/median/stdev with 2 decimals', () => { + expect(formatNumber(3.14159, 'average')).toBe('3.14') + expect(formatNumber(3.14159, 'median')).toBe('3.14') + expect(formatNumber(3.14159, 'stdev')).toBe('3.14') + }) + + it('formats sum/min/max as integer when whole', () => { + expect(formatNumber(100, 'sum')).toBe('100') + expect(formatNumber(100, 'min')).toBe('100') + expect(formatNumber(100, 'max')).toBe('100') + }) + + it('formats sum/min/max with 2 decimals when fractional', () => { + expect(formatNumber(3.5, 'sum')).toBe('3.50') + }) + + it('formats sumOverSum with 2 decimals', () => { + expect(formatNumber(1.5, 'sumOverSum')).toBe('1.50') + }) + + it('formats pct types as percentage', () => { + expect(formatNumber(0.5, 'pctTotal')).toBe('50.0%') + expect(formatNumber(1.0, 'pctRow')).toBe('100.0%') + expect(formatNumber(0.333, 'pctCol')).toBe('33.3%') + }) + + it('uses custom plugin formatter when registered', () => { + registerAggregator({ + type: '__test_fmt__', + label: 'Fmt', + factory: () => createAggregator('count'), + format: (v) => v === null ? 'null' : `$${v.toFixed(0)}`, + }) + expect(formatNumber(42, '__test_fmt__')).toBe('$42') + expect(formatNumber(null, '__test_fmt__')).toBe('null') + }) +}) + +// ─── calculatePercentage ────────────────────────────────────────────────────── + +describe('calculatePercentage', () => { + it('returns cellValue / totalValue', () => { + expect(calculatePercentage(25, 100)).toBe(0.25) + }) + + it('returns null when totalValue is 0', () => { + expect(calculatePercentage(5, 0)).toBeNull() + }) + + it('returns null when cellValue is null', () => { + expect(calculatePercentage(null, 100)).toBeNull() + }) + + it('returns null when totalValue is null', () => { + expect(calculatePercentage(50, null)).toBeNull() + }) +}) diff --git a/packages/pivot-engine/src/__tests__/sorters.test.ts b/packages/pivot-engine/src/__tests__/sorters.test.ts new file mode 100644 index 0000000..7b7e3b0 --- /dev/null +++ b/packages/pivot-engine/src/__tests__/sorters.test.ts @@ -0,0 +1,174 @@ +import { describe, it, expect } from 'vitest' +import { + naturalSort, + flattenKey, + expandKey, + compositeKey, + createKeyComparator, + createValueComparator, +} from '../engine/sorters' + +describe('naturalSort', () => { + describe('nulls', () => { + it('sorts null before any value', () => { + expect(naturalSort(null, 'a')).toBeLessThan(0) + expect(naturalSort('a', null)).toBeGreaterThan(0) + }) + + it('treats null === null as equal', () => { + expect(naturalSort(null, null)).toBe(0) + }) + }) + + describe('pure numbers', () => { + it('sorts numbers numerically', () => { + expect(naturalSort(2, 10)).toBeLessThan(0) + expect(naturalSort(10, 2)).toBeGreaterThan(0) + expect(naturalSort(5, 5)).toBe(0) + }) + + it('sorts numeric strings numerically', () => { + expect(naturalSort('2', '10')).toBeLessThan(0) + expect(naturalSort('10', '2')).toBeGreaterThan(0) + }) + + it('places numbers before non-numeric strings', () => { + expect(naturalSort(1, 'abc')).toBeLessThan(0) + expect(naturalSort('abc', 1)).toBeGreaterThan(0) + }) + }) + + describe('alphanumeric (natural order)', () => { + it('sorts item2 before item10', () => { + expect(naturalSort('item2', 'item10')).toBeLessThan(0) + expect(naturalSort('item10', 'item2')).toBeGreaterThan(0) + }) + + it('handles version strings correctly', () => { + expect(naturalSort('v1.2', 'v1.10')).toBeLessThan(0) + expect(naturalSort('v1.10', 'v1.2')).toBeGreaterThan(0) + }) + + it('handles file names', () => { + const files = ['file10', 'file2', 'file1'] + const sorted = [...files].sort(naturalSort) + expect(sorted).toEqual(['file1', 'file2', 'file10']) + }) + }) + + describe('pure strings (no digits)', () => { + it('sorts lexicographically', () => { + expect(naturalSort('apple', 'banana')).toBeLessThan(0) + expect(naturalSort('banana', 'apple')).toBeGreaterThan(0) + expect(naturalSort('apple', 'apple')).toBe(0) + }) + }) + + describe('NaN handling', () => { + it('sorts NaN before other numbers', () => { + expect(naturalSort(NaN, 1)).toBeLessThan(0) + expect(naturalSort(1, NaN)).toBeGreaterThan(0) + }) + }) +}) + +describe('key utilities', () => { + it('flattenKey joins parts with null delimiter', () => { + const key = ['North', 'Q1', '2024'] + const flat = flattenKey(key) + expect(flat).toBe('North\x00Q1\x002024') + }) + + it('expandKey splits on null delimiter', () => { + expect(expandKey('North\x00Q1\x002024')).toEqual(['North', 'Q1', '2024']) + }) + + it('flattenKey / expandKey round-trip', () => { + const key = ['Region A', 'Sub B', '2020'] + expect(expandKey(flattenKey(key))).toEqual(key) + }) + + it('handles single-element key', () => { + expect(expandKey(flattenKey(['only']))).toEqual(['only']) + }) + + it('compositeKey separates row and col with pipe', () => { + expect(compositeKey('row', 'col')).toBe('row|col') + }) + + it('compositeKey works with empty strings', () => { + expect(compositeKey('', '')).toBe('|') + }) +}) + +describe('createKeyComparator', () => { + it('sorts single-element keys ascending', () => { + const cmp = createKeyComparator(false) + const keys = [['banana'], ['apple'], ['cherry']] + const sorted = [...keys].sort(cmp) + expect(sorted.map((k) => k[0])).toEqual(['apple', 'banana', 'cherry']) + }) + + it('sorts single-element keys descending', () => { + const cmp = createKeyComparator(true) + const keys = [['banana'], ['apple'], ['cherry']] + const sorted = [...keys].sort(cmp) + expect(sorted.map((k) => k[0])).toEqual(['cherry', 'banana', 'apple']) + }) + + it('sorts multi-element keys by first element, then second', () => { + const cmp = createKeyComparator(false) + const keys = [ + ['B', '2'], + ['A', '10'], + ['A', '2'], + ] + const sorted = [...keys].sort(cmp) + expect(sorted).toEqual([['A', '2'], ['A', '10'], ['B', '2']]) + }) + + it('uses natural sort (item2 < item10)', () => { + const cmp = createKeyComparator(false) + expect(cmp([['item10']][0]!, [['item2']][0]!)).toBeGreaterThan(0) + }) +}) + +describe('createValueComparator', () => { + const makeGetter = (map: Record) => + (key: string[]) => map[key[0]!] ?? null + + it('sorts ascending', () => { + const getter = makeGetter({ a: 10, b: 5, c: 20 }) + const cmp = createValueComparator(getter, false) + const keys = [['a'], ['b'], ['c']] + const sorted = [...keys].sort(cmp) + expect(sorted.map((k) => k[0])).toEqual(['b', 'a', 'c']) + }) + + it('sorts descending', () => { + const getter = makeGetter({ a: 10, b: 5, c: 20 }) + const cmp = createValueComparator(getter, true) + const keys = [['a'], ['b'], ['c']] + const sorted = [...keys].sort(cmp) + expect(sorted.map((k) => k[0])).toEqual(['c', 'a', 'b']) + }) + + it('places nulls at the end regardless of sort direction', () => { + const getter = makeGetter({ a: 10, b: null, c: 5 }) + const cmpAsc = createValueComparator(getter, false) + const cmpDesc = createValueComparator(getter, true) + const keys = [['a'], ['b'], ['c']] + + const sortedAsc = [...keys].sort(cmpAsc).map((k) => k[0]) + const sortedDesc = [...keys].sort(cmpDesc).map((k) => k[0]) + + expect(sortedAsc[2]).toBe('b') // null at end + expect(sortedDesc[2]).toBe('b') // null at end + }) + + it('treats null === null as equal', () => { + const getter = makeGetter({ a: null, b: null }) + const cmp = createValueComparator(getter, false) + expect(cmp(['a'], ['b'])).toBe(0) + }) +}) diff --git a/src/tools/pivot-table/engine/PivotEngine.ts b/packages/pivot-engine/src/engine/PivotEngine.ts similarity index 92% rename from src/tools/pivot-table/engine/PivotEngine.ts rename to packages/pivot-engine/src/engine/PivotEngine.ts index 1d58401..d72d845 100644 --- a/src/tools/pivot-table/engine/PivotEngine.ts +++ b/packages/pivot-engine/src/engine/PivotEngine.ts @@ -6,9 +6,13 @@ import { Aggregator, ValueConfig, SortOrder, - DERIVED_AGGREGATIONS, } from '../types' -import { createAggregator, formatNumber, calculatePercentage } from './aggregators' +import { + createAggregator, + formatNumber, + calculatePercentage, + isEffectivelyDerived, +} from './aggregators' import { flattenKey, compositeKey, @@ -22,8 +26,7 @@ import { interface AggregatorGroup { aggregators: Aggregator[] push(record: DataRecord): void - getValues(): (number | null)[] - getRawValues(): (number | null)[] // For percentage calculations + getRawValues(): (number | null)[] } function createAggregatorGroup(valueConfigs: ValueConfig[]): AggregatorGroup { @@ -39,9 +42,6 @@ function createAggregatorGroup(valueConfigs: ValueConfig[]): AggregatorGroup { aggregators[i]!.push(value, value2) } }, - getValues() { - return aggregators.map((a) => a.value()) - }, getRawValues() { return aggregators.map((a) => a.value()) }, @@ -75,7 +75,7 @@ export class PivotEngine { private processRecords(): void { const { rows, cols, values, filters } = this.config - // Build filter lookup for fast checking + // Build filter lookup for fast O(1) checking const filterMap = new Map>() for (const f of filters) { if (f.excludedValues.size > 0) { @@ -160,12 +160,11 @@ export class PivotEngine { const isValueSort = order === 'value_asc' || order === 'value_desc' if (isValueSort) { - // Sort by first aggregated value const valueGetter = (key: string[]): number | null => { const flatKey = flattenKey(key) const group = totalsMap.get(flatKey) if (!group) return null - const values = group.getValues() + const values = group.getRawValues() return values[0] ?? null } return [...keys].sort(createValueComparator(valueGetter, isDescending)) @@ -183,15 +182,14 @@ export class PivotEngine { grandTotalValues: (number | null)[] ): (number | null)[] { const { values } = this.config - + return rawValues.map((raw, i) => { const agg = values[i]!.aggregation - - if (!DERIVED_AGGREGATIONS.has(agg)) { + + if (!isEffectivelyDerived(agg)) { return raw } - // Determine which total to use based on aggregation type switch (agg) { case 'pctTotal': case 'countPctTotal': @@ -203,6 +201,8 @@ export class PivotEngine { case 'countPctCol': return calculatePercentage(raw, colTotalValues?.[i] ?? grandTotalValues[i] ?? null) default: + // Custom derived aggregations: no automatic percentage formula — + // the plugin's aggregator computes its own final value return raw } }) @@ -225,44 +225,42 @@ export class PivotEngine { this.colTotals ) - // Get grand total raw values for percentage calculations const grandTotalRaw = this.grandTotal.getRawValues() // Build cells map with derived calculations const cells = new Map() for (const [key, group] of this.cells) { - // Parse the composite key to get row and col totals const [flatRowKey, flatColKey] = key.split('|') const rowTotalRaw = flatRowKey ? this.rowTotals.get(flatRowKey)?.getRawValues() ?? null : null const colTotalRaw = flatColKey ? this.colTotals.get(flatColKey)?.getRawValues() ?? null : null - + const rawValues = group.getRawValues() const computedValues = this.computeDerivedValues(rawValues, rowTotalRaw, colTotalRaw, grandTotalRaw) - + cells.set(key, { values: computedValues, formatted: computedValues.map((v, i) => formatNumber(v, values[i]!.aggregation)), }) } - // Build row totals (for % row, the row total should show 100%) + // Row totals (% row shows 100% in the total column) const rowTotalsResult = new Map() for (const [key, group] of this.rowTotals) { const rawValues = group.getRawValues() const computedValues = this.computeDerivedValues(rawValues, rawValues, null, grandTotalRaw) - + rowTotalsResult.set(key, { values: computedValues, formatted: computedValues.map((v, i) => formatNumber(v, values[i]!.aggregation)), }) } - // Build column totals (for % col, the col total should show 100%) + // Column totals (% col shows 100% in the total row) const colTotalsResult = new Map() for (const [key, group] of this.colTotals) { const rawValues = group.getRawValues() const computedValues = this.computeDerivedValues(rawValues, null, rawValues, grandTotalRaw) - + colTotalsResult.set(key, { values: computedValues, formatted: computedValues.map((v, i) => formatNumber(v, values[i]!.aggregation)), @@ -270,7 +268,9 @@ export class PivotEngine { } // Grand total - const grandTotalComputed = this.computeDerivedValues(grandTotalRaw, grandTotalRaw, grandTotalRaw, grandTotalRaw) + const grandTotalComputed = this.computeDerivedValues( + grandTotalRaw, grandTotalRaw, grandTotalRaw, grandTotalRaw + ) const grandTotal: CellValue = { values: grandTotalComputed, formatted: grandTotalComputed.map((v, i) => formatNumber(v, values[i]!.aggregation)), @@ -380,7 +380,6 @@ export function getHeatmapColor( if (range.max === range.min) return 'rgba(59, 130, 246, 0.3)' // Single value const ratio = (value - range.min) / (range.max - range.min) - // Use blue color scale (matches accent color) const alpha = 0.1 + ratio * 0.5 return `rgba(59, 130, 246, ${alpha.toFixed(2)})` } diff --git a/src/tools/pivot-table/engine/aggregators.ts b/packages/pivot-engine/src/engine/aggregators.ts similarity index 69% rename from src/tools/pivot-table/engine/aggregators.ts rename to packages/pivot-engine/src/engine/aggregators.ts index d595ef8..2ebb0e9 100644 --- a/src/tools/pivot-table/engine/aggregators.ts +++ b/packages/pivot-engine/src/engine/aggregators.ts @@ -1,4 +1,11 @@ -import { Aggregator, AggregatorFactory, AggregationType, DERIVED_AGGREGATIONS } from '../types' +import { + Aggregator, + AggregatorFactory, + AggregatorPlugin, + AggregationType, + AGGREGATION_LABELS, + DERIVED_AGGREGATIONS, +} from '../types' // ─── Count Aggregator ───────────────────────────────────────────────────────── @@ -167,7 +174,7 @@ class MaxAggregator implements Aggregator { } } -// ─── Standard Deviation Aggregator (Welford's algorithm) ────────────────────── +// ─── Standard Deviation Aggregator (Welford's algorithm) ───────────────────── class StdevAggregator implements Aggregator { private n = 0 @@ -234,7 +241,7 @@ class SumOverSumAggregator implements Aggregator { } // ─── Derived Aggregators (% of total/row/col) ───────────────────────────────── -// These use Sum internally but format as percentage after totals are known +// Collect raw sums/counts; the engine applies percentage formula using totals class DerivedSumAggregator implements Aggregator { private sum = 0 @@ -278,7 +285,7 @@ class DerivedCountAggregator implements Aggregator { } } -// ─── Factory Map ────────────────────────────────────────────────────────────── +// ─── Built-in Factory Map ───────────────────────────────────────────────────── const AGGREGATOR_FACTORIES: Record = { count: () => new CountAggregator(), @@ -299,20 +306,96 @@ const AGGREGATOR_FACTORIES: Record = { countPctCol: () => new DerivedCountAggregator(), } -export function createAggregator(type: AggregationType): Aggregator { - return AGGREGATOR_FACTORIES[type]() +// ─── Plugin Registry ────────────────────────────────────────────────────────── + +const pluginRegistry = new Map() + +/** + * Register a custom aggregation type. Call this once at startup before + * creating any PivotEngine instances that use the custom type. + * + * @example + * registerAggregator({ + * type: 'geoMean', + * label: 'Geo Mean', + * factory: () => { ... }, + * format: (v) => v?.toFixed(3) ?? '—', + * }) + */ +export function registerAggregator(plugin: AggregatorPlugin): void { + if (plugin.type in AGGREGATOR_FACTORIES) { + throw new Error(`[pivot-engine] Cannot override built-in aggregation type: "${plugin.type}"`) + } + if (pluginRegistry.has(plugin.type)) { + console.warn(`[pivot-engine] Aggregator "${plugin.type}" is already registered. Overwriting.`) + } + pluginRegistry.set(plugin.type, plugin) } -// ─── Number Formatting (US format) ──────────────────────────────────────────── +/** + * Returns all registered custom aggregator plugins (does not include built-ins). + * Useful for populating UI dropdowns with custom aggregation options. + */ +export function getRegisteredPlugins(): ReadonlyMap { + return pluginRegistry +} + +// ─── Factory ────────────────────────────────────────────────────────────────── + +export function createAggregator(type: AggregationType | string): Aggregator { + if (Object.prototype.hasOwnProperty.call(AGGREGATOR_FACTORIES, type)) { + return (AGGREGATOR_FACTORIES as Record)[type]!() + } + const plugin = pluginRegistry.get(type) + if (plugin) return plugin.factory() + throw new Error( + `[pivot-engine] Unknown aggregation type: "${type}". Register it with registerAggregator() first.` + ) +} + +// ─── Derived Check ──────────────────────────────────────────────────────────── + +/** Returns true if the aggregation type should be treated as post-hoc derived. */ +export function isEffectivelyDerived(type: AggregationType | string): boolean { + if (DERIVED_AGGREGATIONS.has(type as AggregationType)) return true + return pluginRegistry.get(type)?.isDerived === true +} + +// ─── Label Lookup ───────────────────────────────────────────────────────────── + +/** Returns the display label for any aggregation type (built-in or custom). */ +export function getAggregationLabel(type: AggregationType | string): string { + return ( + AGGREGATION_LABELS[type as AggregationType] ?? + pluginRegistry.get(type)?.label ?? + type + ) +} + +/** + * Returns all aggregation options (built-ins + registered plugins) as a flat list, + * suitable for populating a UI select element. + */ +export function getAllAggregations(): Array<{ type: string; label: string }> { + const builtIns = Object.entries(AGGREGATION_LABELS).map(([type, label]) => ({ type, label })) + const customs = Array.from(pluginRegistry.values()).map(({ type, label }) => ({ type, label })) + return [...builtIns, ...customs] +} + +// ─── Number Formatting ──────────────────────────────────────────────────────── export function formatNumber( value: number | null, - aggregationType: AggregationType + aggregationType: AggregationType | string ): string { + // Check plugin registry first (plugin may handle null its own way) + const plugin = pluginRegistry.get(aggregationType) + if (plugin?.format) return plugin.format(value) + if (value === null) return '—' // Percentage types - if (DERIVED_AGGREGATIONS.has(aggregationType)) { + if (DERIVED_AGGREGATIONS.has(aggregationType as AggregationType)) { return (value * 100).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1, @@ -327,7 +410,7 @@ export function formatNumber( }) } - // Determine decimal places based on aggregation type + // Decimal places by aggregation type let decimals: number switch (aggregationType) { case 'count': @@ -340,7 +423,7 @@ export function formatNumber( decimals = 2 break default: - // For sum, min, max - use up to 2 decimals only if needed + // sum, min, max — integer if whole, 2 decimals otherwise decimals = Number.isInteger(value) ? 0 : 2 } diff --git a/packages/pivot-engine/src/engine/analyzeData.ts b/packages/pivot-engine/src/engine/analyzeData.ts new file mode 100644 index 0000000..d8c93df --- /dev/null +++ b/packages/pivot-engine/src/engine/analyzeData.ts @@ -0,0 +1,60 @@ +import { DataRecord, FieldInfo } from '../types' +import { naturalSort } from './sorters' + +/** + * Analyzes an array of records and returns field metadata: + * - whether the field is numeric + * - sorted list of unique values + * - total unique value count + * + * Single pass through records — O(n * fields). + */ +export function analyzeData(records: DataRecord[]): FieldInfo[] { + if (records.length === 0) return [] + + const firstRecord = records[0] + if (!firstRecord) return [] + + const fieldNames = Object.keys(firstRecord) + const fieldInfoMap = new Map() + + for (const name of fieldNames) { + fieldInfoMap.set(name, { + name, + isNumeric: true, // Assume numeric until proven otherwise + uniqueValues: [], + valueCount: 0, + }) + } + + const uniqueSets = new Map>() + for (const name of fieldNames) { + uniqueSets.set(name, new Set()) + } + + for (const record of records) { + for (const name of fieldNames) { + const info = fieldInfoMap.get(name)! + const uniqueSet = uniqueSets.get(name)! + const value = record[name] + + uniqueSet.add(String(value ?? 'null')) + + if (info.isNumeric && value !== null && value !== undefined && value !== '') { + const numValue = typeof value === 'number' ? value : parseFloat(String(value)) + if (isNaN(numValue)) { + info.isNumeric = false + } + } + } + } + + for (const name of fieldNames) { + const info = fieldInfoMap.get(name)! + const uniqueSet = uniqueSets.get(name)! + info.uniqueValues = Array.from(uniqueSet).sort((a, b) => naturalSort(a, b)) + info.valueCount = uniqueSet.size + } + + return Array.from(fieldInfoMap.values()) +} diff --git a/src/tools/pivot-table/engine/sorters.ts b/packages/pivot-engine/src/engine/sorters.ts similarity index 97% rename from src/tools/pivot-table/engine/sorters.ts rename to packages/pivot-engine/src/engine/sorters.ts index fce6c63..c68eb53 100644 --- a/src/tools/pivot-table/engine/sorters.ts +++ b/packages/pivot-engine/src/engine/sorters.ts @@ -56,8 +56,7 @@ export function naturalSort(a: unknown, b: unknown): number { if (RX_DIGIT.test(chunkA) && RX_DIGIT.test(chunkB)) { const diff = parseInt(chunkA, 10) - parseInt(chunkB, 10) if (diff !== 0) return diff - // Same numeric value but different string (e.g., "01" vs "1") - // Shorter string first + // Same numeric value but different string (e.g., "01" vs "1") — shorter first if (chunkA.length !== chunkB.length) { return chunkA.length - chunkB.length } @@ -105,7 +104,7 @@ export function createValueComparator( // ─── Key Utilities ──────────────────────────────────────────────────────────── -// Use null character as delimiter (won't appear in normal data) +// Null character as delimiter — won't appear in normal data const KEY_DELIMITER = '\0' export function flattenKey(key: string[]): string { diff --git a/packages/pivot-engine/src/index.ts b/packages/pivot-engine/src/index.ts new file mode 100644 index 0000000..5eee7ee --- /dev/null +++ b/packages/pivot-engine/src/index.ts @@ -0,0 +1,60 @@ +// ─── Types ──────────────────────────────────────────────────────────────────── + +export type { + AggregationType, + AggregatorFactory, + Aggregator, + AggregatorPlugin, + ValueConfig, + FilterConfig, + SortOrder, + HeatmapMode, + PivotConfig, + DataRecord, + ParsedData, + CellValue, + PivotResult, + FieldInfo, +} from './types' + +export { + AGGREGATION_LABELS, + DUAL_FIELD_AGGREGATIONS, + DERIVED_AGGREGATIONS, + SORT_ORDER_LABELS, + HEATMAP_LABELS, +} from './types' + +// ─── Engine ─────────────────────────────────────────────────────────────────── + +export { PivotEngine, getHeatmapColor } from './engine/PivotEngine' + +// ─── Aggregators ────────────────────────────────────────────────────────────── + +export { + createAggregator, + registerAggregator, + getRegisteredPlugins, + isEffectivelyDerived, + getAggregationLabel, + getAllAggregations, + formatNumber, + calculatePercentage, +} from './engine/aggregators' + +// ─── Sorters ────────────────────────────────────────────────────────────────── + +export { + naturalSort, + flattenKey, + expandKey, + compositeKey, + createKeyComparator, + createValueComparator, +} from './engine/sorters' + +export type { Comparator } from './engine/sorters' + +// ─── Data Analysis ──────────────────────────────────────────────────────────── + +export { analyzeData } from './engine/analyzeData' diff --git a/src/tools/pivot-table/types.ts b/packages/pivot-engine/src/types.ts similarity index 80% rename from src/tools/pivot-table/types.ts rename to packages/pivot-engine/src/types.ts index 43fadf4..27ac72d 100644 --- a/src/tools/pivot-table/types.ts +++ b/packages/pivot-engine/src/types.ts @@ -44,12 +44,39 @@ export const DERIVED_AGGREGATIONS: Set = new Set([ 'countPctTotal', 'countPctRow', 'countPctCol', ]) +// ─── Aggregator Interface ───────────────────────────────────────────────────── + +export interface Aggregator { + push(value: unknown, value2?: unknown): void + value(): number | null + clone(): Aggregator +} + +export type AggregatorFactory = () => Aggregator + +// ─── Custom Aggregator Plugin ───────────────────────────────────────────────── + +export interface AggregatorPlugin { + /** Unique identifier. Must not collide with built-in AggregationType values. */ + type: string + /** Human-readable label shown in UI dropdowns. */ + label: string + /** If true, the UI shows a second field selector (like sumOverSum). */ + requiresSecondField?: boolean + /** If true, engine treats this as a post-hoc derived value (like pct types). */ + isDerived?: boolean + /** Factory returning a fresh Aggregator instance. */ + factory: AggregatorFactory + /** Optional custom formatter. Falls back to built-in formatNumber if not provided. */ + format?: (value: number | null) => string +} + // ─── Configuration Types ────────────────────────────────────────────────────── export interface ValueConfig { field: string - field2?: string // Second field for sumOverSum - aggregation: AggregationType + field2?: string // Second field for sumOverSum + aggregation: AggregationType | string // Built-in or custom (via registerAggregator) } export interface FilterConfig { @@ -98,16 +125,6 @@ export interface ParsedData { numericFields: Set } -// ─── Aggregator Interface ───────────────────────────────────────────────────── - -export interface Aggregator { - push(value: unknown, value2?: unknown): void - value(): number | null - clone(): Aggregator -} - -export type AggregatorFactory = () => Aggregator - // ─── Pivot Result Types ─────────────────────────────────────────────────────── export interface CellValue { @@ -126,7 +143,7 @@ export interface PivotResult { isEmpty: boolean } -// ─── UI State Types ─────────────────────────────────────────────────────────── +// ─── Field Analysis Types ───────────────────────────────────────────────────── export interface FieldInfo { name: string diff --git a/packages/pivot-engine/tsconfig.json b/packages/pivot-engine/tsconfig.json new file mode 100644 index 0000000..515a0e6 --- /dev/null +++ b/packages/pivot-engine/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "include": ["src"] +} diff --git a/packages/pivot-engine/vitest.config.ts b/packages/pivot-engine/vitest.config.ts new file mode 100644 index 0000000..14597e0 --- /dev/null +++ b/packages/pivot-engine/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/__tests__/**/*.test.ts'], + coverage: { + provider: 'v8', + include: ['src/**/*.ts'], + exclude: ['src/__tests__/**', 'src/index.ts'], + }, + }, +}) diff --git a/packages/pivot-react/package.json b/packages/pivot-react/package.json new file mode 100644 index 0000000..dab7d15 --- /dev/null +++ b/packages/pivot-react/package.json @@ -0,0 +1,18 @@ +{ + "name": "@utils-foo/pivot-react", + "private": true, + "version": "0.1.0", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@utils-foo/pivot-engine": "*", + "@types/papaparse": "^5.5.2", + "papaparse": "^5.5.3" + }, + "peerDependencies": { + "lucide-react": "^1.0.0", + "react": "^19.0.0" + } +} diff --git a/src/tools/pivot-table/components/ConfigPanel.tsx b/packages/pivot-react/src/components/ConfigPanel.tsx similarity index 86% rename from src/tools/pivot-table/components/ConfigPanel.tsx rename to packages/pivot-react/src/components/ConfigPanel.tsx index e3ec90f..c366415 100644 --- a/src/tools/pivot-table/components/ConfigPanel.tsx +++ b/packages/pivot-react/src/components/ConfigPanel.tsx @@ -1,17 +1,18 @@ import { useState, useCallback, useMemo } from 'react' import { Plus, X, GripVertical } from 'lucide-react' -import { Toggle } from '../../../components/ui/Toggle' -import { FilterModal } from './FilterModal' import { PivotConfig, ValueConfig, FilterConfig, FieldInfo, - AGGREGATION_LABELS, SORT_ORDER_LABELS, HEATMAP_LABELS, DUAL_FIELD_AGGREGATIONS, -} from '../types' + getAllAggregations, + getRegisteredPlugins, +} from '@utils-foo/pivot-engine' +import { Toggle } from '../ui/Toggle' +import { FilterModal } from './FilterModal' // ─── Draggable Field Chip ───────────────────────────────────────────────────── @@ -60,7 +61,6 @@ interface DropZoneProps { allFields: FieldInfo[] onDrop: (field: string) => void onRemove: (field: string) => void - onReorder?: (fields: string[]) => void activeFilters?: Map onFilterClick?: (field: string) => void className?: string @@ -89,9 +89,7 @@ function DropZone({ setIsDragOver(true) }, []) - const handleDragLeave = useCallback(() => { - setIsDragOver(false) - }, []) + const handleDragLeave = useCallback(() => setIsDragOver(false), []) const handleDrop = useCallback( (e: React.DragEvent) => { @@ -121,9 +119,7 @@ function DropZone({
{fields.length === 0 ? ( - - Drop fields here - + Drop fields here ) : ( fields.map((field) => { const info = fieldInfoMap.get(field) @@ -163,14 +159,19 @@ interface ValueConfigInlineProps { onRemove: () => void } -function ValueConfigInline({ - config, - fields, - numericFields, - onUpdate, - onRemove, -}: ValueConfigInlineProps) { - const isDualField = DUAL_FIELD_AGGREGATIONS.has(config.aggregation) +function ValueConfigInline({ config, fields, numericFields, onUpdate, onRemove }: ValueConfigInlineProps) { + // All aggregation options: built-ins + registered plugins + const allAggregations = useMemo( + () => getAllAggregations(), + // Re-compute only if new plugins are registered (rare, so useMemo is stable) + // eslint-disable-next-line react-hooks/exhaustive-deps + [getRegisteredPlugins().size] + ) + + const isDualField = + DUAL_FIELD_AGGREGATIONS.has(config.aggregation as Parameters[0]) || + (getRegisteredPlugins().get(config.aggregation)?.requiresSecondField === true) + const availableFields = config.aggregation === 'count' || config.aggregation === 'countUnique' ? fields @@ -182,15 +183,11 @@ function ValueConfigInline({ of @@ -201,9 +198,7 @@ function ValueConfigInline({ > {availableFields.map((f) => ( - + ))} {isDualField && ( @@ -216,9 +211,7 @@ function ValueConfigInline({ > {availableFields2.map((f) => ( - + ))} @@ -248,13 +241,11 @@ export function ConfigPanel({ config, fields, onConfigChange }: ConfigPanelProps [fields] ) - // Fields not yet assigned to rows, cols, or filters const unassignedFields = useMemo(() => { const assigned = new Set([...config.rows, ...config.cols]) return fieldNames.filter((f) => !assigned.has(f)) }, [fieldNames, config.rows, config.cols]) - // Active filter counts by field const activeFilterCounts = useMemo(() => { const counts = new Map() for (const f of config.filters) { @@ -265,11 +256,8 @@ export function ConfigPanel({ config, fields, onConfigChange }: ConfigPanelProps return counts }, [config.filters]) - // ─── Handlers ────────────────────────────────────────────────────────────── - const handleDropToRows = useCallback( (field: string) => { - // Remove from cols if present const newCols = config.cols.filter((f) => f !== field) onConfigChange({ ...config, rows: [...config.rows, field], cols: newCols }) }, @@ -278,7 +266,6 @@ export function ConfigPanel({ config, fields, onConfigChange }: ConfigPanelProps const handleDropToCols = useCallback( (field: string) => { - // Remove from rows if present const newRows = config.rows.filter((f) => f !== field) onConfigChange({ ...config, cols: [...config.cols, field], rows: newRows }) }, @@ -286,16 +273,12 @@ export function ConfigPanel({ config, fields, onConfigChange }: ConfigPanelProps ) const handleRemoveFromRows = useCallback( - (field: string) => { - onConfigChange({ ...config, rows: config.rows.filter((f) => f !== field) }) - }, + (field: string) => onConfigChange({ ...config, rows: config.rows.filter((f) => f !== field) }), [config, onConfigChange] ) const handleRemoveFromCols = useCallback( - (field: string) => { - onConfigChange({ ...config, cols: config.cols.filter((f) => f !== field) }) - }, + (field: string) => onConfigChange({ ...config, cols: config.cols.filter((f) => f !== field) }), [config, onConfigChange] ) @@ -319,8 +302,7 @@ export function ConfigPanel({ config, fields, onConfigChange }: ConfigPanelProps const handleRemoveValue = useCallback( (index: number) => { - const values = config.values.filter((_, i) => i !== index) - onConfigChange({ ...config, values }) + onConfigChange({ ...config, values: config.values.filter((_, i) => i !== index) }) }, [config, onConfigChange] ) @@ -349,21 +331,17 @@ export function ConfigPanel({ config, fields, onConfigChange }: ConfigPanelProps ? config.filters.find((f) => f.field === filterModalField)?.excludedValues || new Set() : new Set() - // ─── Render ──────────────────────────────────────────────────────────────── - return ( <>
- {/* Row 1: Available Fields */} + {/* Available Fields */}
Fields
{unassignedFields.length === 0 ? ( - - All fields assigned - + All fields assigned ) : ( unassignedFields.map((field) => { const info = fields.find((f) => f.name === field) @@ -373,7 +351,7 @@ export function ConfigPanel({ config, fields, onConfigChange }: ConfigPanelProps
- {/* Row 2: Drop Zones - Rows, Columns, Filters */} + {/* Drop Zones */}
- {/* Row 3: Values and Options */} + {/* Values and Options */}
- {/* Values */}
Values: @@ -427,7 +404,6 @@ export function ConfigPanel({ config, fields, onConfigChange }: ConfigPanelProps
- {/* Options */}
@@ -471,9 +445,7 @@ export function ConfigPanel({ config, fields, onConfigChange }: ConfigPanelProps className="px-1.5 py-0.5 bg-[var(--color-surface)] border border-[var(--color-border)] rounded text-xs focus:outline-none cursor-pointer" > {Object.entries(SORT_ORDER_LABELS).map(([key, label]) => ( - + ))} @@ -481,7 +453,6 @@ export function ConfigPanel({ config, fields, onConfigChange }: ConfigPanelProps
- {/* Filter Modal */} {modalFieldInfo && ( >(() => new Set(excludedValues)) const [search, setSearch] = useState('') const searchRef = useRef(null) - // Focus search input on mount useEffect(() => { if (!open) return const timer = setTimeout(() => searchRef.current?.focus(), 100) return () => clearTimeout(timer) }, [open]) - // Sort values naturally const sortedValues = useMemo( () => [...allValues].sort((a, b) => naturalSort(a, b)), [allValues] ) - // Filter by search const filteredValues = useMemo(() => { const q = search.trim().toLowerCase() if (!q) return sortedValues @@ -64,9 +59,7 @@ export function FilterModal({ const handleSelectAll = useCallback(() => { setLocalExcluded((prev) => { const next = new Set(prev) - for (const v of filteredValues) { - next.delete(v) - } + for (const v of filteredValues) next.delete(v) return next }) }, [filteredValues]) @@ -74,9 +67,7 @@ export function FilterModal({ const handleSelectNone = useCallback(() => { setLocalExcluded((prev) => { const next = new Set(prev) - for (const v of filteredValues) { - next.add(v) - } + for (const v of filteredValues) next.add(v) return next }) }, [filteredValues]) @@ -87,7 +78,6 @@ export function FilterModal({ }, [onApply, onClose, localExcluded]) const handleCancel = useCallback(() => { - // Reset to original and close setLocalExcluded(new Set(excludedValues)) setSearch('') onClose() @@ -98,7 +88,6 @@ export function FilterModal({ return (
- {/* Search */}
- {/* Quick actions */}
- | - @@ -131,7 +113,6 @@ export function FilterModal({
- {/* Value list */}
{filteredValues.length === 0 ? ( @@ -156,7 +137,6 @@ export function FilterModal({ )}
- {/* Actions */}
+
+ )} +
{children}
+
+
+ ) + } +) +Modal.displayName = 'Modal' diff --git a/packages/pivot-react/src/ui/Toggle.tsx b/packages/pivot-react/src/ui/Toggle.tsx new file mode 100644 index 0000000..bed80a2 --- /dev/null +++ b/packages/pivot-react/src/ui/Toggle.tsx @@ -0,0 +1,38 @@ +import { InputHTMLAttributes, forwardRef } from 'react' +import { cn } from '../lib/utils' + +export interface ToggleProps extends Omit, 'type'> { + label?: string +} + +export const Toggle = forwardRef( + ({ className, label, id, ...props }, ref) => ( +