diff --git a/biome.json b/biome.json index da1b9208c..823ebb96a 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.6/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.0/schema.json", "assist": { "actions": { "source": { @@ -16,7 +16,8 @@ }, "css": { "parser": { - "cssModules": true + "cssModules": true, + "tailwindDirectives": true } }, "linter": { diff --git a/next.config.ts b/next.config.ts index 8668b0f52..36fb6497c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -8,9 +8,9 @@ import type { NextConfig } from "next"; import "./src/env.js"; const nextConfig: NextConfig = { - // Already doing linting and typechecking as separate tasks in CI - eslint: { ignoreDuringBuilds: true }, + // Already doing typechecking as separate task in CI typescript: { ignoreBuildErrors: true }, + cacheComponents: true, }; export default nextConfig; diff --git a/package.json b/package.json index 12708fdc7..047bafb9d 100644 --- a/package.json +++ b/package.json @@ -54,13 +54,13 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", - "drizzle-orm": "^0.44.6", + "drizzle-orm": "^0.44.7", "export-to-csv": "^1.4.0", "geist": "^1.5.1", - "lucide-react": "^0.546.0", + "lucide-react": "^0.548.0", "motion": "^12.23.24", "nanoid": "^5.1.6", - "next": "^15.5.6", + "next": "^16.0.0", "next-themes": "^0.4.6", "nuqs": "^2.7.2", "postgres": "^3.4.7", @@ -69,18 +69,18 @@ "react-dom": "^19.2.0", "react-hook-form": "^7.65.0", "server-only": "^0.0.1", - "shadcn": "3.4.2", + "shadcn": "3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "vaul": "^1.1.2", "zod": "^4.1.12" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", + "@biomejs/biome": "^2.3.0", "@faker-js/faker": "^10.1.0", - "@tailwindcss/postcss": "^4.1.14", + "@tailwindcss/postcss": "^4.1.16", "@total-typescript/ts-reset": "^0.6.1", - "@types/node": "^24.8.1", + "@types/node": "^24.9.1", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "dotenv-cli": "^10.0.0", @@ -88,7 +88,7 @@ "pg": "^8.16.3", "postcss": "^8.5.6", "rimraf": "^6.0.1", - "tailwindcss": "^4.1.14", + "tailwindcss": "^4.1.16", "tsx": "^4.20.6", "tw-animate-css": "^1.4.0", "typescript": "^5.9.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aed3b1222..7e27b7852 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,17 +84,17 @@ importers: specifier: ^4.1.0 version: 4.1.0 drizzle-orm: - specifier: ^0.44.6 - version: 0.44.6(pg@8.16.3)(postgres@3.4.7) + specifier: ^0.44.7 + version: 0.44.7(pg@8.16.3)(postgres@3.4.7) export-to-csv: specifier: ^1.4.0 version: 1.4.0 geist: specifier: ^1.5.1 - version: 1.5.1(next@15.5.6(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) + version: 1.5.1(next@16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)) lucide-react: - specifier: ^0.546.0 - version: 0.546.0(react@19.2.0) + specifier: ^0.548.0 + version: 0.548.0(react@19.2.0) motion: specifier: ^12.23.24 version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -102,14 +102,14 @@ importers: specifier: ^5.1.6 version: 5.1.6 next: - specifier: ^15.5.6 - version: 15.5.6(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + specifier: ^16.0.0 + version: 16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) nuqs: specifier: ^2.7.2 - version: 2.7.2(next@15.5.6(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) + version: 2.7.2(next@16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) postgres: specifier: ^3.4.7 version: 3.4.7 @@ -129,8 +129,8 @@ importers: specifier: ^0.0.1 version: 0.0.1 shadcn: - specifier: 3.4.2 - version: 3.4.2(@types/node@24.8.1)(typescript@5.9.3) + specifier: 3.5.0 + version: 3.5.0(@types/node@24.9.1)(typescript@5.9.3) sonner: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -145,20 +145,20 @@ importers: version: 4.1.12 devDependencies: '@biomejs/biome': - specifier: ^2.2.6 - version: 2.2.6 + specifier: ^2.3.0 + version: 2.3.0 '@faker-js/faker': specifier: ^10.1.0 version: 10.1.0 '@tailwindcss/postcss': - specifier: ^4.1.14 - version: 4.1.14 + specifier: ^4.1.16 + version: 4.1.16 '@total-typescript/ts-reset': specifier: ^0.6.1 version: 0.6.1 '@types/node': - specifier: ^24.8.1 - version: 24.8.1 + specifier: ^24.9.1 + version: 24.9.1 '@types/react': specifier: ^19.2.2 version: 19.2.2 @@ -181,8 +181,8 @@ importers: specifier: ^6.0.1 version: 6.0.1 tailwindcss: - specifier: ^4.1.14 - version: 4.1.14 + specifier: ^4.1.16 + version: 4.1.16 tsx: specifier: ^4.20.6 version: 4.20.6 @@ -207,16 +207,16 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.4': - resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.4': - resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.27.3': @@ -227,8 +227,8 @@ packages: resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.28.3': - resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} + '@babel/helper-create-class-features-plugin@7.28.5': + resolution: {integrity: sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -237,8 +237,8 @@ packages: resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} - '@babel/helper-member-expression-to-functions@7.27.1': - resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.27.1': @@ -273,8 +273,8 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': @@ -285,8 +285,8 @@ packages: resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -308,14 +308,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.28.0': - resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + '@babel/plugin-transform-typescript@7.28.5': + resolution: {integrity: sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/preset-typescript@7.27.1': - resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -324,63 +324,63 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.4': - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.2.6': - resolution: {integrity: sha512-yKTCNGhek0rL5OEW1jbLeZX8LHaM8yk7+3JRGv08my+gkpmtb5dDE+54r2ZjZx0ediFEn1pYBOJSmOdDP9xtFw==} + '@biomejs/biome@2.3.0': + resolution: {integrity: sha512-shdUY5H3S3tJVUWoVWo5ua+GdPW5lRHf+b0IwZ4OC1o2zOKQECZ6l2KbU6t89FNhtd3Qx5eg5N7/UsQWGQbAFw==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.2.6': - resolution: {integrity: sha512-UZPmn3M45CjTYulgcrFJFZv7YmK3pTxTJDrFYlNElT2FNnkkX4fsxjExTSMeWKQYoZjvekpH5cvrYZZlWu3yfA==} + '@biomejs/cli-darwin-arm64@2.3.0': + resolution: {integrity: sha512-3cJVT0Z5pbTkoBmbjmDZTDFYxIkRcrs9sYVJbIBHU8E6qQxgXAaBfSVjjCreG56rfDuQBr43GzwzmaHPcu4vlw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.2.6': - resolution: {integrity: sha512-HOUIquhHVgh/jvxyClpwlpl/oeMqntlteL89YqjuFDiZ091P0vhHccwz+8muu3nTyHWM5FQslt+4Jdcd67+xWQ==} + '@biomejs/cli-darwin-x64@2.3.0': + resolution: {integrity: sha512-6LIkhglh3UGjuDqJXsK42qCA0XkD1Ke4K/raFOii7QQPbM8Pia7Qj2Hji4XuF2/R78hRmEx7uKJH3t/Y9UahtQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.2.6': - resolution: {integrity: sha512-TjCenQq3N6g1C+5UT3jE1bIiJb5MWQvulpUngTIpFsL4StVAUXucWD0SL9MCW89Tm6awWfeXBbZBAhJwjyFbRQ==} + '@biomejs/cli-linux-arm64-musl@2.3.0': + resolution: {integrity: sha512-nDksoFdwZ2YrE7NiYDhtMhL2UgFn8Kb7Y0bYvnTAakHnqEdb4lKindtBc1f+xg2Snz0JQhJUYO7r9CDBosRU5w==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.2.6': - resolution: {integrity: sha512-BpGtuMJGN+o8pQjvYsUKZ+4JEErxdSmcRD/JG3mXoWc6zrcA7OkuyGFN1mDggO0Q1n7qXxo/PcupHk8gzijt5g==} + '@biomejs/cli-linux-arm64@2.3.0': + resolution: {integrity: sha512-uhAsbXySX7xsXahegDg5h3CDgfMcRsJvWLFPG0pjkylgBb9lErbK2C0UINW52zhwg0cPISB09lxHPxCau4e2xA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.2.6': - resolution: {integrity: sha512-1ZcBux8zVM3JhWN2ZCPaYf0+ogxXG316uaoXJdgoPZcdK/rmRcRY7PqHdAos2ExzvjIdvhQp72UcveI98hgOog==} + '@biomejs/cli-linux-x64-musl@2.3.0': + resolution: {integrity: sha512-+i9UcJwl99uAhtRQDz9jUAh+Xkb097eekxs/D9j4deWDg5/yB/jPWzISe1nBHvlzTXsdUSj0VvB4Go2DSpKIMw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.2.6': - resolution: {integrity: sha512-1HaM/dpI/1Z68zp8ZdT6EiBq+/O/z97a2AiHMl+VAdv5/ELckFt9EvRb8hDHpk8hUMoz03gXkC7VPXOVtU7faA==} + '@biomejs/cli-linux-x64@2.3.0': + resolution: {integrity: sha512-uxa8reA2s1VgoH8MhbGlCmMOt3JuSE1vJBifkh1ulaPiuk0SPx8cCdpnm9NWnTe2x/LfWInWx4sZ7muaXTPGGw==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.2.6': - resolution: {integrity: sha512-h3A88G8PGM1ryTeZyLlSdfC/gz3e95EJw9BZmA6Po412DRqwqPBa2Y9U+4ZSGUAXCsnSQE00jLV8Pyrh0d+jQw==} + '@biomejs/cli-win32-arm64@2.3.0': + resolution: {integrity: sha512-ynjmsJLIKrAjC3CCnKMMhzcnNy8dbQWjKfSU5YA0mIruTxBNMbkAJp+Pr2iV7/hFou+66ZSD/WV8hmLEmhUaXA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.2.6': - resolution: {integrity: sha512-yx0CqeOhPjYQ5ZXgPfu8QYkgBhVJyvWe36as7jRuPrKPO5ylVDfwVtPQ+K/mooNTADW0IhxOZm3aPu16dP8yNQ==} + '@biomejs/cli-win32-x64@2.3.0': + resolution: {integrity: sha512-zOCYmCRVkWXc9v8P7OLbLlGGMxQTKMvi+5IC4v7O8DkjLCOHRzRVK/Lno2pGZNo0lzKM60pcQOhH8HVkXMQdFg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -429,8 +429,8 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 - '@emnapi/runtime@1.5.0': - resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + '@emnapi/runtime@1.6.0': + resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==} '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} @@ -925,10 +925,6 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -945,61 +941,61 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@modelcontextprotocol/sdk@1.20.1': - resolution: {integrity: sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==} + '@modelcontextprotocol/sdk@1.20.2': + resolution: {integrity: sha512-6rqTdFt67AAAzln3NOKsXRmv5ZzPkgbfaebKBqUbts7vK1GZudqnrun5a8d3M/h955cam9RHZ6Jb4Y1XhnmFPg==} engines: {node: '>=18'} - '@mswjs/interceptors@0.39.8': - resolution: {integrity: sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==} + '@mswjs/interceptors@0.40.0': + resolution: {integrity: sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==} engines: {node: '>=18'} - '@next/env@15.5.6': - resolution: {integrity: sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==} + '@next/env@16.0.0': + resolution: {integrity: sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA==} - '@next/swc-darwin-arm64@15.5.6': - resolution: {integrity: sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==} + '@next/swc-darwin-arm64@16.0.0': + resolution: {integrity: sha512-/CntqDCnk5w2qIwMiF0a9r6+9qunZzFmU0cBX4T82LOflE72zzH6gnOjCwUXYKOBlQi8OpP/rMj8cBIr18x4TA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.5.6': - resolution: {integrity: sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==} + '@next/swc-darwin-x64@16.0.0': + resolution: {integrity: sha512-hB4GZnJGKa8m4efvTGNyii6qs76vTNl+3dKHTCAUaksN6KjYy4iEO3Q5ira405NW2PKb3EcqWiRaL9DrYJfMHg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.5.6': - resolution: {integrity: sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==} + '@next/swc-linux-arm64-gnu@16.0.0': + resolution: {integrity: sha512-E2IHMdE+C1k+nUgndM13/BY/iJY9KGCphCftMh7SXWcaQqExq/pJU/1Hgn8n/tFwSoLoYC/yUghOv97tAsIxqg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.5.6': - resolution: {integrity: sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==} + '@next/swc-linux-arm64-musl@16.0.0': + resolution: {integrity: sha512-xzgl7c7BVk4+7PDWldU+On2nlwnGgFqJ1siWp3/8S0KBBLCjonB6zwJYPtl4MUY7YZJrzzumdUpUoquu5zk8vg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.5.6': - resolution: {integrity: sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==} + '@next/swc-linux-x64-gnu@16.0.0': + resolution: {integrity: sha512-sdyOg4cbiCw7YUr0F/7ya42oiVBXLD21EYkSwN+PhE4csJH4MSXUsYyslliiiBwkM+KsuQH/y9wuxVz6s7Nstg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.5.6': - resolution: {integrity: sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==} + '@next/swc-linux-x64-musl@16.0.0': + resolution: {integrity: sha512-IAXv3OBYqVaNOgyd3kxR4L3msuhmSy1bcchPHxDOjypG33i2yDWvGBwFD94OuuTjjTt/7cuIKtAmoOOml6kfbg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.5.6': - resolution: {integrity: sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==} + '@next/swc-win32-arm64-msvc@16.0.0': + resolution: {integrity: sha512-bmo3ncIJKUS9PWK1JD9pEVv0yuvp1KPuOsyJTHXTv8KDrEmgV/K+U0C75rl9rhIaODcS7JEb6/7eJhdwXI0XmA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.5.6': - resolution: {integrity: sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==} + '@next/swc-win32-x64-msvc@16.0.0': + resolution: {integrity: sha512-O1cJbT+lZp+cTjYyZGiDwsOjO3UHHzSqobkPNipdlnnuPb1swfcuY6r3p8dsKU4hAIEO4cO67ZCfVVH/M1ETXA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1508,65 +1504,65 @@ packages: zod: optional: true - '@tailwindcss/node@4.1.14': - resolution: {integrity: sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==} + '@tailwindcss/node@4.1.16': + resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==} - '@tailwindcss/oxide-android-arm64@4.1.14': - resolution: {integrity: sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==} + '@tailwindcss/oxide-android-arm64@4.1.16': + resolution: {integrity: sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.14': - resolution: {integrity: sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==} + '@tailwindcss/oxide-darwin-arm64@4.1.16': + resolution: {integrity: sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.14': - resolution: {integrity: sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==} + '@tailwindcss/oxide-darwin-x64@4.1.16': + resolution: {integrity: sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.14': - resolution: {integrity: sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==} + '@tailwindcss/oxide-freebsd-x64@4.1.16': + resolution: {integrity: sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': - resolution: {integrity: sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': + resolution: {integrity: sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': - resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': + resolution: {integrity: sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.14': - resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': + resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.14': - resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': + resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.14': - resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==} + '@tailwindcss/oxide-linux-x64-musl@4.1.16': + resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.14': - resolution: {integrity: sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==} + '@tailwindcss/oxide-wasm32-wasi@4.1.16': + resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -1577,24 +1573,24 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': - resolution: {integrity: sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': + resolution: {integrity: sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.14': - resolution: {integrity: sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': + resolution: {integrity: sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.14': - resolution: {integrity: sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==} + '@tailwindcss/oxide@4.1.16': + resolution: {integrity: sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==} engines: {node: '>= 10'} - '@tailwindcss/postcss@4.1.14': - resolution: {integrity: sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==} + '@tailwindcss/postcss@4.1.16': + resolution: {integrity: sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==} '@tanstack/react-table@8.21.3': resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} @@ -1622,8 +1618,8 @@ packages: '@ts-morph/common@0.27.0': resolution: {integrity: sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ==} - '@types/node@24.8.1': - resolution: {integrity: sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==} + '@types/node@24.9.1': + resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} '@types/react-dom@19.2.2': resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} @@ -1678,8 +1674,8 @@ packages: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} - baseline-browser-mapping@2.8.18: - resolution: {integrity: sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==} + baseline-browser-mapping@2.8.20: + resolution: {integrity: sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==} hasBin: true body-parser@2.2.0: @@ -1690,8 +1686,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.26.3: - resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + browserslist@4.27.0: + resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1721,10 +1717,6 @@ packages: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -1771,8 +1763,8 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} - commander@14.0.1: - resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} content-disposition@1.0.0: @@ -1884,8 +1876,8 @@ packages: resolution: {integrity: sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg==} hasBin: true - drizzle-orm@0.44.6: - resolution: {integrity: sha512-uy6uarrrEOc9K1u5/uhBFJbdF5VJ5xQ/Yzbecw3eAYOunv5FDeYkR2m8iitocdHBOHbvorviKOW5GVw0U1j4LQ==} + drizzle-orm@0.44.7: + resolution: {integrity: sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=4' @@ -1990,8 +1982,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.237: - resolution: {integrity: sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==} + electron-to-chromium@1.5.240: + resolution: {integrity: sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -2217,8 +2209,8 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} - get-tsconfig@4.12.0: - resolution: {integrity: sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2396,68 +2388,74 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - lightningcss-darwin-arm64@1.30.1: - resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.30.1: - resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - lightningcss-freebsd-x64@1.30.1: - resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.30.1: - resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.30.1: - resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.30.1: - resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.30.1: - resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.30.1: - resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-win32-arm64-msvc@1.30.1: - resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.30.1: - resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.30.1: - resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} lines-and-columns@1.2.4: @@ -2474,13 +2472,13 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@0.546.0: - resolution: {integrity: sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==} + lucide-react@0.548.0: + resolution: {integrity: sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} @@ -2532,10 +2530,6 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@3.1.0: - resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} - engines: {node: '>= 18'} - motion-dom@12.23.23: resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} @@ -2559,8 +2553,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msw@2.11.5: - resolution: {integrity: sha512-atFI4GjKSJComxcigz273honh8h4j5zzpk5kwG4tGm0TPcYne6bqmVrufeRll6auBeouIkXqZYXxVbWSWxM3RA==} + msw@2.11.6: + resolution: {integrity: sha512-MCYMykvmiYScyUm7I6y0VCxpNq1rgd5v7kG8ks5dKtvmxRUUPjribX6mUoUNBbM5/3PhUyoelEWiKXGOz84c+w==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -2593,9 +2587,9 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@15.5.6: - resolution: {integrity: sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==} - engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + next@16.0.0: + resolution: {integrity: sha512-nYohiNdxGu4OmBzggxy9rczmjIGI+TpR5vbKTsE1HqYwNm1B+YSiugSrFguX6omMOKnDHAmBPY4+8TNJk0Idyg==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -2623,8 +2617,8 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-releases@2.0.25: - resolution: {integrity: sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA==} + node-releases@2.0.26: + resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} @@ -2961,8 +2955,8 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shadcn@3.4.2: - resolution: {integrity: sha512-afa1UIHC9UzWX2NMHHwaZf6GfT5VGkTDen77RtyUBMfJ5i6ZQuzXOFTE1bFHTaWwftb8wQnMiG8kXdEu3Kx9IQ==} + shadcn@3.5.0: + resolution: {integrity: sha512-5f9tn7gHOiI8CqJ8LGrUEmX6dOivGgSbHkMCeOMCzxHZy9cPwyuRXt7ZyjwukO8QH9kGqi6oiKOpfvTXNGBP4g==} hasBin: true sharp@0.34.4: @@ -3091,17 +3085,13 @@ packages: tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} - tailwindcss@4.1.14: - resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==} + tailwindcss@4.1.16: + resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==} tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - tar@7.5.1: - resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} - engines: {node: '>=18'} - tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -3158,8 +3148,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@7.14.0: - resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} @@ -3176,8 +3166,8 @@ packages: until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -3255,10 +3245,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -3299,23 +3285,23 @@ snapshots: '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.28.4': {} + '@babel/compat-data@7.28.5': {} - '@babel/core@7.28.4': + '@babel/core@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -3325,197 +3311,197 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.28.3': + '@babel/generator@7.28.5': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.28.4 + '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.26.3 + browserslist: 4.27.0 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.4)': + '@babel/helper-create-class-features-plugin@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.5) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 semver: 6.3.1 transitivePeerDependencies: - supports-color '@babel/helper-globals@7.28.0': {} - '@babel/helper-member-expression-to-functions@7.27.1': + '@babel/helper-member-expression-to-functions@7.28.5': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.4)': + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/core': 7.28.5 + '@babel/helper-member-expression-to-functions': 7.28.5 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 - '@babel/parser@7.28.4': + '@babel/parser@7.28.5': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.4)': + '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-create-class-features-plugin': 7.28.5(@babel/core@7.28.5) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.27.1(@babel/core@7.28.4)': + '@babel/preset-typescript@7.28.5(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) transitivePeerDependencies: - supports-color '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 - '@babel/traverse@7.28.4': + '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.28.4': + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 - '@biomejs/biome@2.2.6': + '@biomejs/biome@2.3.0': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.2.6 - '@biomejs/cli-darwin-x64': 2.2.6 - '@biomejs/cli-linux-arm64': 2.2.6 - '@biomejs/cli-linux-arm64-musl': 2.2.6 - '@biomejs/cli-linux-x64': 2.2.6 - '@biomejs/cli-linux-x64-musl': 2.2.6 - '@biomejs/cli-win32-arm64': 2.2.6 - '@biomejs/cli-win32-x64': 2.2.6 - - '@biomejs/cli-darwin-arm64@2.2.6': + '@biomejs/cli-darwin-arm64': 2.3.0 + '@biomejs/cli-darwin-x64': 2.3.0 + '@biomejs/cli-linux-arm64': 2.3.0 + '@biomejs/cli-linux-arm64-musl': 2.3.0 + '@biomejs/cli-linux-x64': 2.3.0 + '@biomejs/cli-linux-x64-musl': 2.3.0 + '@biomejs/cli-win32-arm64': 2.3.0 + '@biomejs/cli-win32-x64': 2.3.0 + + '@biomejs/cli-darwin-arm64@2.3.0': optional: true - '@biomejs/cli-darwin-x64@2.2.6': + '@biomejs/cli-darwin-x64@2.3.0': optional: true - '@biomejs/cli-linux-arm64-musl@2.2.6': + '@biomejs/cli-linux-arm64-musl@2.3.0': optional: true - '@biomejs/cli-linux-arm64@2.2.6': + '@biomejs/cli-linux-arm64@2.3.0': optional: true - '@biomejs/cli-linux-x64-musl@2.2.6': + '@biomejs/cli-linux-x64-musl@2.3.0': optional: true - '@biomejs/cli-linux-x64@2.2.6': + '@biomejs/cli-linux-x64@2.3.0': optional: true - '@biomejs/cli-win32-arm64@2.2.6': + '@biomejs/cli-win32-arm64@2.3.0': optional: true - '@biomejs/cli-win32-x64@2.2.6': + '@biomejs/cli-win32-x64@2.3.0': optional: true '@date-fns/tz@1.4.1': {} @@ -3570,7 +3556,7 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 - '@emnapi/runtime@1.5.0': + '@emnapi/runtime@1.6.0': dependencies: tslib: 2.8.1 optional: true @@ -3583,7 +3569,7 @@ snapshots: '@esbuild-kit/esm-loader@2.6.5': dependencies: '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.12.0 + get-tsconfig: 4.13.0 '@esbuild/aix-ppc64@0.25.11': optional: true @@ -3830,7 +3816,7 @@ snapshots: '@img/sharp-wasm32@0.34.4': dependencies: - '@emnapi/runtime': 1.5.0 + '@emnapi/runtime': 1.6.0 optional: true '@img/sharp-win32-arm64@0.34.4': @@ -3844,31 +3830,31 @@ snapshots: '@inquirer/ansi@1.0.1': {} - '@inquirer/confirm@5.1.19(@types/node@24.8.1)': + '@inquirer/confirm@5.1.19(@types/node@24.9.1)': dependencies: - '@inquirer/core': 10.3.0(@types/node@24.8.1) - '@inquirer/type': 3.0.9(@types/node@24.8.1) + '@inquirer/core': 10.3.0(@types/node@24.9.1) + '@inquirer/type': 3.0.9(@types/node@24.9.1) optionalDependencies: - '@types/node': 24.8.1 + '@types/node': 24.9.1 - '@inquirer/core@10.3.0(@types/node@24.8.1)': + '@inquirer/core@10.3.0(@types/node@24.9.1)': dependencies: '@inquirer/ansi': 1.0.1 '@inquirer/figures': 1.0.14 - '@inquirer/type': 3.0.9(@types/node@24.8.1) + '@inquirer/type': 3.0.9(@types/node@24.9.1) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 24.8.1 + '@types/node': 24.9.1 '@inquirer/figures@1.0.14': {} - '@inquirer/type@3.0.9(@types/node@24.8.1)': + '@inquirer/type@3.0.9(@types/node@24.9.1)': optionalDependencies: - '@types/node': 24.8.1 + '@types/node': 24.9.1 '@isaacs/balanced-match@4.0.1': {} @@ -3885,10 +3871,6 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.2 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3908,7 +3890,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@modelcontextprotocol/sdk@1.20.1': + '@modelcontextprotocol/sdk@1.20.2': dependencies: ajv: 6.12.6 content-type: 1.0.5 @@ -3925,7 +3907,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@mswjs/interceptors@0.39.8': + '@mswjs/interceptors@0.40.0': dependencies: '@open-draft/deferred-promise': 2.2.0 '@open-draft/logger': 0.3.0 @@ -3934,30 +3916,30 @@ snapshots: outvariant: 1.4.3 strict-event-emitter: 0.5.1 - '@next/env@15.5.6': {} + '@next/env@16.0.0': {} - '@next/swc-darwin-arm64@15.5.6': + '@next/swc-darwin-arm64@16.0.0': optional: true - '@next/swc-darwin-x64@15.5.6': + '@next/swc-darwin-x64@16.0.0': optional: true - '@next/swc-linux-arm64-gnu@15.5.6': + '@next/swc-linux-arm64-gnu@16.0.0': optional: true - '@next/swc-linux-arm64-musl@15.5.6': + '@next/swc-linux-arm64-musl@16.0.0': optional: true - '@next/swc-linux-x64-gnu@15.5.6': + '@next/swc-linux-x64-gnu@16.0.0': optional: true - '@next/swc-linux-x64-musl@15.5.6': + '@next/swc-linux-x64-musl@16.0.0': optional: true - '@next/swc-win32-arm64-msvc@15.5.6': + '@next/swc-win32-arm64-msvc@16.0.0': optional: true - '@next/swc-win32-x64-msvc@15.5.6': + '@next/swc-win32-x64-msvc@16.0.0': optional: true '@noble/ciphers@1.3.0': {} @@ -4443,77 +4425,74 @@ snapshots: typescript: 5.9.3 zod: 4.1.12 - '@tailwindcss/node@4.1.14': + '@tailwindcss/node@4.1.16': dependencies: '@jridgewell/remapping': 2.3.5 enhanced-resolve: 5.18.3 jiti: 2.6.1 - lightningcss: 1.30.1 - magic-string: 0.30.19 + lightningcss: 1.30.2 + magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.14 + tailwindcss: 4.1.16 - '@tailwindcss/oxide-android-arm64@4.1.14': + '@tailwindcss/oxide-android-arm64@4.1.16': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.14': + '@tailwindcss/oxide-darwin-arm64@4.1.16': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.14': + '@tailwindcss/oxide-darwin-x64@4.1.16': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.14': + '@tailwindcss/oxide-freebsd-x64@4.1.16': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.14': + '@tailwindcss/oxide-linux-x64-musl@4.1.16': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.14': + '@tailwindcss/oxide-wasm32-wasi@4.1.16': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': optional: true - '@tailwindcss/oxide@4.1.14': - dependencies: - detect-libc: 2.1.2 - tar: 7.5.1 + '@tailwindcss/oxide@4.1.16': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.14 - '@tailwindcss/oxide-darwin-arm64': 4.1.14 - '@tailwindcss/oxide-darwin-x64': 4.1.14 - '@tailwindcss/oxide-freebsd-x64': 4.1.14 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.14 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.14 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.14 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.14 - '@tailwindcss/oxide-linux-x64-musl': 4.1.14 - '@tailwindcss/oxide-wasm32-wasi': 4.1.14 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.14 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.14 - - '@tailwindcss/postcss@4.1.14': + '@tailwindcss/oxide-android-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-x64': 4.1.16 + '@tailwindcss/oxide-freebsd-x64': 4.1.16 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.16 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.16 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-x64-musl': 4.1.16 + '@tailwindcss/oxide-wasm32-wasi': 4.1.16 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.16 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.16 + + '@tailwindcss/postcss@4.1.16': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.14 - '@tailwindcss/oxide': 4.1.14 + '@tailwindcss/node': 4.1.16 + '@tailwindcss/oxide': 4.1.16 postcss: 8.5.6 - tailwindcss: 4.1.14 + tailwindcss: 4.1.16 '@tanstack/react-table@8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: @@ -4539,9 +4518,9 @@ snapshots: minimatch: 10.0.3 path-browserify: 1.0.1 - '@types/node@24.8.1': + '@types/node@24.9.1': dependencies: - undici-types: 7.14.0 + undici-types: 7.16.0 '@types/react-dom@19.2.2(@types/react@19.2.2)': dependencies: @@ -4589,7 +4568,7 @@ snapshots: dependencies: tslib: 2.8.1 - baseline-browser-mapping@2.8.18: {} + baseline-browser-mapping@2.8.20: {} body-parser@2.2.0: dependencies: @@ -4609,13 +4588,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.26.3: + browserslist@4.27.0: dependencies: - baseline-browser-mapping: 2.8.18 + baseline-browser-mapping: 2.8.20 caniuse-lite: 1.0.30001751 - electron-to-chromium: 1.5.237 - node-releases: 2.0.25 - update-browserslist-db: 1.1.3(browserslist@4.26.3) + electron-to-chromium: 1.5.240 + node-releases: 2.0.26 + update-browserslist-db: 1.1.4(browserslist@4.27.0) buffer-from@1.1.2: {} @@ -4637,8 +4616,6 @@ snapshots: chalk@5.6.2: {} - chownr@3.0.0: {} - class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -4683,7 +4660,7 @@ snapshots: commander@11.1.0: {} - commander@14.0.1: {} + commander@14.0.2: {} content-disposition@1.0.0: dependencies: @@ -4767,7 +4744,7 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.44.6(pg@8.16.3)(postgres@3.4.7): + drizzle-orm@0.44.7(pg@8.16.3)(postgres@3.4.7): optionalDependencies: pg: 8.16.3 postgres: 3.4.7 @@ -4789,7 +4766,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.237: {} + electron-to-chromium@1.5.240: {} emoji-regex@10.6.0: {} @@ -5039,9 +5016,9 @@ snapshots: fzf@0.5.2: {} - geist@1.5.1(next@15.5.6(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)): + geist@1.5.1(next@16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)): dependencies: - next: 15.5.6(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) gensync@1.0.0-beta.2: {} @@ -5078,7 +5055,7 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 - get-tsconfig@4.12.0: + get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -5213,50 +5190,54 @@ snapshots: kleur@4.1.5: {} - lightningcss-darwin-arm64@1.30.1: + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: optional: true - lightningcss-darwin-x64@1.30.1: + lightningcss-darwin-x64@1.30.2: optional: true - lightningcss-freebsd-x64@1.30.1: + lightningcss-freebsd-x64@1.30.2: optional: true - lightningcss-linux-arm-gnueabihf@1.30.1: + lightningcss-linux-arm-gnueabihf@1.30.2: optional: true - lightningcss-linux-arm64-gnu@1.30.1: + lightningcss-linux-arm64-gnu@1.30.2: optional: true - lightningcss-linux-arm64-musl@1.30.1: + lightningcss-linux-arm64-musl@1.30.2: optional: true - lightningcss-linux-x64-gnu@1.30.1: + lightningcss-linux-x64-gnu@1.30.2: optional: true - lightningcss-linux-x64-musl@1.30.1: + lightningcss-linux-x64-musl@1.30.2: optional: true - lightningcss-win32-arm64-msvc@1.30.1: + lightningcss-win32-arm64-msvc@1.30.2: optional: true - lightningcss-win32-x64-msvc@1.30.1: + lightningcss-win32-x64-msvc@1.30.2: optional: true - lightningcss@1.30.1: + lightningcss@1.30.2: dependencies: detect-libc: 2.1.2 optionalDependencies: - lightningcss-darwin-arm64: 1.30.1 - lightningcss-darwin-x64: 1.30.1 - lightningcss-freebsd-x64: 1.30.1 - lightningcss-linux-arm-gnueabihf: 1.30.1 - lightningcss-linux-arm64-gnu: 1.30.1 - lightningcss-linux-arm64-musl: 1.30.1 - lightningcss-linux-x64-gnu: 1.30.1 - lightningcss-linux-x64-musl: 1.30.1 - lightningcss-win32-arm64-msvc: 1.30.1 - lightningcss-win32-x64-msvc: 1.30.1 + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 lines-and-columns@1.2.4: {} @@ -5271,11 +5252,11 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@0.546.0(react@19.2.0): + lucide-react@0.548.0(react@19.2.0): dependencies: react: 19.2.0 - magic-string@0.30.19: + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5312,10 +5293,6 @@ snapshots: minipass@7.1.2: {} - minizlib@3.1.0: - dependencies: - minipass: 7.1.2 - motion-dom@12.23.23: dependencies: motion-utils: 12.23.6 @@ -5332,10 +5309,10 @@ snapshots: ms@2.1.3: {} - msw@2.11.5(@types/node@24.8.1)(typescript@5.9.3): + msw@2.11.6(@types/node@24.9.1)(typescript@5.9.3): dependencies: - '@inquirer/confirm': 5.1.19(@types/node@24.8.1) - '@mswjs/interceptors': 0.39.8 + '@inquirer/confirm': 5.1.19(@types/node@24.9.1) + '@mswjs/interceptors': 0.40.0 '@open-draft/deferred-promise': 2.2.0 '@types/statuses': 2.0.6 cookie: 1.0.2 @@ -5370,24 +5347,24 @@ snapshots: react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - next@15.5.6(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + next@16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@next/env': 15.5.6 + '@next/env': 16.0.0 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001751 postcss: 8.4.31 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - styled-jsx: 5.1.6(@babel/core@7.28.4)(react@19.2.0) + styled-jsx: 5.1.6(@babel/core@7.28.5)(react@19.2.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.5.6 - '@next/swc-darwin-x64': 15.5.6 - '@next/swc-linux-arm64-gnu': 15.5.6 - '@next/swc-linux-arm64-musl': 15.5.6 - '@next/swc-linux-x64-gnu': 15.5.6 - '@next/swc-linux-x64-musl': 15.5.6 - '@next/swc-win32-arm64-msvc': 15.5.6 - '@next/swc-win32-x64-msvc': 15.5.6 + '@next/swc-darwin-arm64': 16.0.0 + '@next/swc-darwin-x64': 16.0.0 + '@next/swc-linux-arm64-gnu': 16.0.0 + '@next/swc-linux-arm64-musl': 16.0.0 + '@next/swc-linux-x64-gnu': 16.0.0 + '@next/swc-linux-x64-musl': 16.0.0 + '@next/swc-win32-arm64-msvc': 16.0.0 + '@next/swc-win32-x64-msvc': 16.0.0 sharp: 0.34.4 transitivePeerDependencies: - '@babel/core' @@ -5401,7 +5378,7 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-releases@2.0.25: {} + node-releases@2.0.26: {} npm-run-path@4.0.1: dependencies: @@ -5412,12 +5389,12 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 - nuqs@2.7.2(next@15.5.6(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0): + nuqs@2.7.2(next@16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0): dependencies: '@standard-schema/spec': 1.0.0 react: 19.2.0 optionalDependencies: - next: 15.5.6(@babel/core@7.28.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + next: 16.0.0(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) object-assign@4.1.1: {} @@ -5714,17 +5691,17 @@ snapshots: setprototypeof@1.2.0: {} - shadcn@3.4.2(@types/node@24.8.1)(typescript@5.9.3): + shadcn@3.5.0(@types/node@24.9.1)(typescript@5.9.3): dependencies: '@antfu/ni': 25.0.0 - '@babel/core': 7.28.4 - '@babel/parser': 7.28.4 - '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4) - '@babel/preset-typescript': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) '@dotenvx/dotenvx': 1.51.0 - '@modelcontextprotocol/sdk': 1.20.1 - browserslist: 4.26.3 - commander: 14.0.1 + '@modelcontextprotocol/sdk': 1.20.2 + browserslist: 4.27.0 + commander: 14.0.2 cosmiconfig: 9.0.0(typescript@5.9.3) dedent: 1.7.0 deepmerge: 4.3.1 @@ -5735,7 +5712,7 @@ snapshots: fuzzysort: 3.1.0 https-proxy-agent: 7.0.6 kleur: 4.1.5 - msw: 2.11.5(@types/node@24.8.1)(typescript@5.9.3) + msw: 2.11.6(@types/node@24.9.1)(typescript@5.9.3) node-fetch: 3.3.2 ora: 8.2.0 postcss: 8.5.6 @@ -5884,27 +5861,19 @@ snapshots: strip-final-newline@4.0.0: {} - styled-jsx@5.1.6(@babel/core@7.28.4)(react@19.2.0): + styled-jsx@5.1.6(@babel/core@7.28.5)(react@19.2.0): dependencies: client-only: 0.0.1 react: 19.2.0 optionalDependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 tailwind-merge@3.3.1: {} - tailwindcss@4.1.14: {} + tailwindcss@4.1.16: {} tapable@2.3.0: {} - tar@7.5.1: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.1.0 - yallist: 5.0.0 - tiny-invariant@1.3.3: {} tinyexec@1.0.1: {} @@ -5941,7 +5910,7 @@ snapshots: tsx@4.20.6: dependencies: esbuild: 0.25.11 - get-tsconfig: 4.12.0 + get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 @@ -5957,7 +5926,7 @@ snapshots: typescript@5.9.3: {} - undici-types@7.14.0: {} + undici-types@7.16.0: {} unicorn-magic@0.3.0: {} @@ -5967,9 +5936,9 @@ snapshots: until-async@3.0.2: {} - update-browserslist-db@1.1.3(browserslist@4.26.3): + update-browserslist-db@1.1.4(browserslist@4.27.0): dependencies: - browserslist: 4.26.3 + browserslist: 4.27.0 escalade: 3.2.0 picocolors: 1.1.1 @@ -6039,8 +6008,6 @@ snapshots: yallist@3.1.1: {} - yallist@5.0.0: {} - yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/public/r/data-grid-row-height-menu.json b/public/r/data-grid-row-height-menu.json index 3381a2027..88baba6a4 100644 --- a/public/r/data-grid-row-height-menu.json +++ b/public/r/data-grid-row-height-menu.json @@ -14,7 +14,7 @@ "files": [ { "path": "src/components/data-grid/data-grid-row-height-menu.tsx", - "content": "\"use client\";\r\n\r\nimport type { Table } from \"@tanstack/react-table\";\r\nimport {\r\n AlignVerticalSpaceAroundIcon,\r\n ChevronsDownUpIcon,\r\n EqualIcon,\r\n MinusIcon,\r\n} from \"lucide-react\";\r\nimport * as React from \"react\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\n\r\nconst rowHeights = [\r\n {\r\n label: \"Short\",\r\n value: \"short\" as const,\r\n icon: MinusIcon,\r\n },\r\n {\r\n label: \"Medium\",\r\n value: \"medium\" as const,\r\n icon: EqualIcon,\r\n },\r\n {\r\n label: \"Tall\",\r\n value: \"tall\" as const,\r\n icon: AlignVerticalSpaceAroundIcon,\r\n },\r\n {\r\n label: \"Extra Tall\",\r\n value: \"extra-tall\" as const,\r\n icon: ChevronsDownUpIcon,\r\n },\r\n] as const;\r\n\r\ninterface DataGridRowHeightMenuProps\r\n extends React.ComponentProps {\r\n table: Table;\r\n}\r\n\r\nexport function DataGridRowHeightMenu({\r\n table,\r\n ...props\r\n}: DataGridRowHeightMenuProps) {\r\n const rowHeight = table.options.meta?.rowHeight;\r\n const onRowHeightChange = table.options.meta?.onRowHeightChange;\r\n\r\n const selectedRowHeight = React.useMemo(() => {\r\n return (\r\n rowHeights.find((opt) => opt.value === rowHeight) ?? {\r\n label: \"Short\",\r\n value: \"short\" as const,\r\n icon: MinusIcon,\r\n }\r\n );\r\n }, [rowHeight]);\r\n\r\n return (\r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport {\n AlignVerticalSpaceAroundIcon,\n ChevronsDownUpIcon,\n EqualIcon,\n MinusIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\n\nconst rowHeights = [\n {\n label: \"Short\",\n value: \"short\" as const,\n icon: MinusIcon,\n },\n {\n label: \"Medium\",\n value: \"medium\" as const,\n icon: EqualIcon,\n },\n {\n label: \"Tall\",\n value: \"tall\" as const,\n icon: AlignVerticalSpaceAroundIcon,\n },\n {\n label: \"Extra Tall\",\n value: \"extra-tall\" as const,\n icon: ChevronsDownUpIcon,\n },\n] as const;\n\ninterface DataGridRowHeightMenuProps\n extends React.ComponentProps {\n table: Table;\n}\n\nexport function DataGridRowHeightMenu({\n table,\n ...props\n}: DataGridRowHeightMenuProps) {\n const rowHeight = table.options.meta?.rowHeight;\n const onRowHeightChange = table.options.meta?.onRowHeightChange;\n\n const selectedRowHeight = React.useMemo(() => {\n return (\n rowHeights.find((opt) => opt.value === rowHeight) ?? {\n label: \"Short\",\n value: \"short\" as const,\n icon: MinusIcon,\n }\n );\n }, [rowHeight]);\n\n return (\n \n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-row-height-menu.tsx" } diff --git a/public/r/data-grid-sort-menu.json b/public/r/data-grid-sort-menu.json index a035251a1..6df328100 100644 --- a/public/r/data-grid-sort-menu.json +++ b/public/r/data-grid-sort-menu.json @@ -18,13 +18,13 @@ "files": [ { "path": "src/components/data-grid/data-grid-sort-menu.tsx", - "content": "\"use client\";\r\n\r\nimport type { ColumnSort, SortDirection, Table } from \"@tanstack/react-table\";\r\nimport {\r\n ArrowDownUp,\r\n ChevronsUpDown,\r\n GripVertical,\r\n Trash2,\r\n} from \"lucide-react\";\r\nimport * as React from \"react\";\r\n\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n} from \"@/components/ui/command\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport {\r\n Sortable,\r\n SortableContent,\r\n SortableItem,\r\n SortableItemHandle,\r\n SortableOverlay,\r\n} from \"@/components/ui/sortable\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nconst SORT_SHORTCUT_KEY = \"s\";\r\nconst REMOVE_SORT_SHORTCUTS = [\"backspace\", \"delete\"];\r\n\r\nconst SORT_ORDERS = [\r\n { label: \"Asc\", value: \"asc\" },\r\n { label: \"Desc\", value: \"desc\" },\r\n];\r\n\r\ninterface DataGridSortMenuProps\r\n extends React.ComponentProps {\r\n table: Table;\r\n}\r\n\r\nexport function DataGridSortMenu({\r\n table,\r\n ...props\r\n}: DataGridSortMenuProps) {\r\n const id = React.useId();\r\n const labelId = React.useId();\r\n const descriptionId = React.useId();\r\n const [open, setOpen] = React.useState(false);\r\n const addButtonRef = React.useRef(null);\r\n\r\n const sorting = table.getState().sorting;\r\n const onSortingChange = table.setSorting;\r\n\r\n const { columnLabels, columns } = React.useMemo(() => {\r\n const labels = new Map();\r\n const sortingIds = new Set(sorting.map((s) => s.id));\r\n const availableColumns: { id: string; label: string }[] = [];\r\n\r\n for (const column of table.getAllColumns()) {\r\n if (!column.getCanSort()) continue;\r\n\r\n const label = column.columnDef.meta?.label ?? column.id;\r\n labels.set(column.id, label);\r\n\r\n if (!sortingIds.has(column.id)) {\r\n availableColumns.push({ id: column.id, label });\r\n }\r\n }\r\n\r\n return {\r\n columnLabels: labels,\r\n columns: availableColumns,\r\n };\r\n }, [sorting, table]);\r\n\r\n const onSortAdd = React.useCallback(() => {\r\n const firstColumn = columns[0];\r\n if (!firstColumn) return;\r\n\r\n onSortingChange((prevSorting) => [\r\n ...prevSorting,\r\n { id: firstColumn.id, desc: false },\r\n ]);\r\n }, [columns, onSortingChange]);\r\n\r\n const onSortUpdate = React.useCallback(\r\n (sortId: string, updates: Partial) => {\r\n onSortingChange((prevSorting) => {\r\n if (!prevSorting) return prevSorting;\r\n return prevSorting.map((sort) =>\r\n sort.id === sortId ? { ...sort, ...updates } : sort,\r\n );\r\n });\r\n },\r\n [onSortingChange],\r\n );\r\n\r\n const onSortRemove = React.useCallback(\r\n (sortId: string) => {\r\n onSortingChange((prevSorting) =>\r\n prevSorting.filter((item) => item.id !== sortId),\r\n );\r\n },\r\n [onSortingChange],\r\n );\r\n\r\n const onSortingReset = React.useCallback(\r\n () => onSortingChange(table.initialState.sorting),\r\n [onSortingChange, table.initialState.sorting],\r\n );\r\n\r\n React.useEffect(() => {\r\n function onKeyDown(event: KeyboardEvent) {\r\n if (\r\n event.target instanceof HTMLInputElement ||\r\n event.target instanceof HTMLTextAreaElement ||\r\n (event.target instanceof HTMLElement &&\r\n event.target.contentEditable === \"true\")\r\n ) {\r\n return;\r\n }\r\n\r\n if (\r\n event.key.toLowerCase() === SORT_SHORTCUT_KEY &&\r\n (event.ctrlKey || event.metaKey) &&\r\n event.shiftKey\r\n ) {\r\n event.preventDefault();\r\n setOpen((prev) => !prev);\r\n }\r\n }\r\n\r\n window.addEventListener(\"keydown\", onKeyDown);\r\n return () => window.removeEventListener(\"keydown\", onKeyDown);\r\n }, []);\r\n\r\n const onTriggerKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (\r\n REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase()) &&\r\n sorting.length > 0\r\n ) {\r\n event.preventDefault();\r\n onSortingReset();\r\n }\r\n },\r\n [sorting.length, onSortingReset],\r\n );\r\n\r\n return (\r\n item.id}\r\n >\r\n \r\n \r\n \r\n \r\n Sort\r\n {sorting.length > 0 && (\r\n \r\n {sorting.length}\r\n \r\n )}\r\n \r\n \r\n \r\n
\r\n

\r\n {sorting.length > 0 ? \"Sort by\" : \"No sorting applied\"}\r\n

\r\n 0 && \"sr-only\",\r\n )}\r\n >\r\n {sorting.length > 0\r\n ? \"Modify sorting to organize your rows.\"\r\n : \"Add sorting to organize your rows.\"}\r\n

\r\n
\r\n {sorting.length > 0 && (\r\n \r\n
    \r\n {sorting.map((sort) => (\r\n \r\n ))}\r\n
\r\n
\r\n )}\r\n
\r\n \r\n Add sort\r\n \r\n {sorting.length > 0 && (\r\n \r\n Reset sorting\r\n \r\n )}\r\n
\r\n \r\n
\r\n \r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n );\r\n}\r\n\r\ninterface DataTableSortItemProps {\r\n sort: ColumnSort;\r\n sortItemId: string;\r\n columns: { id: string; label: string }[];\r\n columnLabels: Map;\r\n onSortUpdate: (sortId: string, updates: Partial) => void;\r\n onSortRemove: (sortId: string) => void;\r\n}\r\n\r\nfunction DataTableSortItem({\r\n sort,\r\n sortItemId,\r\n columns,\r\n columnLabels,\r\n onSortUpdate,\r\n onSortRemove,\r\n}: DataTableSortItemProps) {\r\n const fieldListboxId = `${sortItemId}-field-listbox`;\r\n const fieldTriggerId = `${sortItemId}-field-trigger`;\r\n const directionListboxId = `${sortItemId}-direction-listbox`;\r\n\r\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\r\n const [showDirectionSelector, setShowDirectionSelector] =\r\n React.useState(false);\r\n\r\n const onItemKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (\r\n event.target instanceof HTMLInputElement ||\r\n event.target instanceof HTMLTextAreaElement\r\n ) {\r\n return;\r\n }\r\n\r\n if (showFieldSelector || showDirectionSelector) {\r\n return;\r\n }\r\n\r\n if (REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase())) {\r\n event.preventDefault();\r\n onSortRemove(sort.id);\r\n }\r\n },\r\n [sort.id, showFieldSelector, showDirectionSelector, onSortRemove],\r\n );\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n \r\n {columnLabels.get(sort.id)}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n No fields found.\r\n \r\n {columns.map((column) => (\r\n onSortUpdate(sort.id, { id: value })}\r\n >\r\n {column.label}\r\n \r\n ))}\r\n \r\n \r\n \r\n \r\n \r\n \r\n onSortUpdate(sort.id, { desc: value === \"desc\" })\r\n }\r\n >\r\n \r\n \r\n \r\n \r\n {SORT_ORDERS.map((order) => (\r\n \r\n {order.label}\r\n \r\n ))}\r\n \r\n \r\n onSortRemove(sort.id)}\r\n >\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { ColumnSort, SortDirection, Table } from \"@tanstack/react-table\";\nimport {\n ArrowDownUp,\n ChevronsUpDown,\n GripVertical,\n Trash2,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n} from \"@/components/ui/sortable\";\nimport { cn } from \"@/lib/utils\";\n\nconst SORT_SHORTCUT_KEY = \"s\";\nconst REMOVE_SORT_SHORTCUTS = [\"backspace\", \"delete\"];\n\nconst SORT_ORDERS = [\n { label: \"Asc\", value: \"asc\" },\n { label: \"Desc\", value: \"desc\" },\n];\n\ninterface DataGridSortMenuProps\n extends React.ComponentProps {\n table: Table;\n}\n\nexport function DataGridSortMenu({\n table,\n ...props\n}: DataGridSortMenuProps) {\n const id = React.useId();\n const labelId = React.useId();\n const descriptionId = React.useId();\n const [open, setOpen] = React.useState(false);\n const addButtonRef = React.useRef(null);\n\n const sorting = table.getState().sorting;\n const onSortingChange = table.setSorting;\n\n const { columnLabels, columns } = React.useMemo(() => {\n const labels = new Map();\n const sortingIds = new Set(sorting.map((s) => s.id));\n const availableColumns: { id: string; label: string }[] = [];\n\n for (const column of table.getAllColumns()) {\n if (!column.getCanSort()) continue;\n\n const label = column.columnDef.meta?.label ?? column.id;\n labels.set(column.id, label);\n\n if (!sortingIds.has(column.id)) {\n availableColumns.push({ id: column.id, label });\n }\n }\n\n return {\n columnLabels: labels,\n columns: availableColumns,\n };\n }, [sorting, table]);\n\n const onSortAdd = React.useCallback(() => {\n const firstColumn = columns[0];\n if (!firstColumn) return;\n\n onSortingChange((prevSorting) => [\n ...prevSorting,\n { id: firstColumn.id, desc: false },\n ]);\n }, [columns, onSortingChange]);\n\n const onSortUpdate = React.useCallback(\n (sortId: string, updates: Partial) => {\n onSortingChange((prevSorting) => {\n if (!prevSorting) return prevSorting;\n return prevSorting.map((sort) =>\n sort.id === sortId ? { ...sort, ...updates } : sort,\n );\n });\n },\n [onSortingChange],\n );\n\n const onSortRemove = React.useCallback(\n (sortId: string) => {\n onSortingChange((prevSorting) =>\n prevSorting.filter((item) => item.id !== sortId),\n );\n },\n [onSortingChange],\n );\n\n const onSortingReset = React.useCallback(\n () => onSortingChange(table.initialState.sorting),\n [onSortingChange, table.initialState.sorting],\n );\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement ||\n (event.target instanceof HTMLElement &&\n event.target.contentEditable === \"true\")\n ) {\n return;\n }\n\n if (\n event.key.toLowerCase() === SORT_SHORTCUT_KEY &&\n (event.ctrlKey || event.metaKey) &&\n event.shiftKey\n ) {\n event.preventDefault();\n setOpen((prev) => !prev);\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, []);\n\n const onTriggerKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase()) &&\n sorting.length > 0\n ) {\n event.preventDefault();\n onSortingReset();\n }\n },\n [sorting.length, onSortingReset],\n );\n\n return (\n item.id}\n >\n \n \n \n \n Sort\n {sorting.length > 0 && (\n \n {sorting.length}\n \n )}\n \n \n \n
\n

\n {sorting.length > 0 ? \"Sort by\" : \"No sorting applied\"}\n

\n 0 && \"sr-only\",\n )}\n >\n {sorting.length > 0\n ? \"Modify sorting to organize your rows.\"\n : \"Add sorting to organize your rows.\"}\n

\n
\n {sorting.length > 0 && (\n \n
    \n {sorting.map((sort) => (\n \n ))}\n
\n
\n )}\n
\n \n Add sort\n \n {sorting.length > 0 && (\n \n Reset sorting\n \n )}\n
\n \n
\n \n
\n
\n
\n
\n
\n
\n \n \n );\n}\n\ninterface DataTableSortItemProps {\n sort: ColumnSort;\n sortItemId: string;\n columns: { id: string; label: string }[];\n columnLabels: Map;\n onSortUpdate: (sortId: string, updates: Partial) => void;\n onSortRemove: (sortId: string) => void;\n}\n\nfunction DataTableSortItem({\n sort,\n sortItemId,\n columns,\n columnLabels,\n onSortUpdate,\n onSortRemove,\n}: DataTableSortItemProps) {\n const fieldListboxId = `${sortItemId}-field-listbox`;\n const fieldTriggerId = `${sortItemId}-field-trigger`;\n const directionListboxId = `${sortItemId}-direction-listbox`;\n\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n const [showDirectionSelector, setShowDirectionSelector] =\n React.useState(false);\n\n const onItemKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (showFieldSelector || showDirectionSelector) {\n return;\n }\n\n if (REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase())) {\n event.preventDefault();\n onSortRemove(sort.id);\n }\n },\n [sort.id, showFieldSelector, showDirectionSelector, onSortRemove],\n );\n\n return (\n \n \n \n \n \n {columnLabels.get(sort.id)}\n \n \n \n \n \n \n \n No fields found.\n \n {columns.map((column) => (\n onSortUpdate(sort.id, { id: value })}\n >\n {column.label}\n \n ))}\n \n \n \n \n \n \n onSortUpdate(sort.id, { desc: value === \"desc\" })\n }\n >\n \n \n \n \n {SORT_ORDERS.map((order) => (\n \n {order.label}\n \n ))}\n \n \n onSortRemove(sort.id)}\n >\n \n \n \n \n \n \n \n \n \n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-sort-menu.tsx" }, { "path": "src/components/ui/sortable.tsx", - "content": "\"use client\";\r\n\r\nimport {\r\n type Announcements,\r\n closestCenter,\r\n closestCorners,\r\n DndContext,\r\n type DndContextProps,\r\n type DragEndEvent,\r\n type DraggableAttributes,\r\n type DraggableSyntheticListeners,\r\n DragOverlay,\r\n type DragStartEvent,\r\n type DropAnimation,\r\n defaultDropAnimationSideEffects,\r\n KeyboardSensor,\r\n MouseSensor,\r\n type ScreenReaderInstructions,\r\n TouchSensor,\r\n type UniqueIdentifier,\r\n useSensor,\r\n useSensors,\r\n} from \"@dnd-kit/core\";\r\nimport {\r\n restrictToHorizontalAxis,\r\n restrictToParentElement,\r\n restrictToVerticalAxis,\r\n} from \"@dnd-kit/modifiers\";\r\nimport {\r\n arrayMove,\r\n horizontalListSortingStrategy,\r\n SortableContext,\r\n type SortableContextProps,\r\n sortableKeyboardCoordinates,\r\n useSortable,\r\n verticalListSortingStrategy,\r\n} from \"@dnd-kit/sortable\";\r\nimport { CSS } from \"@dnd-kit/utilities\";\r\nimport { Slot } from \"@radix-ui/react-slot\";\r\nimport * as React from \"react\";\r\nimport * as ReactDOM from \"react-dom\";\r\nimport { useComposedRefs } from \"@/lib/compose-refs\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nconst orientationConfig = {\r\n vertical: {\r\n modifiers: [restrictToVerticalAxis, restrictToParentElement],\r\n strategy: verticalListSortingStrategy,\r\n collisionDetection: closestCenter,\r\n },\r\n horizontal: {\r\n modifiers: [restrictToHorizontalAxis, restrictToParentElement],\r\n strategy: horizontalListSortingStrategy,\r\n collisionDetection: closestCenter,\r\n },\r\n mixed: {\r\n modifiers: [restrictToParentElement],\r\n strategy: undefined,\r\n collisionDetection: closestCorners,\r\n },\r\n};\r\n\r\nconst ROOT_NAME = \"Sortable\";\r\nconst CONTENT_NAME = \"SortableContent\";\r\nconst ITEM_NAME = \"SortableItem\";\r\nconst ITEM_HANDLE_NAME = \"SortableItemHandle\";\r\nconst OVERLAY_NAME = \"SortableOverlay\";\r\n\r\ninterface SortableRootContextValue {\r\n id: string;\r\n items: UniqueIdentifier[];\r\n modifiers: DndContextProps[\"modifiers\"];\r\n strategy: SortableContextProps[\"strategy\"];\r\n activeId: UniqueIdentifier | null;\r\n setActiveId: (id: UniqueIdentifier | null) => void;\r\n getItemValue: (item: T) => UniqueIdentifier;\r\n flatCursor: boolean;\r\n}\r\n\r\nconst SortableRootContext =\r\n React.createContext | null>(null);\r\n\r\nfunction useSortableContext(consumerName: string) {\r\n const context = React.useContext(SortableRootContext);\r\n if (!context) {\r\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\r\n }\r\n return context;\r\n}\r\n\r\ninterface GetItemValue {\r\n /**\r\n * Callback that returns a unique identifier for each sortable item. Required for array of objects.\r\n * @example getItemValue={(item) => item.id}\r\n */\r\n getItemValue: (item: T) => UniqueIdentifier;\r\n}\r\n\r\ntype SortableRootProps = DndContextProps &\r\n (T extends object ? GetItemValue : Partial>) & {\r\n value: T[];\r\n onValueChange?: (items: T[]) => void;\r\n onMove?: (\r\n event: DragEndEvent & { activeIndex: number; overIndex: number },\r\n ) => void;\r\n strategy?: SortableContextProps[\"strategy\"];\r\n orientation?: \"vertical\" | \"horizontal\" | \"mixed\";\r\n flatCursor?: boolean;\r\n };\r\n\r\nfunction SortableRoot(props: SortableRootProps) {\r\n const {\r\n value,\r\n onValueChange,\r\n collisionDetection,\r\n modifiers,\r\n strategy,\r\n onMove,\r\n orientation = \"vertical\",\r\n flatCursor = false,\r\n getItemValue: getItemValueProp,\r\n accessibility,\r\n onDragStart: onDragStartProp,\r\n onDragEnd: onDragEndProp,\r\n onDragCancel: onDragCancelProp,\r\n ...sortableProps\r\n } = props;\r\n\r\n const id = React.useId();\r\n const [activeId, setActiveId] = React.useState(null);\r\n\r\n const sensors = useSensors(\r\n useSensor(MouseSensor),\r\n useSensor(TouchSensor),\r\n useSensor(KeyboardSensor, {\r\n coordinateGetter: sortableKeyboardCoordinates,\r\n }),\r\n );\r\n const config = React.useMemo(\r\n () => orientationConfig[orientation],\r\n [orientation],\r\n );\r\n\r\n const getItemValue = React.useCallback(\r\n (item: T): UniqueIdentifier => {\r\n if (typeof item === \"object\" && !getItemValueProp) {\r\n throw new Error(\"getItemValue is required when using array of objects\");\r\n }\r\n return getItemValueProp\r\n ? getItemValueProp(item)\r\n : (item as UniqueIdentifier);\r\n },\r\n [getItemValueProp],\r\n );\r\n\r\n const items = React.useMemo(() => {\r\n return value.map((item) => getItemValue(item));\r\n }, [value, getItemValue]);\r\n\r\n const onDragStart = React.useCallback(\r\n (event: DragStartEvent) => {\r\n onDragStartProp?.(event);\r\n\r\n if (event.activatorEvent.defaultPrevented) return;\r\n\r\n setActiveId(event.active.id);\r\n },\r\n [onDragStartProp],\r\n );\r\n\r\n const onDragEnd = React.useCallback(\r\n (event: DragEndEvent) => {\r\n onDragEndProp?.(event);\r\n\r\n if (event.activatorEvent.defaultPrevented) return;\r\n\r\n const { active, over } = event;\r\n if (over && active.id !== over?.id) {\r\n const activeIndex = value.findIndex(\r\n (item) => getItemValue(item) === active.id,\r\n );\r\n const overIndex = value.findIndex(\r\n (item) => getItemValue(item) === over.id,\r\n );\r\n\r\n if (onMove) {\r\n onMove({ ...event, activeIndex, overIndex });\r\n } else {\r\n onValueChange?.(arrayMove(value, activeIndex, overIndex));\r\n }\r\n }\r\n setActiveId(null);\r\n },\r\n [value, onValueChange, onMove, getItemValue, onDragEndProp],\r\n );\r\n\r\n const onDragCancel = React.useCallback(\r\n (event: DragEndEvent) => {\r\n onDragCancelProp?.(event);\r\n\r\n if (event.activatorEvent.defaultPrevented) return;\r\n\r\n setActiveId(null);\r\n },\r\n [onDragCancelProp],\r\n );\r\n\r\n const announcements: Announcements = React.useMemo(\r\n () => ({\r\n onDragStart({ active }) {\r\n const activeValue = active.id.toString();\r\n return `Grabbed sortable item \"${activeValue}\". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;\r\n },\r\n onDragOver({ active, over }) {\r\n if (over) {\r\n const overIndex = over.data.current?.sortable.index ?? 0;\r\n const activeIndex = active.data.current?.sortable.index ?? 0;\r\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\r\n const activeValue = active.id.toString();\r\n return `Sortable item \"${activeValue}\" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\r\n }\r\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\r\n },\r\n onDragEnd({ active, over }) {\r\n const activeValue = active.id.toString();\r\n if (over) {\r\n const overIndex = over.data.current?.sortable.index ?? 0;\r\n return `Sortable item \"${activeValue}\" dropped at position ${overIndex + 1} of ${value.length}.`;\r\n }\r\n return `Sortable item \"${activeValue}\" dropped. No changes were made.`;\r\n },\r\n onDragCancel({ active }) {\r\n const activeIndex = active.data.current?.sortable.index ?? 0;\r\n const activeValue = active.id.toString();\r\n return `Sorting cancelled. Sortable item \"${activeValue}\" returned to position ${activeIndex + 1} of ${value.length}.`;\r\n },\r\n onDragMove({ active, over }) {\r\n if (over) {\r\n const overIndex = over.data.current?.sortable.index ?? 0;\r\n const activeIndex = active.data.current?.sortable.index ?? 0;\r\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\r\n const activeValue = active.id.toString();\r\n return `Sortable item \"${activeValue}\" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\r\n }\r\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\r\n },\r\n }),\r\n [value],\r\n );\r\n\r\n const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(\r\n () => ({\r\n draggable: `\r\n To pick up a sortable item, press space or enter.\r\n While dragging, use the ${orientation === \"vertical\" ? \"up and down\" : orientation === \"horizontal\" ? \"left and right\" : \"arrow\"} keys to move the item.\r\n Press space or enter again to drop the item in its new position, or press escape to cancel.\r\n `,\r\n }),\r\n [orientation],\r\n );\r\n\r\n const contextValue = React.useMemo(\r\n () => ({\r\n id,\r\n items,\r\n modifiers: modifiers ?? config.modifiers,\r\n strategy: strategy ?? config.strategy,\r\n activeId,\r\n setActiveId,\r\n getItemValue,\r\n flatCursor,\r\n }),\r\n [\r\n id,\r\n items,\r\n modifiers,\r\n strategy,\r\n config.modifiers,\r\n config.strategy,\r\n activeId,\r\n getItemValue,\r\n flatCursor,\r\n ],\r\n );\r\n\r\n return (\r\n }\r\n >\r\n \r\n \r\n );\r\n}\r\n\r\nconst SortableContentContext = React.createContext(false);\r\n\r\ninterface SortableContentProps extends React.ComponentProps<\"div\"> {\r\n strategy?: SortableContextProps[\"strategy\"];\r\n children: React.ReactNode;\r\n asChild?: boolean;\r\n withoutSlot?: boolean;\r\n}\r\n\r\nfunction SortableContent(props: SortableContentProps) {\r\n const {\r\n strategy: strategyProp,\r\n asChild,\r\n withoutSlot,\r\n children,\r\n ref,\r\n ...contentProps\r\n } = props;\r\n\r\n const context = useSortableContext(CONTENT_NAME);\r\n\r\n const ContentPrimitive = asChild ? Slot : \"div\";\r\n\r\n return (\r\n \r\n \r\n {withoutSlot ? (\r\n children\r\n ) : (\r\n \r\n {children}\r\n \r\n )}\r\n \r\n \r\n );\r\n}\r\n\r\ninterface SortableItemContextValue {\r\n id: string;\r\n attributes: DraggableAttributes;\r\n listeners: DraggableSyntheticListeners | undefined;\r\n setActivatorNodeRef: (node: HTMLElement | null) => void;\r\n isDragging?: boolean;\r\n disabled?: boolean;\r\n}\r\n\r\nconst SortableItemContext =\r\n React.createContext(null);\r\n\r\nfunction useSortableItemContext(consumerName: string) {\r\n const context = React.useContext(SortableItemContext);\r\n if (!context) {\r\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\r\n }\r\n return context;\r\n}\r\n\r\ninterface SortableItemProps extends React.ComponentProps<\"div\"> {\r\n value: UniqueIdentifier;\r\n asHandle?: boolean;\r\n asChild?: boolean;\r\n disabled?: boolean;\r\n}\r\n\r\nfunction SortableItem(props: SortableItemProps) {\r\n const {\r\n value,\r\n style,\r\n asHandle,\r\n asChild,\r\n disabled,\r\n className,\r\n ref,\r\n ...itemProps\r\n } = props;\r\n\r\n const inSortableContent = React.useContext(SortableContentContext);\r\n const inSortableOverlay = React.useContext(SortableOverlayContext);\r\n\r\n if (!inSortableContent && !inSortableOverlay) {\r\n throw new Error(\r\n `\\`${ITEM_NAME}\\` must be used within \\`${CONTENT_NAME}\\` or \\`${OVERLAY_NAME}\\``,\r\n );\r\n }\r\n\r\n if (value === \"\") {\r\n throw new Error(`\\`${ITEM_NAME}\\` value cannot be an empty string`);\r\n }\r\n\r\n const context = useSortableContext(ITEM_NAME);\r\n const id = React.useId();\r\n const {\r\n attributes,\r\n listeners,\r\n setNodeRef,\r\n setActivatorNodeRef,\r\n transform,\r\n transition,\r\n isDragging,\r\n } = useSortable({ id: value, disabled });\r\n\r\n const composedRef = useComposedRefs(ref, (node) => {\r\n if (disabled) return;\r\n setNodeRef(node);\r\n if (asHandle) setActivatorNodeRef(node);\r\n });\r\n\r\n const composedStyle = React.useMemo(() => {\r\n return {\r\n transform: CSS.Translate.toString(transform),\r\n transition,\r\n ...style,\r\n };\r\n }, [transform, transition, style]);\r\n\r\n const itemContext = React.useMemo(\r\n () => ({\r\n id,\r\n attributes,\r\n listeners,\r\n setActivatorNodeRef,\r\n isDragging,\r\n disabled,\r\n }),\r\n [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],\r\n );\r\n\r\n const ItemPrimitive = asChild ? Slot : \"div\";\r\n\r\n return (\r\n \r\n \r\n \r\n );\r\n}\r\n\r\ninterface SortableItemHandleProps extends React.ComponentProps<\"button\"> {\r\n asChild?: boolean;\r\n}\r\n\r\nfunction SortableItemHandle(props: SortableItemHandleProps) {\r\n const { asChild, disabled, className, ref, ...itemHandleProps } = props;\r\n\r\n const context = useSortableContext(ITEM_HANDLE_NAME);\r\n const itemContext = useSortableItemContext(ITEM_HANDLE_NAME);\r\n\r\n const isDisabled = disabled ?? itemContext.disabled;\r\n\r\n const composedRef = useComposedRefs(ref, (node) => {\r\n if (!isDisabled) return;\r\n itemContext.setActivatorNodeRef(node);\r\n });\r\n\r\n const HandlePrimitive = asChild ? Slot : \"button\";\r\n\r\n return (\r\n \r\n );\r\n}\r\n\r\nconst SortableOverlayContext = React.createContext(false);\r\n\r\nconst dropAnimation: DropAnimation = {\r\n sideEffects: defaultDropAnimationSideEffects({\r\n styles: {\r\n active: {\r\n opacity: \"0.4\",\r\n },\r\n },\r\n }),\r\n};\r\n\r\ninterface SortableOverlayProps\r\n extends Omit, \"children\"> {\r\n container?: Element | DocumentFragment | null;\r\n children?:\r\n | ((params: { value: UniqueIdentifier }) => React.ReactNode)\r\n | React.ReactNode;\r\n}\r\n\r\nfunction SortableOverlay(props: SortableOverlayProps) {\r\n const { container: containerProp, children, ...overlayProps } = props;\r\n\r\n const context = useSortableContext(OVERLAY_NAME);\r\n\r\n const [mounted, setMounted] = React.useState(false);\r\n React.useLayoutEffect(() => setMounted(true), []);\r\n\r\n const container =\r\n containerProp ?? (mounted ? globalThis.document?.body : null);\r\n\r\n if (!container) return null;\r\n\r\n return ReactDOM.createPortal(\r\n \r\n \r\n {context.activeId\r\n ? typeof children === \"function\"\r\n ? children({ value: context.activeId })\r\n : children\r\n : null}\r\n \r\n ,\r\n container,\r\n );\r\n}\r\n\r\nexport {\r\n SortableRoot as Sortable,\r\n SortableContent,\r\n SortableItem,\r\n SortableItemHandle,\r\n SortableOverlay,\r\n //\r\n SortableRoot as Root,\r\n SortableContent as Content,\r\n SortableItem as Item,\r\n SortableItemHandle as ItemHandle,\r\n SortableOverlay as Overlay,\r\n};\r\n", + "content": "\"use client\";\n\nimport {\n type Announcements,\n closestCenter,\n closestCorners,\n DndContext,\n type DndContextProps,\n type DragEndEvent,\n type DraggableAttributes,\n type DraggableSyntheticListeners,\n DragOverlay,\n type DragStartEvent,\n type DropAnimation,\n defaultDropAnimationSideEffects,\n KeyboardSensor,\n MouseSensor,\n type ScreenReaderInstructions,\n TouchSensor,\n type UniqueIdentifier,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport {\n restrictToHorizontalAxis,\n restrictToParentElement,\n restrictToVerticalAxis,\n} from \"@dnd-kit/modifiers\";\nimport {\n arrayMove,\n horizontalListSortingStrategy,\n SortableContext,\n type SortableContextProps,\n sortableKeyboardCoordinates,\n useSortable,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\n\nconst orientationConfig = {\n vertical: {\n modifiers: [restrictToVerticalAxis, restrictToParentElement],\n strategy: verticalListSortingStrategy,\n collisionDetection: closestCenter,\n },\n horizontal: {\n modifiers: [restrictToHorizontalAxis, restrictToParentElement],\n strategy: horizontalListSortingStrategy,\n collisionDetection: closestCenter,\n },\n mixed: {\n modifiers: [restrictToParentElement],\n strategy: undefined,\n collisionDetection: closestCorners,\n },\n};\n\nconst ROOT_NAME = \"Sortable\";\nconst CONTENT_NAME = \"SortableContent\";\nconst ITEM_NAME = \"SortableItem\";\nconst ITEM_HANDLE_NAME = \"SortableItemHandle\";\nconst OVERLAY_NAME = \"SortableOverlay\";\n\ninterface SortableRootContextValue {\n id: string;\n items: UniqueIdentifier[];\n modifiers: DndContextProps[\"modifiers\"];\n strategy: SortableContextProps[\"strategy\"];\n activeId: UniqueIdentifier | null;\n setActiveId: (id: UniqueIdentifier | null) => void;\n getItemValue: (item: T) => UniqueIdentifier;\n flatCursor: boolean;\n}\n\nconst SortableRootContext =\n React.createContext | null>(null);\n\nfunction useSortableContext(consumerName: string) {\n const context = React.useContext(SortableRootContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface GetItemValue {\n /**\n * Callback that returns a unique identifier for each sortable item. Required for array of objects.\n * @example getItemValue={(item) => item.id}\n */\n getItemValue: (item: T) => UniqueIdentifier;\n}\n\ntype SortableRootProps = DndContextProps &\n (T extends object ? GetItemValue : Partial>) & {\n value: T[];\n onValueChange?: (items: T[]) => void;\n onMove?: (\n event: DragEndEvent & { activeIndex: number; overIndex: number },\n ) => void;\n strategy?: SortableContextProps[\"strategy\"];\n orientation?: \"vertical\" | \"horizontal\" | \"mixed\";\n flatCursor?: boolean;\n };\n\nfunction SortableRoot(props: SortableRootProps) {\n const {\n value,\n onValueChange,\n collisionDetection,\n modifiers,\n strategy,\n onMove,\n orientation = \"vertical\",\n flatCursor = false,\n getItemValue: getItemValueProp,\n accessibility,\n onDragStart: onDragStartProp,\n onDragEnd: onDragEndProp,\n onDragCancel: onDragCancelProp,\n ...sortableProps\n } = props;\n\n const id = React.useId();\n const [activeId, setActiveId] = React.useState(null);\n\n const sensors = useSensors(\n useSensor(MouseSensor),\n useSensor(TouchSensor),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n }),\n );\n const config = React.useMemo(\n () => orientationConfig[orientation],\n [orientation],\n );\n\n const getItemValue = React.useCallback(\n (item: T): UniqueIdentifier => {\n if (typeof item === \"object\" && !getItemValueProp) {\n throw new Error(\"getItemValue is required when using array of objects\");\n }\n return getItemValueProp\n ? getItemValueProp(item)\n : (item as UniqueIdentifier);\n },\n [getItemValueProp],\n );\n\n const items = React.useMemo(() => {\n return value.map((item) => getItemValue(item));\n }, [value, getItemValue]);\n\n const onDragStart = React.useCallback(\n (event: DragStartEvent) => {\n onDragStartProp?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(event.active.id);\n },\n [onDragStartProp],\n );\n\n const onDragEnd = React.useCallback(\n (event: DragEndEvent) => {\n onDragEndProp?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n const { active, over } = event;\n if (over && active.id !== over?.id) {\n const activeIndex = value.findIndex(\n (item) => getItemValue(item) === active.id,\n );\n const overIndex = value.findIndex(\n (item) => getItemValue(item) === over.id,\n );\n\n if (onMove) {\n onMove({ ...event, activeIndex, overIndex });\n } else {\n onValueChange?.(arrayMove(value, activeIndex, overIndex));\n }\n }\n setActiveId(null);\n },\n [value, onValueChange, onMove, getItemValue, onDragEndProp],\n );\n\n const onDragCancel = React.useCallback(\n (event: DragEndEvent) => {\n onDragCancelProp?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(null);\n },\n [onDragCancelProp],\n );\n\n const announcements: Announcements = React.useMemo(\n () => ({\n onDragStart({ active }) {\n const activeValue = active.id.toString();\n return `Grabbed sortable item \"${activeValue}\". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;\n },\n onDragOver({ active, over }) {\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n const activeValue = active.id.toString();\n return `Sortable item \"${activeValue}\" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n }\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n },\n onDragEnd({ active, over }) {\n const activeValue = active.id.toString();\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n return `Sortable item \"${activeValue}\" dropped at position ${overIndex + 1} of ${value.length}.`;\n }\n return `Sortable item \"${activeValue}\" dropped. No changes were made.`;\n },\n onDragCancel({ active }) {\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const activeValue = active.id.toString();\n return `Sorting cancelled. Sortable item \"${activeValue}\" returned to position ${activeIndex + 1} of ${value.length}.`;\n },\n onDragMove({ active, over }) {\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n const activeValue = active.id.toString();\n return `Sortable item \"${activeValue}\" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n }\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n },\n }),\n [value],\n );\n\n const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(\n () => ({\n draggable: `\n To pick up a sortable item, press space or enter.\n While dragging, use the ${orientation === \"vertical\" ? \"up and down\" : orientation === \"horizontal\" ? \"left and right\" : \"arrow\"} keys to move the item.\n Press space or enter again to drop the item in its new position, or press escape to cancel.\n `,\n }),\n [orientation],\n );\n\n const contextValue = React.useMemo(\n () => ({\n id,\n items,\n modifiers: modifiers ?? config.modifiers,\n strategy: strategy ?? config.strategy,\n activeId,\n setActiveId,\n getItemValue,\n flatCursor,\n }),\n [\n id,\n items,\n modifiers,\n strategy,\n config.modifiers,\n config.strategy,\n activeId,\n getItemValue,\n flatCursor,\n ],\n );\n\n return (\n }\n >\n \n \n );\n}\n\nconst SortableContentContext = React.createContext(false);\n\ninterface SortableContentProps extends React.ComponentProps<\"div\"> {\n strategy?: SortableContextProps[\"strategy\"];\n children: React.ReactNode;\n asChild?: boolean;\n withoutSlot?: boolean;\n}\n\nfunction SortableContent(props: SortableContentProps) {\n const {\n strategy: strategyProp,\n asChild,\n withoutSlot,\n children,\n ref,\n ...contentProps\n } = props;\n\n const context = useSortableContext(CONTENT_NAME);\n\n const ContentPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n {withoutSlot ? (\n children\n ) : (\n \n {children}\n \n )}\n \n \n );\n}\n\ninterface SortableItemContextValue {\n id: string;\n attributes: DraggableAttributes;\n listeners: DraggableSyntheticListeners | undefined;\n setActivatorNodeRef: (node: HTMLElement | null) => void;\n isDragging?: boolean;\n disabled?: boolean;\n}\n\nconst SortableItemContext =\n React.createContext(null);\n\nfunction useSortableItemContext(consumerName: string) {\n const context = React.useContext(SortableItemContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\n }\n return context;\n}\n\ninterface SortableItemProps extends React.ComponentProps<\"div\"> {\n value: UniqueIdentifier;\n asHandle?: boolean;\n asChild?: boolean;\n disabled?: boolean;\n}\n\nfunction SortableItem(props: SortableItemProps) {\n const {\n value,\n style,\n asHandle,\n asChild,\n disabled,\n className,\n ref,\n ...itemProps\n } = props;\n\n const inSortableContent = React.useContext(SortableContentContext);\n const inSortableOverlay = React.useContext(SortableOverlayContext);\n\n if (!inSortableContent && !inSortableOverlay) {\n throw new Error(\n `\\`${ITEM_NAME}\\` must be used within \\`${CONTENT_NAME}\\` or \\`${OVERLAY_NAME}\\``,\n );\n }\n\n if (value === \"\") {\n throw new Error(`\\`${ITEM_NAME}\\` value cannot be an empty string`);\n }\n\n const context = useSortableContext(ITEM_NAME);\n const id = React.useId();\n const {\n attributes,\n listeners,\n setNodeRef,\n setActivatorNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: value, disabled });\n\n const composedRef = useComposedRefs(ref, (node) => {\n if (disabled) return;\n setNodeRef(node);\n if (asHandle) setActivatorNodeRef(node);\n });\n\n const composedStyle = React.useMemo(() => {\n return {\n transform: CSS.Translate.toString(transform),\n transition,\n ...style,\n };\n }, [transform, transition, style]);\n\n const itemContext = React.useMemo(\n () => ({\n id,\n attributes,\n listeners,\n setActivatorNodeRef,\n isDragging,\n disabled,\n }),\n [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],\n );\n\n const ItemPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n \n );\n}\n\ninterface SortableItemHandleProps extends React.ComponentProps<\"button\"> {\n asChild?: boolean;\n}\n\nfunction SortableItemHandle(props: SortableItemHandleProps) {\n const { asChild, disabled, className, ref, ...itemHandleProps } = props;\n\n const context = useSortableContext(ITEM_HANDLE_NAME);\n const itemContext = useSortableItemContext(ITEM_HANDLE_NAME);\n\n const isDisabled = disabled ?? itemContext.disabled;\n\n const composedRef = useComposedRefs(ref, (node) => {\n if (!isDisabled) return;\n itemContext.setActivatorNodeRef(node);\n });\n\n const HandlePrimitive = asChild ? Slot : \"button\";\n\n return (\n \n );\n}\n\nconst SortableOverlayContext = React.createContext(false);\n\nconst dropAnimation: DropAnimation = {\n sideEffects: defaultDropAnimationSideEffects({\n styles: {\n active: {\n opacity: \"0.4\",\n },\n },\n }),\n};\n\ninterface SortableOverlayProps\n extends Omit, \"children\"> {\n container?: Element | DocumentFragment | null;\n children?:\n | ((params: { value: UniqueIdentifier }) => React.ReactNode)\n | React.ReactNode;\n}\n\nfunction SortableOverlay(props: SortableOverlayProps) {\n const { container: containerProp, children, ...overlayProps } = props;\n\n const context = useSortableContext(OVERLAY_NAME);\n\n const [mounted, setMounted] = React.useState(false);\n React.useLayoutEffect(() => setMounted(true), []);\n\n const container =\n containerProp ?? (mounted ? globalThis.document?.body : null);\n\n if (!container) return null;\n\n return ReactDOM.createPortal(\n \n \n {context.activeId\n ? typeof children === \"function\"\n ? children({ value: context.activeId })\n : children\n : null}\n \n ,\n container,\n );\n}\n\nexport {\n SortableRoot as Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n //\n SortableRoot as Root,\n SortableContent as Content,\n SortableItem as Item,\n SortableItemHandle as ItemHandle,\n SortableOverlay as Overlay,\n};\n", "type": "registry:ui", "target": "src/components/ui/sortable.tsx" }, diff --git a/public/r/data-grid-view-menu.json b/public/r/data-grid-view-menu.json index be0d2abad..478890b53 100644 --- a/public/r/data-grid-view-menu.json +++ b/public/r/data-grid-view-menu.json @@ -16,7 +16,7 @@ "files": [ { "path": "src/components/data-grid/data-grid-view-menu.tsx", - "content": "\"use client\";\r\n\r\nimport type { Table } from \"@tanstack/react-table\";\r\nimport { Check, Settings2 } from \"lucide-react\";\r\nimport * as React from \"react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n} from \"@/components/ui/command\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface DataGridViewMenuProps\r\n extends React.ComponentProps {\r\n table: Table;\r\n}\r\n\r\nexport function DataGridViewMenu({\r\n table,\r\n ...props\r\n}: DataGridViewMenuProps) {\r\n const columns = React.useMemo(\r\n () =>\r\n table\r\n .getAllColumns()\r\n .filter(\r\n (column) =>\r\n typeof column.accessorFn !== \"undefined\" && column.getCanHide(),\r\n ),\r\n [table],\r\n );\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n No columns found.\r\n \r\n {columns.map((column) => (\r\n \r\n column.toggleVisibility(!column.getIsVisible())\r\n }\r\n >\r\n \r\n {column.columnDef.meta?.label ?? column.id}\r\n \r\n \r\n \r\n ))}\r\n \r\n \r\n \r\n \r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { Check, Settings2 } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataGridViewMenuProps\n extends React.ComponentProps {\n table: Table;\n}\n\nexport function DataGridViewMenu({\n table,\n ...props\n}: DataGridViewMenuProps) {\n const columns = React.useMemo(\n () =>\n table\n .getAllColumns()\n .filter(\n (column) =>\n typeof column.accessorFn !== \"undefined\" && column.getCanHide(),\n ),\n [table],\n );\n\n return (\n \n \n \n \n \n \n \n No columns found.\n \n {columns.map((column) => (\n \n column.toggleVisibility(!column.getIsVisible())\n }\n >\n \n {column.columnDef.meta?.label ?? column.id}\n \n \n \n ))}\n \n \n \n \n \n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-view-menu.tsx" } diff --git a/public/r/data-grid.json b/public/r/data-grid.json index ac4cffdc0..246a98b1d 100644 --- a/public/r/data-grid.json +++ b/public/r/data-grid.json @@ -29,55 +29,55 @@ "files": [ { "path": "src/components/data-grid/data-grid.tsx", - "content": "\"use client\";\r\n\r\nimport { flexRender } from \"@tanstack/react-table\";\r\nimport { Plus } from \"lucide-react\";\r\nimport * as React from \"react\";\r\nimport { DataGridColumnHeader } from \"@/components/data-grid/data-grid-column-header\";\r\nimport { DataGridContextMenu } from \"@/components/data-grid/data-grid-context-menu\";\r\nimport { DataGridKeyboardShortcuts } from \"@/components/data-grid/data-grid-keyboard-shortcuts\";\r\nimport { DataGridRow } from \"@/components/data-grid/data-grid-row\";\r\nimport { DataGridSearch } from \"@/components/data-grid/data-grid-search\";\r\nimport type { useDataGrid } from \"@/hooks/use-data-grid\";\r\nimport { getCommonPinningStyles } from \"@/lib/data-table\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport type { CellPosition } from \"@/types/data-grid\";\r\n\r\ninterface DataGridProps\r\n extends ReturnType>,\r\n React.ComponentProps<\"div\"> {\r\n height?: number;\r\n onRowAdd?: (event?: React.MouseEvent) =>\r\n | Partial\r\n | Promise>\r\n | null\r\n // biome-ignore lint/suspicious/noConfusingVoidType: void is needed here to allow functions without explicit return\r\n | void;\r\n}\r\n\r\nexport function DataGrid({\r\n dataGridRef,\r\n headerRef,\r\n rowMapRef,\r\n footerRef,\r\n table,\r\n rowVirtualizer,\r\n height = 600,\r\n searchState,\r\n columnSizeVars,\r\n scrollToRow,\r\n onRowAdd: onRowAddProp,\r\n className,\r\n ...props\r\n}: DataGridProps) {\r\n const rows = table.getRowModel().rows;\r\n const columns = table.getAllColumns();\r\n\r\n const meta = table.options.meta;\r\n const rowHeight = meta?.rowHeight ?? \"short\";\r\n const focusedCell = meta?.focusedCell ?? null;\r\n\r\n const onGridContextMenu = React.useCallback(\r\n (event: React.MouseEvent) => {\r\n event.preventDefault();\r\n },\r\n [],\r\n );\r\n\r\n const onRowAdd = React.useCallback(\r\n async (event?: React.MouseEvent) => {\r\n if (!onRowAddProp) return;\r\n\r\n const result = await onRowAddProp();\r\n\r\n if (event?.defaultPrevented || result === null) return;\r\n\r\n if (result) {\r\n const adjustedRowIndex =\r\n (result.rowIndex ?? 0) >= rows.length ? rows.length : result.rowIndex;\r\n\r\n scrollToRow({\r\n rowIndex: adjustedRowIndex,\r\n columnId: result.columnId,\r\n });\r\n return;\r\n }\r\n\r\n scrollToRow({ rowIndex: rows.length });\r\n },\r\n [onRowAddProp, scrollToRow, rows.length],\r\n );\r\n\r\n const onAddRowKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (!onRowAddProp) return;\r\n\r\n if (event.key === \"Enter\" || event.key === \" \") {\r\n event.preventDefault();\r\n onRowAdd();\r\n }\r\n },\r\n [onRowAddProp, onRowAdd],\r\n );\r\n\r\n return (\r\n \r\n {searchState && }\r\n \r\n \r\n \r\n \r\n {table.getHeaderGroups().map((headerGroup, rowIndex) => (\r\n \r\n {headerGroup.headers.map((header, colIndex) => {\r\n const sorting = table.getState().sorting;\r\n const currentSort = sorting.find(\r\n (sort) => sort.id === header.column.id,\r\n );\r\n const isSortable = header.column.getCanSort();\r\n\r\n return (\r\n \r\n {header.isPlaceholder ? null : typeof header.column\r\n .columnDef.header === \"function\" ? (\r\n
\r\n {flexRender(\r\n header.column.columnDef.header,\r\n header.getContext(),\r\n )}\r\n
\r\n ) : (\r\n \r\n )}\r\n
\r\n );\r\n })}\r\n
\r\n ))}\r\n
\r\n \r\n {rowVirtualizer.getVirtualIndexes().map((virtualRowIndex) => {\r\n const row = rows[virtualRowIndex];\r\n if (!row) return null;\r\n\r\n return (\r\n \r\n );\r\n })}\r\n
\r\n {onRowAdd && (\r\n \r\n \r\n \r\n
\r\n \r\n Add row\r\n
\r\n
\r\n
\r\n
\r\n )}\r\n
\r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport { flexRender } from \"@tanstack/react-table\";\nimport { Plus } from \"lucide-react\";\nimport * as React from \"react\";\nimport { DataGridColumnHeader } from \"@/components/data-grid/data-grid-column-header\";\nimport { DataGridContextMenu } from \"@/components/data-grid/data-grid-context-menu\";\nimport { DataGridKeyboardShortcuts } from \"@/components/data-grid/data-grid-keyboard-shortcuts\";\nimport { DataGridRow } from \"@/components/data-grid/data-grid-row\";\nimport { DataGridSearch } from \"@/components/data-grid/data-grid-search\";\nimport type { useDataGrid } from \"@/hooks/use-data-grid\";\nimport { getCommonPinningStyles } from \"@/lib/data-table\";\nimport { cn } from \"@/lib/utils\";\nimport type { CellPosition } from \"@/types/data-grid\";\n\ninterface DataGridProps\n extends ReturnType>,\n React.ComponentProps<\"div\"> {\n height?: number;\n onRowAdd?: (event?: React.MouseEvent) =>\n | Partial\n | Promise>\n | null\n // biome-ignore lint/suspicious/noConfusingVoidType: void is needed here to allow functions without explicit return\n | void;\n}\n\nexport function DataGrid({\n dataGridRef,\n headerRef,\n rowMapRef,\n footerRef,\n table,\n rowVirtualizer,\n height = 600,\n searchState,\n columnSizeVars,\n scrollToRow,\n onRowAdd: onRowAddProp,\n className,\n ...props\n}: DataGridProps) {\n const rows = table.getRowModel().rows;\n const columns = table.getAllColumns();\n\n const meta = table.options.meta;\n const rowHeight = meta?.rowHeight ?? \"short\";\n const focusedCell = meta?.focusedCell ?? null;\n\n const onGridContextMenu = React.useCallback(\n (event: React.MouseEvent) => {\n event.preventDefault();\n },\n [],\n );\n\n const onRowAdd = React.useCallback(\n async (event?: React.MouseEvent) => {\n if (!onRowAddProp) return;\n\n const result = await onRowAddProp();\n\n if (event?.defaultPrevented || result === null) return;\n\n if (result) {\n const adjustedRowIndex =\n (result.rowIndex ?? 0) >= rows.length ? rows.length : result.rowIndex;\n\n scrollToRow({\n rowIndex: adjustedRowIndex,\n columnId: result.columnId,\n });\n return;\n }\n\n scrollToRow({ rowIndex: rows.length });\n },\n [onRowAddProp, scrollToRow, rows.length],\n );\n\n const onAddRowKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (!onRowAddProp) return;\n\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onRowAdd();\n }\n },\n [onRowAddProp, onRowAdd],\n );\n\n return (\n \n {searchState && }\n \n \n \n \n {table.getHeaderGroups().map((headerGroup, rowIndex) => (\n \n {headerGroup.headers.map((header, colIndex) => {\n const sorting = table.getState().sorting;\n const currentSort = sorting.find(\n (sort) => sort.id === header.column.id,\n );\n const isSortable = header.column.getCanSort();\n\n return (\n \n {header.isPlaceholder ? null : typeof header.column\n .columnDef.header === \"function\" ? (\n
\n {flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n
\n ) : (\n \n )}\n \n );\n })}\n \n ))}\n \n \n {rowVirtualizer.getVirtualIndexes().map((virtualRowIndex) => {\n const row = rows[virtualRowIndex];\n if (!row) return null;\n\n return (\n \n );\n })}\n \n {onRowAdd && (\n \n \n \n
\n \n Add row\n
\n \n \n \n )}\n \n \n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid.tsx" }, { "path": "src/components/data-grid/data-grid-cell.tsx", - "content": "\"use client\";\r\n\r\nimport type { Cell, Table } from \"@tanstack/react-table\";\r\nimport * as React from \"react\";\r\n\r\nimport {\r\n CheckboxCell,\r\n DateCell,\r\n LongTextCell,\r\n MultiSelectCell,\r\n NumberCell,\r\n SelectCell,\r\n ShortTextCell,\r\n} from \"@/components/data-grid/data-grid-cell-variants\";\r\n\r\ninterface DataGridCellProps {\r\n cell: Cell;\r\n table: Table;\r\n}\r\n\r\nexport function DataGridCell({ cell, table }: DataGridCellProps) {\r\n const meta = table.options.meta;\r\n const originalRowIndex = cell.row.index;\r\n\r\n const rows = table.getRowModel().rows;\r\n const displayRowIndex = rows.findIndex(\r\n (row) => row.original === cell.row.original,\r\n );\r\n const rowIndex = displayRowIndex >= 0 ? displayRowIndex : originalRowIndex;\r\n const columnId = cell.column.id;\r\n\r\n const isFocused =\r\n meta?.focusedCell?.rowIndex === rowIndex &&\r\n meta?.focusedCell?.columnId === columnId;\r\n const isEditing =\r\n meta?.editingCell?.rowIndex === rowIndex &&\r\n meta?.editingCell?.columnId === columnId;\r\n const isSelected = meta?.getIsCellSelected?.(rowIndex, columnId) ?? false;\r\n\r\n const cellOpts = cell.column.columnDef.meta?.cell;\r\n const variant = cellOpts?.variant ?? \"text\";\r\n\r\n switch (variant) {\r\n case \"short-text\":\r\n return (\r\n \r\n );\r\n case \"long-text\":\r\n return (\r\n \r\n );\r\n case \"number\":\r\n return (\r\n \r\n );\r\n case \"select\":\r\n return (\r\n \r\n );\r\n case \"multi-select\":\r\n return (\r\n \r\n );\r\n case \"checkbox\":\r\n return (\r\n \r\n );\r\n case \"date\":\r\n return (\r\n \r\n );\r\n\r\n default:\r\n return (\r\n \r\n );\r\n }\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Cell, Table } from \"@tanstack/react-table\";\nimport * as React from \"react\";\n\nimport {\n CheckboxCell,\n DateCell,\n LongTextCell,\n MultiSelectCell,\n NumberCell,\n SelectCell,\n ShortTextCell,\n} from \"@/components/data-grid/data-grid-cell-variants\";\n\ninterface DataGridCellProps {\n cell: Cell;\n table: Table;\n}\n\nexport function DataGridCell({ cell, table }: DataGridCellProps) {\n const meta = table.options.meta;\n const originalRowIndex = cell.row.index;\n\n const rows = table.getRowModel().rows;\n const displayRowIndex = rows.findIndex(\n (row) => row.original === cell.row.original,\n );\n const rowIndex = displayRowIndex >= 0 ? displayRowIndex : originalRowIndex;\n const columnId = cell.column.id;\n\n const isFocused =\n meta?.focusedCell?.rowIndex === rowIndex &&\n meta?.focusedCell?.columnId === columnId;\n const isEditing =\n meta?.editingCell?.rowIndex === rowIndex &&\n meta?.editingCell?.columnId === columnId;\n const isSelected = meta?.getIsCellSelected?.(rowIndex, columnId) ?? false;\n\n const cellOpts = cell.column.columnDef.meta?.cell;\n const variant = cellOpts?.variant ?? \"text\";\n\n switch (variant) {\n case \"short-text\":\n return (\n \n );\n case \"long-text\":\n return (\n \n );\n case \"number\":\n return (\n \n );\n case \"select\":\n return (\n \n );\n case \"multi-select\":\n return (\n \n );\n case \"checkbox\":\n return (\n \n );\n case \"date\":\n return (\n \n );\n\n default:\n return (\n \n );\n }\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-cell.tsx" }, { "path": "src/components/data-grid/data-grid-cell-wrapper.tsx", - "content": "\"use client\";\r\n\r\nimport type { Cell, Table } from \"@tanstack/react-table\";\r\nimport * as React from \"react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface DataGridCellWrapperProps extends React.ComponentProps<\"div\"> {\r\n cell: Cell;\r\n table: Table;\r\n rowIndex: number;\r\n columnId: string;\r\n isEditing: boolean;\r\n isFocused: boolean;\r\n isSelected: boolean;\r\n}\r\n\r\nexport function DataGridCellWrapper({\r\n table,\r\n rowIndex,\r\n columnId,\r\n isEditing,\r\n isFocused,\r\n isSelected,\r\n className,\r\n onClick: onClickProp,\r\n onKeyDown: onKeyDownProp,\r\n ...props\r\n}: DataGridCellWrapperProps) {\r\n const meta = table.options.meta;\r\n\r\n const isSearchMatch = meta?.getIsSearchMatch?.(rowIndex, columnId) ?? false;\r\n const isActiveSearchMatch =\r\n meta?.getIsActiveSearchMatch?.(rowIndex, columnId) ?? false;\r\n\r\n const onClick = React.useCallback(\r\n (event: React.MouseEvent) => {\r\n if (!isEditing) {\r\n event.preventDefault();\r\n onClickProp?.(event);\r\n if (isFocused) {\r\n meta?.onCellEditingStart?.(rowIndex, columnId);\r\n } else {\r\n meta?.onCellClick?.(rowIndex, columnId, event);\r\n }\r\n }\r\n },\r\n [meta, rowIndex, columnId, isEditing, isFocused, onClickProp],\r\n );\r\n\r\n const onContextMenu = React.useCallback(\r\n (event: React.MouseEvent) => {\r\n if (!isEditing) {\r\n meta?.onCellContextMenu?.(rowIndex, columnId, event);\r\n }\r\n },\r\n [meta, rowIndex, columnId, isEditing],\r\n );\r\n\r\n const onMouseDown = React.useCallback(\r\n (event: React.MouseEvent) => {\r\n if (!isEditing) {\r\n meta?.onCellMouseDown?.(rowIndex, columnId, event);\r\n }\r\n },\r\n [meta, rowIndex, columnId, isEditing],\r\n );\r\n\r\n const onMouseEnter = React.useCallback(\r\n (event: React.MouseEvent) => {\r\n if (!isEditing) {\r\n meta?.onCellMouseEnter?.(rowIndex, columnId, event);\r\n }\r\n },\r\n [meta, rowIndex, columnId, isEditing],\r\n );\r\n\r\n const onMouseUp = React.useCallback(() => {\r\n if (!isEditing) {\r\n meta?.onCellMouseUp?.();\r\n }\r\n }, [meta, isEditing]);\r\n\r\n const onDoubleClick = React.useCallback(\r\n (event: React.MouseEvent) => {\r\n if (!isEditing) {\r\n event.preventDefault();\r\n meta?.onCellDoubleClick?.(rowIndex, columnId);\r\n }\r\n },\r\n [meta, rowIndex, columnId, isEditing],\r\n );\r\n\r\n const onKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n onKeyDownProp?.(event);\r\n\r\n if (event.defaultPrevented) return;\r\n\r\n if (\r\n event.key === \"ArrowUp\" ||\r\n event.key === \"ArrowDown\" ||\r\n event.key === \"ArrowLeft\" ||\r\n event.key === \"ArrowRight\" ||\r\n event.key === \"Home\" ||\r\n event.key === \"End\" ||\r\n event.key === \"PageUp\" ||\r\n event.key === \"PageDown\" ||\r\n event.key === \"Tab\"\r\n ) {\r\n return;\r\n }\r\n\r\n if (isFocused && !isEditing) {\r\n if (event.key === \"F2\" || event.key === \"Enter\") {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n meta?.onCellEditingStart?.(rowIndex, columnId);\r\n return;\r\n }\r\n\r\n if (event.key === \" \") {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n meta?.onCellEditingStart?.(rowIndex, columnId);\r\n return;\r\n }\r\n\r\n if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n meta?.onCellEditingStart?.(rowIndex, columnId);\r\n }\r\n }\r\n },\r\n [onKeyDownProp, isFocused, isEditing, meta, rowIndex, columnId],\r\n );\r\n\r\n const rowHeight = meta?.rowHeight ?? \"short\";\r\n\r\n return (\r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Cell, Table } from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataGridCellWrapperProps extends React.ComponentProps<\"div\"> {\n cell: Cell;\n table: Table;\n rowIndex: number;\n columnId: string;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n}\n\nexport function DataGridCellWrapper({\n table,\n rowIndex,\n columnId,\n isEditing,\n isFocused,\n isSelected,\n className,\n onClick: onClickProp,\n onKeyDown: onKeyDownProp,\n ...props\n}: DataGridCellWrapperProps) {\n const meta = table.options.meta;\n\n const isSearchMatch = meta?.getIsSearchMatch?.(rowIndex, columnId) ?? false;\n const isActiveSearchMatch =\n meta?.getIsActiveSearchMatch?.(rowIndex, columnId) ?? false;\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n event.preventDefault();\n onClickProp?.(event);\n if (isFocused) {\n meta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n meta?.onCellClick?.(rowIndex, columnId, event);\n }\n }\n },\n [meta, rowIndex, columnId, isEditing, isFocused, onClickProp],\n );\n\n const onContextMenu = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n meta?.onCellContextMenu?.(rowIndex, columnId, event);\n }\n },\n [meta, rowIndex, columnId, isEditing],\n );\n\n const onMouseDown = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n meta?.onCellMouseDown?.(rowIndex, columnId, event);\n }\n },\n [meta, rowIndex, columnId, isEditing],\n );\n\n const onMouseEnter = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n meta?.onCellMouseEnter?.(rowIndex, columnId, event);\n }\n },\n [meta, rowIndex, columnId, isEditing],\n );\n\n const onMouseUp = React.useCallback(() => {\n if (!isEditing) {\n meta?.onCellMouseUp?.();\n }\n }, [meta, isEditing]);\n\n const onDoubleClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n event.preventDefault();\n meta?.onCellDoubleClick?.(rowIndex, columnId);\n }\n },\n [meta, rowIndex, columnId, isEditing],\n );\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n onKeyDownProp?.(event);\n\n if (event.defaultPrevented) return;\n\n if (\n event.key === \"ArrowUp\" ||\n event.key === \"ArrowDown\" ||\n event.key === \"ArrowLeft\" ||\n event.key === \"ArrowRight\" ||\n event.key === \"Home\" ||\n event.key === \"End\" ||\n event.key === \"PageUp\" ||\n event.key === \"PageDown\" ||\n event.key === \"Tab\"\n ) {\n return;\n }\n\n if (isFocused && !isEditing) {\n if (event.key === \"F2\" || event.key === \"Enter\") {\n event.preventDefault();\n event.stopPropagation();\n meta?.onCellEditingStart?.(rowIndex, columnId);\n return;\n }\n\n if (event.key === \" \") {\n event.preventDefault();\n event.stopPropagation();\n meta?.onCellEditingStart?.(rowIndex, columnId);\n return;\n }\n\n if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {\n event.preventDefault();\n event.stopPropagation();\n meta?.onCellEditingStart?.(rowIndex, columnId);\n }\n }\n },\n [onKeyDownProp, isFocused, isEditing, meta, rowIndex, columnId],\n );\n\n const rowHeight = meta?.rowHeight ?? \"short\";\n\n return (\n \n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-cell-wrapper.tsx" }, { "path": "src/components/data-grid/data-grid-cell-variants.tsx", - "content": "\"use client\";\r\n\r\nimport type { Cell, Table } from \"@tanstack/react-table\";\r\nimport { Check, X } from \"lucide-react\";\r\nimport * as React from \"react\";\r\nimport { DataGridCellWrapper } from \"@/components/data-grid/data-grid-cell-wrapper\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Calendar } from \"@/components/ui/calendar\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n CommandSeparator,\r\n} from \"@/components/ui/command\";\r\nimport {\r\n Popover,\r\n PopoverAnchor,\r\n PopoverContent,\r\n} from \"@/components/ui/popover\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\r\nimport { getLineCount } from \"@/lib/data-grid\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface CellVariantProps {\r\n cell: Cell;\r\n table: Table;\r\n rowIndex: number;\r\n columnId: string;\r\n isEditing: boolean;\r\n isFocused: boolean;\r\n isSelected: boolean;\r\n}\r\n\r\nexport function ShortTextCell({\r\n cell,\r\n table,\r\n rowIndex,\r\n columnId,\r\n isEditing,\r\n isFocused,\r\n isSelected,\r\n}: CellVariantProps) {\r\n const initialValue = cell.getValue() as string;\r\n const [value, setValue] = React.useState(initialValue);\r\n const cellRef = React.useRef(null);\r\n const containerRef = React.useRef(null);\r\n const meta = table.options.meta;\r\n\r\n const onBlur = React.useCallback(() => {\r\n // Read the current value directly from the DOM to avoid stale state\r\n const currentValue = cellRef.current?.textContent ?? \"\";\r\n if (currentValue !== initialValue) {\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: currentValue });\r\n }\r\n meta?.onCellEditingStop?.();\r\n }, [meta, rowIndex, columnId, initialValue]);\r\n\r\n const onInput = React.useCallback(\r\n (event: React.FormEvent) => {\r\n const currentValue = event.currentTarget.textContent ?? \"\";\r\n setValue(currentValue);\r\n },\r\n [],\r\n );\r\n\r\n const onWrapperKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (isEditing) {\r\n if (event.key === \"Enter\") {\r\n event.preventDefault();\r\n const currentValue = cellRef.current?.textContent ?? \"\";\r\n if (currentValue !== initialValue) {\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: currentValue });\r\n }\r\n meta?.onCellEditingStop?.({ moveToNextRow: true });\r\n } else if (event.key === \"Tab\") {\r\n event.preventDefault();\r\n const currentValue = cellRef.current?.textContent ?? \"\";\r\n if (currentValue !== initialValue) {\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: currentValue });\r\n }\r\n meta?.onCellEditingStop?.({\r\n direction: event.shiftKey ? \"left\" : \"right\",\r\n });\r\n } else if (event.key === \"Escape\") {\r\n event.preventDefault();\r\n setValue(initialValue);\r\n cellRef.current?.blur();\r\n }\r\n } else if (\r\n isFocused &&\r\n event.key.length === 1 &&\r\n !event.ctrlKey &&\r\n !event.metaKey\r\n ) {\r\n // Handle typing to pre-fill the value when editing starts\r\n setValue(event.key);\r\n\r\n queueMicrotask(() => {\r\n if (cellRef.current && cellRef.current.contentEditable === \"true\") {\r\n cellRef.current.textContent = event.key;\r\n const range = document.createRange();\r\n const selection = window.getSelection();\r\n range.selectNodeContents(cellRef.current);\r\n range.collapse(false);\r\n selection?.removeAllRanges();\r\n selection?.addRange(range);\r\n }\r\n });\r\n }\r\n },\r\n [isEditing, isFocused, initialValue, meta, rowIndex, columnId],\r\n );\r\n\r\n React.useEffect(() => {\r\n setValue(initialValue);\r\n if (cellRef.current && !isEditing) {\r\n cellRef.current.textContent = initialValue;\r\n }\r\n }, [initialValue, isEditing]);\r\n\r\n React.useEffect(() => {\r\n if (isEditing && cellRef.current) {\r\n cellRef.current.focus();\r\n\r\n if (!cellRef.current.textContent && value) {\r\n cellRef.current.textContent = value;\r\n }\r\n\r\n if (cellRef.current.textContent) {\r\n const range = document.createRange();\r\n const selection = window.getSelection();\r\n range.selectNodeContents(cellRef.current);\r\n range.collapse(false);\r\n selection?.removeAllRanges();\r\n selection?.addRange(range);\r\n }\r\n }\r\n // Don't focus if we're in the middle of a scroll operation\r\n if (\r\n isFocused &&\r\n !isEditing &&\r\n !meta?.searchOpen &&\r\n !meta?.isScrolling &&\r\n containerRef.current\r\n ) {\r\n containerRef.current.focus();\r\n }\r\n }, [isFocused, isEditing, value, meta?.searchOpen, meta?.isScrolling]);\r\n\r\n const displayValue = !isEditing ? (value ?? \"\") : \"\";\r\n\r\n return (\r\n \r\n \r\n {displayValue}\r\n \r\n \r\n );\r\n}\r\n\r\nexport function LongTextCell({\r\n cell,\r\n table,\r\n rowIndex,\r\n columnId,\r\n isFocused,\r\n isEditing,\r\n isSelected,\r\n}: CellVariantProps) {\r\n const initialValue = cell.getValue() as string;\r\n const [value, setValue] = React.useState(initialValue ?? \"\");\r\n const [open, setOpen] = React.useState(false);\r\n const textareaRef = React.useRef(null);\r\n const containerRef = React.useRef(null);\r\n const meta = table.options.meta;\r\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\r\n\r\n const prevInitialValueRef = React.useRef(initialValue);\r\n if (initialValue !== prevInitialValueRef.current) {\r\n prevInitialValueRef.current = initialValue;\r\n setValue(initialValue ?? \"\");\r\n }\r\n\r\n // Debounced auto-save (300ms delay)\r\n const debouncedSave = useDebouncedCallback((newValue: string) => {\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\r\n }, 300);\r\n\r\n const onSave = React.useCallback(() => {\r\n // Immediately save any pending changes and close the popover\r\n if (value !== initialValue) {\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value });\r\n }\r\n setOpen(false);\r\n meta?.onCellEditingStop?.();\r\n }, [meta, value, initialValue, rowIndex, columnId]);\r\n\r\n const onCancel = React.useCallback(() => {\r\n // Restore the original value\r\n setValue(initialValue ?? \"\");\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: initialValue });\r\n setOpen(false);\r\n meta?.onCellEditingStop?.();\r\n }, [meta, initialValue, rowIndex, columnId]);\r\n\r\n const onChange = React.useCallback(\r\n (event: React.ChangeEvent) => {\r\n const newValue = event.target.value;\r\n setValue(newValue);\r\n // Debounced auto-save\r\n debouncedSave(newValue);\r\n },\r\n [debouncedSave],\r\n );\r\n\r\n const onOpenChange = React.useCallback(\r\n (isOpen: boolean) => {\r\n setOpen(isOpen);\r\n if (!isOpen) {\r\n // Immediately save any pending changes when closing\r\n if (value !== initialValue) {\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value });\r\n }\r\n meta?.onCellEditingStop?.();\r\n }\r\n },\r\n [meta, value, initialValue, rowIndex, columnId],\r\n );\r\n\r\n const onOpenAutoFocus: NonNullable<\r\n React.ComponentProps[\"onOpenAutoFocus\"]\r\n > = React.useCallback((event) => {\r\n event.preventDefault();\r\n if (textareaRef.current) {\r\n textareaRef.current.focus();\r\n const length = textareaRef.current.value.length;\r\n textareaRef.current.setSelectionRange(length, length);\r\n }\r\n }, []);\r\n\r\n const onWrapperKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (isEditing && !open) {\r\n if (event.key === \"Escape\") {\r\n event.preventDefault();\r\n meta?.onCellEditingStop?.();\r\n } else if (event.key === \"Tab\") {\r\n event.preventDefault();\r\n // Save any pending changes\r\n if (value !== initialValue) {\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value });\r\n }\r\n meta?.onCellEditingStop?.({\r\n direction: event.shiftKey ? \"left\" : \"right\",\r\n });\r\n }\r\n }\r\n },\r\n [isEditing, open, meta, value, initialValue, rowIndex, columnId],\r\n );\r\n\r\n const onTextareaKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (event.key === \"Escape\") {\r\n event.preventDefault();\r\n onCancel();\r\n } else if (event.key === \"Enter\" && (event.ctrlKey || event.metaKey)) {\r\n event.preventDefault();\r\n onSave();\r\n }\r\n // Stop propagation to prevent grid navigation\r\n event.stopPropagation();\r\n },\r\n [onCancel, onSave],\r\n );\r\n\r\n const onTextareaBlur = React.useCallback(() => {\r\n // Immediately save any pending changes on blur\r\n if (value !== initialValue) {\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value });\r\n }\r\n setOpen(false);\r\n meta?.onCellEditingStop?.();\r\n }, [meta, value, initialValue, rowIndex, columnId]);\r\n\r\n React.useEffect(() => {\r\n if (isEditing && !open) {\r\n setOpen(true);\r\n }\r\n if (\r\n isFocused &&\r\n !isEditing &&\r\n !meta?.searchOpen &&\r\n !meta?.isScrolling &&\r\n containerRef.current\r\n ) {\r\n containerRef.current.focus();\r\n }\r\n }, [isFocused, isEditing, open, meta?.searchOpen, meta?.isScrolling]);\r\n\r\n return (\r\n \r\n \r\n \r\n {value}\r\n \r\n \r\n \r\n \r\n \r\n \r\n );\r\n}\r\n\r\nexport function NumberCell({\r\n cell,\r\n table,\r\n rowIndex,\r\n columnId,\r\n isFocused,\r\n isEditing,\r\n isSelected,\r\n}: CellVariantProps) {\r\n const initialValue = cell.getValue() as number;\r\n const [value, setValue] = React.useState(String(initialValue ?? \"\"));\r\n const inputRef = React.useRef(null);\r\n const containerRef = React.useRef(null);\r\n const meta = table.options.meta;\r\n const cellOpts = cell.column.columnDef.meta?.cell;\r\n const min = cellOpts?.variant === \"number\" ? cellOpts.min : undefined;\r\n const max = cellOpts?.variant === \"number\" ? cellOpts.max : undefined;\r\n const step = cellOpts?.variant === \"number\" ? cellOpts.step : undefined;\r\n\r\n const onBlur = React.useCallback(() => {\r\n const numValue = value === \"\" ? null : Number(value);\r\n if (numValue !== initialValue) {\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\r\n }\r\n meta?.onCellEditingStop?.();\r\n }, [meta, rowIndex, columnId, initialValue, value]);\r\n\r\n const onChange = React.useCallback(\r\n (event: React.ChangeEvent) => {\r\n setValue(event.target.value);\r\n },\r\n [],\r\n );\r\n\r\n const onWrapperKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (isEditing) {\r\n if (event.key === \"Enter\") {\r\n event.preventDefault();\r\n const numValue = value === \"\" ? null : Number(value);\r\n if (numValue !== initialValue) {\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\r\n }\r\n meta?.onCellEditingStop?.({ moveToNextRow: true });\r\n } else if (event.key === \"Tab\") {\r\n event.preventDefault();\r\n const numValue = value === \"\" ? null : Number(value);\r\n if (numValue !== initialValue) {\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\r\n }\r\n meta?.onCellEditingStop?.({\r\n direction: event.shiftKey ? \"left\" : \"right\",\r\n });\r\n } else if (event.key === \"Escape\") {\r\n event.preventDefault();\r\n setValue(String(initialValue ?? \"\"));\r\n inputRef.current?.blur();\r\n }\r\n } else if (isFocused) {\r\n // Handle Backspace to start editing with empty value\r\n if (event.key === \"Backspace\") {\r\n setValue(\"\");\r\n } else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {\r\n // Handle typing to pre-fill the value when editing starts\r\n setValue(event.key);\r\n }\r\n }\r\n },\r\n [isEditing, isFocused, initialValue, meta, rowIndex, columnId, value],\r\n );\r\n\r\n React.useEffect(() => {\r\n setValue(String(initialValue ?? \"\"));\r\n }, [initialValue]);\r\n\r\n React.useEffect(() => {\r\n if (isEditing && inputRef.current) {\r\n inputRef.current.focus();\r\n inputRef.current.select();\r\n }\r\n if (\r\n isFocused &&\r\n !isEditing &&\r\n !meta?.searchOpen &&\r\n !meta?.isScrolling &&\r\n containerRef.current\r\n ) {\r\n containerRef.current.focus();\r\n }\r\n }, [isFocused, isEditing, meta?.searchOpen, meta?.isScrolling]);\r\n\r\n return (\r\n \r\n {isEditing ? (\r\n \r\n ) : (\r\n {value}\r\n )}\r\n \r\n );\r\n}\r\n\r\nexport function SelectCell({\r\n cell,\r\n table,\r\n rowIndex,\r\n columnId,\r\n isFocused,\r\n isEditing,\r\n isSelected,\r\n}: CellVariantProps) {\r\n const initialValue = cell.getValue() as string;\r\n const [value, setValue] = React.useState(initialValue);\r\n const [open, setOpen] = React.useState(false);\r\n const containerRef = React.useRef(null);\r\n const meta = table.options.meta;\r\n const cellOpts = cell.column.columnDef.meta?.cell;\r\n const options = cellOpts?.variant === \"select\" ? cellOpts.options : [];\r\n\r\n const onValueChange = React.useCallback(\r\n (newValue: string) => {\r\n setValue(newValue);\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\r\n meta?.onCellEditingStop?.();\r\n },\r\n [meta, rowIndex, columnId],\r\n );\r\n\r\n const onOpenChange = React.useCallback(\r\n (isOpen: boolean) => {\r\n setOpen(isOpen);\r\n if (!isOpen) {\r\n meta?.onCellEditingStop?.();\r\n }\r\n },\r\n [meta],\r\n );\r\n\r\n const onWrapperKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (isEditing) {\r\n if (event.key === \"Escape\") {\r\n event.preventDefault();\r\n setValue(initialValue);\r\n setOpen(false);\r\n meta?.onCellEditingStop?.();\r\n } else if (event.key === \"Tab\") {\r\n event.preventDefault();\r\n setOpen(false);\r\n meta?.onCellEditingStop?.({\r\n direction: event.shiftKey ? \"left\" : \"right\",\r\n });\r\n }\r\n }\r\n },\r\n [isEditing, initialValue, meta],\r\n );\r\n\r\n React.useEffect(() => {\r\n setValue(initialValue);\r\n }, [initialValue]);\r\n\r\n React.useEffect(() => {\r\n if (isEditing && !open) {\r\n setOpen(true);\r\n }\r\n if (\r\n isFocused &&\r\n !isEditing &&\r\n !meta?.searchOpen &&\r\n !meta?.isScrolling &&\r\n containerRef.current\r\n ) {\r\n containerRef.current.focus();\r\n }\r\n }, [isFocused, isEditing, open, meta?.searchOpen, meta?.isScrolling]);\r\n\r\n const displayLabel =\r\n options.find((opt) => opt.value === value)?.label ?? value;\r\n\r\n return (\r\n \r\n {isEditing ? (\r\n \r\n \r\n \r\n \r\n \r\n {options.map((option) => (\r\n \r\n {option.label}\r\n \r\n ))}\r\n \r\n \r\n ) : (\r\n {displayLabel}\r\n )}\r\n \r\n );\r\n}\r\n\r\nexport function MultiSelectCell({\r\n cell,\r\n table,\r\n rowIndex,\r\n columnId,\r\n isFocused,\r\n isEditing,\r\n isSelected,\r\n}: CellVariantProps) {\r\n const cellValue = React.useMemo(\r\n () => (cell.getValue() as string[]) ?? [],\r\n [cell],\r\n );\r\n\r\n const cellId = `${rowIndex}-${columnId}`;\r\n const prevCellIdRef = React.useRef(cellId);\r\n\r\n const [selectedValues, setSelectedValues] =\r\n React.useState(cellValue);\r\n const [open, setOpen] = React.useState(false);\r\n const [searchValue, setSearchValue] = React.useState(\"\");\r\n const containerRef = React.useRef(null);\r\n const inputRef = React.useRef(null);\r\n const meta = table.options.meta;\r\n const cellOpts = cell.column.columnDef.meta?.cell;\r\n const options = cellOpts?.variant === \"multi-select\" ? cellOpts.options : [];\r\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\r\n\r\n if (prevCellIdRef.current !== cellId) {\r\n prevCellIdRef.current = cellId;\r\n setSelectedValues(cellValue);\r\n setOpen(false);\r\n setSearchValue(\"\");\r\n }\r\n\r\n const onValueChange = React.useCallback(\r\n (value: string) => {\r\n const newValues = selectedValues.includes(value)\r\n ? selectedValues.filter((v) => v !== value)\r\n : [...selectedValues, value];\r\n\r\n setSelectedValues(newValues);\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\r\n // Clear search input and focus back on input after selection\r\n setSearchValue(\"\");\r\n queueMicrotask(() => inputRef.current?.focus());\r\n },\r\n [selectedValues, meta, rowIndex, columnId],\r\n );\r\n\r\n const removeValue = React.useCallback(\r\n (valueToRemove: string, event?: React.MouseEvent) => {\r\n event?.stopPropagation();\r\n event?.preventDefault();\r\n const newValues = selectedValues.filter((v) => v !== valueToRemove);\r\n setSelectedValues(newValues);\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\r\n // Focus back on input after removing\r\n setTimeout(() => inputRef.current?.focus(), 0);\r\n },\r\n [selectedValues, meta, rowIndex, columnId],\r\n );\r\n\r\n const clearAll = React.useCallback(() => {\r\n setSelectedValues([]);\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: [] });\r\n queueMicrotask(() => inputRef.current?.focus());\r\n }, [meta, rowIndex, columnId]);\r\n\r\n const onOpenChange = React.useCallback(\r\n (isOpen: boolean) => {\r\n setOpen(isOpen);\r\n if (!isOpen) {\r\n setSearchValue(\"\");\r\n meta?.onCellEditingStop?.();\r\n }\r\n },\r\n [meta],\r\n );\r\n\r\n const onOpenAutoFocus: NonNullable<\r\n React.ComponentProps[\"onOpenAutoFocus\"]\r\n > = React.useCallback((event) => {\r\n event.preventDefault();\r\n inputRef.current?.focus();\r\n }, []);\r\n\r\n const onWrapperKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (isEditing) {\r\n if (event.key === \"Escape\") {\r\n event.preventDefault();\r\n setSelectedValues(cellValue);\r\n setSearchValue(\"\");\r\n setOpen(false);\r\n meta?.onCellEditingStop?.();\r\n } else if (event.key === \"Tab\") {\r\n event.preventDefault();\r\n setSearchValue(\"\");\r\n setOpen(false);\r\n meta?.onCellEditingStop?.({\r\n direction: event.shiftKey ? \"left\" : \"right\",\r\n });\r\n }\r\n }\r\n },\r\n [isEditing, cellValue, meta],\r\n );\r\n\r\n const onInputKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n // Handle backspace when input is empty - remove last selected item\r\n if (\r\n event.key === \"Backspace\" &&\r\n searchValue === \"\" &&\r\n selectedValues.length > 0\r\n ) {\r\n event.preventDefault();\r\n const lastValue = selectedValues[selectedValues.length - 1];\r\n if (lastValue) {\r\n removeValue(lastValue);\r\n }\r\n }\r\n // Prevent escape from propagating to close the popover immediately\r\n // Let the command handle it first\r\n if (event.key === \"Escape\") {\r\n event.stopPropagation();\r\n }\r\n },\r\n [searchValue, selectedValues, removeValue],\r\n );\r\n\r\n React.useEffect(() => {\r\n if (isEditing && !open) {\r\n setOpen(true);\r\n }\r\n if (\r\n isFocused &&\r\n !isEditing &&\r\n !meta?.searchOpen &&\r\n !meta?.isScrolling &&\r\n containerRef.current\r\n ) {\r\n containerRef.current.focus();\r\n }\r\n }, [isFocused, isEditing, open, meta?.searchOpen, meta?.isScrolling]);\r\n\r\n // Focus input when popover opens\r\n React.useEffect(() => {\r\n if (open && inputRef.current) {\r\n setTimeout(() => inputRef.current?.focus(), 0);\r\n }\r\n }, [open]);\r\n\r\n const displayLabels = selectedValues\r\n .map((val) => options.find((opt) => opt.value === val)?.label ?? val)\r\n .filter(Boolean);\r\n\r\n const rowHeight = table.options.meta?.rowHeight ?? \"short\";\r\n\r\n const lineCount = getLineCount(rowHeight);\r\n const maxVisibleBadgeCount = lineCount * 3;\r\n\r\n const visibleLabels = displayLabels.slice(0, maxVisibleBadgeCount);\r\n const hiddenBadgeCount = Math.max(\r\n 0,\r\n displayLabels.length - maxVisibleBadgeCount,\r\n );\r\n\r\n return (\r\n \r\n {isEditing ? (\r\n \r\n \r\n
\r\n \r\n \r\n \r\n
\r\n {selectedValues.map((value) => {\r\n const option = options.find((opt) => opt.value === value);\r\n const label = option?.label ?? value;\r\n\r\n return (\r\n \r\n {label}\r\n removeValue(value, event)}\r\n onPointerDown={(event) => {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n }}\r\n >\r\n \r\n \r\n \r\n );\r\n })}\r\n \r\n
\r\n \r\n No options found.\r\n \r\n {options.map((option) => {\r\n const isSelected = selectedValues.includes(option.value);\r\n\r\n return (\r\n onValueChange(option.value)}\r\n >\r\n \r\n \r\n
\r\n {option.label}\r\n \r\n );\r\n })}\r\n \r\n {selectedValues.length > 0 && (\r\n <>\r\n \r\n \r\n \r\n Clear all\r\n \r\n \r\n \r\n )}\r\n \r\n \r\n \r\n
\r\n ) : null}\r\n {displayLabels.length > 0 ? (\r\n
\r\n {visibleLabels.map((label, index) => (\r\n \r\n {label}\r\n \r\n ))}\r\n {hiddenBadgeCount > 0 && (\r\n \r\n +{hiddenBadgeCount}\r\n \r\n )}\r\n
\r\n ) : null}\r\n \r\n );\r\n}\r\n\r\nexport function CheckboxCell({\r\n cell,\r\n table,\r\n rowIndex,\r\n columnId,\r\n isFocused,\r\n isSelected,\r\n}: CellVariantProps) {\r\n const initialValue = cell.getValue() as boolean;\r\n const [value, setValue] = React.useState(Boolean(initialValue));\r\n const containerRef = React.useRef(null);\r\n const meta = table.options.meta;\r\n\r\n const onCheckedChange = React.useCallback(\r\n (checked: boolean) => {\r\n setValue(checked);\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: checked });\r\n },\r\n [meta, rowIndex, columnId],\r\n );\r\n\r\n const onWrapperKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (isFocused && (event.key === \" \" || event.key === \"Enter\")) {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n onCheckedChange(!value);\r\n }\r\n },\r\n [isFocused, value, onCheckedChange],\r\n );\r\n\r\n React.useEffect(() => {\r\n setValue(Boolean(initialValue));\r\n }, [initialValue]);\r\n\r\n React.useEffect(() => {\r\n if (\r\n isFocused &&\r\n !meta?.searchOpen &&\r\n !meta?.isScrolling &&\r\n containerRef.current\r\n ) {\r\n containerRef.current.focus();\r\n }\r\n }, [isFocused, meta?.searchOpen, meta?.isScrolling]);\r\n\r\n const onWrapperClick = React.useCallback(\r\n (event: React.MouseEvent) => {\r\n if (isFocused) {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n onCheckedChange(!value);\r\n }\r\n },\r\n [isFocused, value, onCheckedChange],\r\n );\r\n\r\n const onCheckboxClick = React.useCallback((event: React.MouseEvent) => {\r\n event.stopPropagation();\r\n }, []);\r\n\r\n const onCheckboxMouseDown = React.useCallback(\r\n (event: React.MouseEvent) => {\r\n event.stopPropagation();\r\n },\r\n [],\r\n );\r\n\r\n const onCheckboxDoubleClick = React.useCallback(\r\n (event: React.MouseEvent) => {\r\n event.stopPropagation();\r\n },\r\n [],\r\n );\r\n\r\n return (\r\n \r\n \r\n \r\n );\r\n}\r\n\r\nfunction formatDateForDisplay(dateStr: string) {\r\n if (!dateStr) return \"\";\r\n const date = new Date(dateStr);\r\n return date.toLocaleDateString();\r\n}\r\n\r\nexport function DateCell({\r\n cell,\r\n table,\r\n rowIndex,\r\n columnId,\r\n isFocused,\r\n isEditing,\r\n isSelected,\r\n}: CellVariantProps) {\r\n const initialValue = cell.getValue() as string;\r\n const [value, setValue] = React.useState(initialValue ?? \"\");\r\n const [open, setOpen] = React.useState(false);\r\n const containerRef = React.useRef(null);\r\n const meta = table.options.meta;\r\n\r\n const prevInitialValueRef = React.useRef(initialValue);\r\n if (initialValue !== prevInitialValueRef.current) {\r\n prevInitialValueRef.current = initialValue;\r\n setValue(initialValue ?? \"\");\r\n }\r\n\r\n const selectedDate = value ? new Date(value) : undefined;\r\n\r\n const onDateSelect = React.useCallback(\r\n (date: Date | undefined) => {\r\n if (!date) return;\r\n\r\n const formattedDate = date.toISOString().split(\"T\")[0] ?? \"\";\r\n setValue(formattedDate);\r\n meta?.onDataUpdate?.({ rowIndex, columnId, value: formattedDate });\r\n setOpen(false);\r\n meta?.onCellEditingStop?.();\r\n },\r\n [meta, rowIndex, columnId],\r\n );\r\n\r\n const onOpenChange = React.useCallback(\r\n (isOpen: boolean) => {\r\n setOpen(isOpen);\r\n if (!isOpen && isEditing) {\r\n meta?.onCellEditingStop?.();\r\n }\r\n },\r\n [isEditing, meta],\r\n );\r\n\r\n const onWrapperKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (isEditing) {\r\n if (event.key === \"Escape\") {\r\n event.preventDefault();\r\n setValue(initialValue);\r\n setOpen(false);\r\n } else if (event.key === \"Tab\") {\r\n event.preventDefault();\r\n setOpen(false);\r\n meta?.onCellEditingStop?.({\r\n direction: event.shiftKey ? \"left\" : \"right\",\r\n });\r\n }\r\n }\r\n },\r\n [isEditing, initialValue, meta],\r\n );\r\n\r\n React.useEffect(() => {\r\n if (isEditing) {\r\n setOpen(true);\r\n } else {\r\n setOpen(false);\r\n }\r\n }, [isEditing]);\r\n\r\n React.useEffect(() => {\r\n if (\r\n isFocused &&\r\n !isEditing &&\r\n !meta?.searchOpen &&\r\n !meta?.isScrolling &&\r\n containerRef.current\r\n ) {\r\n containerRef.current.focus();\r\n }\r\n }, [isFocused, isEditing, meta?.searchOpen, meta?.isScrolling]);\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n {formatDateForDisplay(value)}\r\n \r\n \r\n {isEditing && (\r\n \r\n \r\n \r\n )}\r\n \r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Cell, Table } from \"@tanstack/react-table\";\nimport { Check, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { DataGridCellWrapper } from \"@/components/data-grid/data-grid-cell-wrapper\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverAnchor,\n PopoverContent,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getLineCount } from \"@/lib/data-grid\";\nimport { cn } from \"@/lib/utils\";\n\ninterface CellVariantProps {\n cell: Cell;\n table: Table;\n rowIndex: number;\n columnId: string;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n}\n\nexport function ShortTextCell({\n cell,\n table,\n rowIndex,\n columnId,\n isEditing,\n isFocused,\n isSelected,\n}: CellVariantProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue);\n const cellRef = React.useRef(null);\n const containerRef = React.useRef(null);\n const meta = table.options.meta;\n\n const onBlur = React.useCallback(() => {\n // Read the current value directly from the DOM to avoid stale state\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (currentValue !== initialValue) {\n meta?.onDataUpdate?.({ rowIndex, columnId, value: currentValue });\n }\n meta?.onCellEditingStop?.();\n }, [meta, rowIndex, columnId, initialValue]);\n\n const onInput = React.useCallback(\n (event: React.FormEvent) => {\n const currentValue = event.currentTarget.textContent ?? \"\";\n setValue(currentValue);\n },\n [],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Enter\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (currentValue !== initialValue) {\n meta?.onDataUpdate?.({ rowIndex, columnId, value: currentValue });\n }\n meta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (currentValue !== initialValue) {\n meta?.onDataUpdate?.({ rowIndex, columnId, value: currentValue });\n }\n meta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n cellRef.current?.blur();\n }\n } else if (\n isFocused &&\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n // Handle typing to pre-fill the value when editing starts\n setValue(event.key);\n\n queueMicrotask(() => {\n if (cellRef.current && cellRef.current.contentEditable === \"true\") {\n cellRef.current.textContent = event.key;\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n });\n }\n },\n [isEditing, isFocused, initialValue, meta, rowIndex, columnId],\n );\n\n React.useEffect(() => {\n setValue(initialValue);\n if (cellRef.current && !isEditing) {\n cellRef.current.textContent = initialValue;\n }\n }, [initialValue, isEditing]);\n\n React.useEffect(() => {\n if (isEditing && cellRef.current) {\n cellRef.current.focus();\n\n if (!cellRef.current.textContent && value) {\n cellRef.current.textContent = value;\n }\n\n if (cellRef.current.textContent) {\n const range = document.createRange();\n const selection = window.getSelection();\n range.selectNodeContents(cellRef.current);\n range.collapse(false);\n selection?.removeAllRanges();\n selection?.addRange(range);\n }\n }\n // Don't focus if we're in the middle of a scroll operation\n if (\n isFocused &&\n !isEditing &&\n !meta?.searchOpen &&\n !meta?.isScrolling &&\n containerRef.current\n ) {\n containerRef.current.focus();\n }\n }, [isFocused, isEditing, value, meta?.searchOpen, meta?.isScrolling]);\n\n const displayValue = !isEditing ? (value ?? \"\") : \"\";\n\n return (\n \n \n {displayValue}\n \n \n );\n}\n\nexport function LongTextCell({\n cell,\n table,\n rowIndex,\n columnId,\n isFocused,\n isEditing,\n isSelected,\n}: CellVariantProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const [open, setOpen] = React.useState(false);\n const textareaRef = React.useRef(null);\n const containerRef = React.useRef(null);\n const meta = table.options.meta;\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n }\n\n // Debounced auto-save (300ms delay)\n const debouncedSave = useDebouncedCallback((newValue: string) => {\n meta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\n }, 300);\n\n const onSave = React.useCallback(() => {\n // Immediately save any pending changes and close the popover\n if (value !== initialValue) {\n meta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n setOpen(false);\n meta?.onCellEditingStop?.();\n }, [meta, value, initialValue, rowIndex, columnId]);\n\n const onCancel = React.useCallback(() => {\n // Restore the original value\n setValue(initialValue ?? \"\");\n meta?.onDataUpdate?.({ rowIndex, columnId, value: initialValue });\n setOpen(false);\n meta?.onCellEditingStop?.();\n }, [meta, initialValue, rowIndex, columnId]);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const newValue = event.target.value;\n setValue(newValue);\n // Debounced auto-save\n debouncedSave(newValue);\n },\n [debouncedSave],\n );\n\n const onOpenChange = React.useCallback(\n (isOpen: boolean) => {\n setOpen(isOpen);\n if (!isOpen) {\n // Immediately save any pending changes when closing\n if (value !== initialValue) {\n meta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n meta?.onCellEditingStop?.();\n }\n },\n [meta, value, initialValue, rowIndex, columnId],\n );\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n if (textareaRef.current) {\n textareaRef.current.focus();\n const length = textareaRef.current.value.length;\n textareaRef.current.setSelectionRange(length, length);\n }\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && !open) {\n if (event.key === \"Escape\") {\n event.preventDefault();\n meta?.onCellEditingStop?.();\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n // Save any pending changes\n if (value !== initialValue) {\n meta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n meta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n }\n },\n [isEditing, open, meta, value, initialValue, rowIndex, columnId],\n );\n\n const onTextareaKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n onCancel();\n } else if (event.key === \"Enter\" && (event.ctrlKey || event.metaKey)) {\n event.preventDefault();\n onSave();\n }\n // Stop propagation to prevent grid navigation\n event.stopPropagation();\n },\n [onCancel, onSave],\n );\n\n const onTextareaBlur = React.useCallback(() => {\n // Immediately save any pending changes on blur\n if (value !== initialValue) {\n meta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n setOpen(false);\n meta?.onCellEditingStop?.();\n }, [meta, value, initialValue, rowIndex, columnId]);\n\n React.useEffect(() => {\n if (isEditing && !open) {\n setOpen(true);\n }\n if (\n isFocused &&\n !isEditing &&\n !meta?.searchOpen &&\n !meta?.isScrolling &&\n containerRef.current\n ) {\n containerRef.current.focus();\n }\n }, [isFocused, isEditing, open, meta?.searchOpen, meta?.isScrolling]);\n\n return (\n \n \n \n {value}\n \n \n \n \n \n \n );\n}\n\nexport function NumberCell({\n cell,\n table,\n rowIndex,\n columnId,\n isFocused,\n isEditing,\n isSelected,\n}: CellVariantProps) {\n const initialValue = cell.getValue() as number;\n const [value, setValue] = React.useState(String(initialValue ?? \"\"));\n const inputRef = React.useRef(null);\n const containerRef = React.useRef(null);\n const meta = table.options.meta;\n const cellOpts = cell.column.columnDef.meta?.cell;\n const min = cellOpts?.variant === \"number\" ? cellOpts.min : undefined;\n const max = cellOpts?.variant === \"number\" ? cellOpts.max : undefined;\n const step = cellOpts?.variant === \"number\" ? cellOpts.step : undefined;\n\n const onBlur = React.useCallback(() => {\n const numValue = value === \"\" ? null : Number(value);\n if (numValue !== initialValue) {\n meta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n meta?.onCellEditingStop?.();\n }, [meta, rowIndex, columnId, initialValue, value]);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n setValue(event.target.value);\n },\n [],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Enter\") {\n event.preventDefault();\n const numValue = value === \"\" ? null : Number(value);\n if (numValue !== initialValue) {\n meta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n meta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const numValue = value === \"\" ? null : Number(value);\n if (numValue !== initialValue) {\n meta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n meta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n } else if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(String(initialValue ?? \"\"));\n inputRef.current?.blur();\n }\n } else if (isFocused) {\n // Handle Backspace to start editing with empty value\n if (event.key === \"Backspace\") {\n setValue(\"\");\n } else if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {\n // Handle typing to pre-fill the value when editing starts\n setValue(event.key);\n }\n }\n },\n [isEditing, isFocused, initialValue, meta, rowIndex, columnId, value],\n );\n\n React.useEffect(() => {\n setValue(String(initialValue ?? \"\"));\n }, [initialValue]);\n\n React.useEffect(() => {\n if (isEditing && inputRef.current) {\n inputRef.current.focus();\n inputRef.current.select();\n }\n if (\n isFocused &&\n !isEditing &&\n !meta?.searchOpen &&\n !meta?.isScrolling &&\n containerRef.current\n ) {\n containerRef.current.focus();\n }\n }, [isFocused, isEditing, meta?.searchOpen, meta?.isScrolling]);\n\n return (\n \n {isEditing ? (\n \n ) : (\n {value}\n )}\n \n );\n}\n\nexport function SelectCell({\n cell,\n table,\n rowIndex,\n columnId,\n isFocused,\n isEditing,\n isSelected,\n}: CellVariantProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue);\n const [open, setOpen] = React.useState(false);\n const containerRef = React.useRef(null);\n const meta = table.options.meta;\n const cellOpts = cell.column.columnDef.meta?.cell;\n const options = cellOpts?.variant === \"select\" ? cellOpts.options : [];\n\n const onValueChange = React.useCallback(\n (newValue: string) => {\n setValue(newValue);\n meta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\n meta?.onCellEditingStop?.();\n },\n [meta, rowIndex, columnId],\n );\n\n const onOpenChange = React.useCallback(\n (isOpen: boolean) => {\n setOpen(isOpen);\n if (!isOpen) {\n meta?.onCellEditingStop?.();\n }\n },\n [meta],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n setOpen(false);\n meta?.onCellEditingStop?.();\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n setOpen(false);\n meta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n }\n },\n [isEditing, initialValue, meta],\n );\n\n React.useEffect(() => {\n setValue(initialValue);\n }, [initialValue]);\n\n React.useEffect(() => {\n if (isEditing && !open) {\n setOpen(true);\n }\n if (\n isFocused &&\n !isEditing &&\n !meta?.searchOpen &&\n !meta?.isScrolling &&\n containerRef.current\n ) {\n containerRef.current.focus();\n }\n }, [isFocused, isEditing, open, meta?.searchOpen, meta?.isScrolling]);\n\n const displayLabel =\n options.find((opt) => opt.value === value)?.label ?? value;\n\n return (\n \n {isEditing ? (\n \n \n \n \n \n {options.map((option) => (\n \n {option.label}\n \n ))}\n \n \n ) : (\n {displayLabel}\n )}\n \n );\n}\n\nexport function MultiSelectCell({\n cell,\n table,\n rowIndex,\n columnId,\n isFocused,\n isEditing,\n isSelected,\n}: CellVariantProps) {\n const cellValue = React.useMemo(\n () => (cell.getValue() as string[]) ?? [],\n [cell],\n );\n\n const cellId = `${rowIndex}-${columnId}`;\n const prevCellIdRef = React.useRef(cellId);\n\n const [selectedValues, setSelectedValues] =\n React.useState(cellValue);\n const [open, setOpen] = React.useState(false);\n const [searchValue, setSearchValue] = React.useState(\"\");\n const containerRef = React.useRef(null);\n const inputRef = React.useRef(null);\n const meta = table.options.meta;\n const cellOpts = cell.column.columnDef.meta?.cell;\n const options = cellOpts?.variant === \"multi-select\" ? cellOpts.options : [];\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n if (prevCellIdRef.current !== cellId) {\n prevCellIdRef.current = cellId;\n setSelectedValues(cellValue);\n setOpen(false);\n setSearchValue(\"\");\n }\n\n const onValueChange = React.useCallback(\n (value: string) => {\n const newValues = selectedValues.includes(value)\n ? selectedValues.filter((v) => v !== value)\n : [...selectedValues, value];\n\n setSelectedValues(newValues);\n meta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n // Clear search input and focus back on input after selection\n setSearchValue(\"\");\n queueMicrotask(() => inputRef.current?.focus());\n },\n [selectedValues, meta, rowIndex, columnId],\n );\n\n const removeValue = React.useCallback(\n (valueToRemove: string, event?: React.MouseEvent) => {\n event?.stopPropagation();\n event?.preventDefault();\n const newValues = selectedValues.filter((v) => v !== valueToRemove);\n setSelectedValues(newValues);\n meta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n // Focus back on input after removing\n setTimeout(() => inputRef.current?.focus(), 0);\n },\n [selectedValues, meta, rowIndex, columnId],\n );\n\n const clearAll = React.useCallback(() => {\n setSelectedValues([]);\n meta?.onDataUpdate?.({ rowIndex, columnId, value: [] });\n queueMicrotask(() => inputRef.current?.focus());\n }, [meta, rowIndex, columnId]);\n\n const onOpenChange = React.useCallback(\n (isOpen: boolean) => {\n setOpen(isOpen);\n if (!isOpen) {\n setSearchValue(\"\");\n meta?.onCellEditingStop?.();\n }\n },\n [meta],\n );\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n inputRef.current?.focus();\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Escape\") {\n event.preventDefault();\n setSelectedValues(cellValue);\n setSearchValue(\"\");\n setOpen(false);\n meta?.onCellEditingStop?.();\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n setSearchValue(\"\");\n setOpen(false);\n meta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n }\n },\n [isEditing, cellValue, meta],\n );\n\n const onInputKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n // Handle backspace when input is empty - remove last selected item\n if (\n event.key === \"Backspace\" &&\n searchValue === \"\" &&\n selectedValues.length > 0\n ) {\n event.preventDefault();\n const lastValue = selectedValues[selectedValues.length - 1];\n if (lastValue) {\n removeValue(lastValue);\n }\n }\n // Prevent escape from propagating to close the popover immediately\n // Let the command handle it first\n if (event.key === \"Escape\") {\n event.stopPropagation();\n }\n },\n [searchValue, selectedValues, removeValue],\n );\n\n React.useEffect(() => {\n if (isEditing && !open) {\n setOpen(true);\n }\n if (\n isFocused &&\n !isEditing &&\n !meta?.searchOpen &&\n !meta?.isScrolling &&\n containerRef.current\n ) {\n containerRef.current.focus();\n }\n }, [isFocused, isEditing, open, meta?.searchOpen, meta?.isScrolling]);\n\n // Focus input when popover opens\n React.useEffect(() => {\n if (open && inputRef.current) {\n setTimeout(() => inputRef.current?.focus(), 0);\n }\n }, [open]);\n\n const displayLabels = selectedValues\n .map((val) => options.find((opt) => opt.value === val)?.label ?? val)\n .filter(Boolean);\n\n const rowHeight = table.options.meta?.rowHeight ?? \"short\";\n\n const lineCount = getLineCount(rowHeight);\n const maxVisibleBadgeCount = lineCount * 3;\n\n const visibleLabels = displayLabels.slice(0, maxVisibleBadgeCount);\n const hiddenBadgeCount = Math.max(\n 0,\n displayLabels.length - maxVisibleBadgeCount,\n );\n\n return (\n \n {isEditing ? (\n \n \n
\n \n \n \n
\n {selectedValues.map((value) => {\n const option = options.find((opt) => opt.value === value);\n const label = option?.label ?? value;\n\n return (\n \n {label}\n removeValue(value, event)}\n onPointerDown={(event) => {\n event.preventDefault();\n event.stopPropagation();\n }}\n >\n \n \n \n );\n })}\n \n
\n \n No options found.\n \n {options.map((option) => {\n const isSelected = selectedValues.includes(option.value);\n\n return (\n onValueChange(option.value)}\n >\n \n \n
\n {option.label}\n \n );\n })}\n \n {selectedValues.length > 0 && (\n <>\n \n \n \n Clear all\n \n \n \n )}\n \n \n \n
\n ) : null}\n {displayLabels.length > 0 ? (\n
\n {visibleLabels.map((label, index) => (\n \n {label}\n \n ))}\n {hiddenBadgeCount > 0 && (\n \n +{hiddenBadgeCount}\n \n )}\n
\n ) : null}\n \n );\n}\n\nexport function CheckboxCell({\n cell,\n table,\n rowIndex,\n columnId,\n isFocused,\n isSelected,\n}: CellVariantProps) {\n const initialValue = cell.getValue() as boolean;\n const [value, setValue] = React.useState(Boolean(initialValue));\n const containerRef = React.useRef(null);\n const meta = table.options.meta;\n\n const onCheckedChange = React.useCallback(\n (checked: boolean) => {\n setValue(checked);\n meta?.onDataUpdate?.({ rowIndex, columnId, value: checked });\n },\n [meta, rowIndex, columnId],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isFocused && (event.key === \" \" || event.key === \"Enter\")) {\n event.preventDefault();\n event.stopPropagation();\n onCheckedChange(!value);\n }\n },\n [isFocused, value, onCheckedChange],\n );\n\n React.useEffect(() => {\n setValue(Boolean(initialValue));\n }, [initialValue]);\n\n React.useEffect(() => {\n if (\n isFocused &&\n !meta?.searchOpen &&\n !meta?.isScrolling &&\n containerRef.current\n ) {\n containerRef.current.focus();\n }\n }, [isFocused, meta?.searchOpen, meta?.isScrolling]);\n\n const onWrapperClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (isFocused) {\n event.preventDefault();\n event.stopPropagation();\n onCheckedChange(!value);\n }\n },\n [isFocused, value, onCheckedChange],\n );\n\n const onCheckboxClick = React.useCallback((event: React.MouseEvent) => {\n event.stopPropagation();\n }, []);\n\n const onCheckboxMouseDown = React.useCallback(\n (event: React.MouseEvent) => {\n event.stopPropagation();\n },\n [],\n );\n\n const onCheckboxDoubleClick = React.useCallback(\n (event: React.MouseEvent) => {\n event.stopPropagation();\n },\n [],\n );\n\n return (\n \n \n \n );\n}\n\nfunction formatDateForDisplay(dateStr: string) {\n if (!dateStr) return \"\";\n const date = new Date(dateStr);\n return date.toLocaleDateString();\n}\n\nexport function DateCell({\n cell,\n table,\n rowIndex,\n columnId,\n isFocused,\n isEditing,\n isSelected,\n}: CellVariantProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const [open, setOpen] = React.useState(false);\n const containerRef = React.useRef(null);\n const meta = table.options.meta;\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n }\n\n const selectedDate = value ? new Date(value) : undefined;\n\n const onDateSelect = React.useCallback(\n (date: Date | undefined) => {\n if (!date) return;\n\n const formattedDate = date.toISOString().split(\"T\")[0] ?? \"\";\n setValue(formattedDate);\n meta?.onDataUpdate?.({ rowIndex, columnId, value: formattedDate });\n setOpen(false);\n meta?.onCellEditingStop?.();\n },\n [meta, rowIndex, columnId],\n );\n\n const onOpenChange = React.useCallback(\n (isOpen: boolean) => {\n setOpen(isOpen);\n if (!isOpen && isEditing) {\n meta?.onCellEditingStop?.();\n }\n },\n [isEditing, meta],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n setOpen(false);\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n setOpen(false);\n meta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n }\n },\n [isEditing, initialValue, meta],\n );\n\n React.useEffect(() => {\n if (isEditing) {\n setOpen(true);\n } else {\n setOpen(false);\n }\n }, [isEditing]);\n\n React.useEffect(() => {\n if (\n isFocused &&\n !isEditing &&\n !meta?.searchOpen &&\n !meta?.isScrolling &&\n containerRef.current\n ) {\n containerRef.current.focus();\n }\n }, [isFocused, isEditing, meta?.searchOpen, meta?.isScrolling]);\n\n return (\n \n \n \n \n {formatDateForDisplay(value)}\n \n \n {isEditing && (\n \n \n \n )}\n \n \n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-cell-variants.tsx" }, { "path": "src/components/data-grid/data-grid-column-header.tsx", - "content": "\"use client\";\r\n\r\nimport type {\r\n ColumnSort,\r\n Header,\r\n SortDirection,\r\n SortingState,\r\n Table,\r\n} from \"@tanstack/react-table\";\r\nimport {\r\n BaselineIcon,\r\n CalendarIcon,\r\n CheckSquareIcon,\r\n ChevronDownIcon,\r\n ChevronUpIcon,\r\n EyeOffIcon,\r\n HashIcon,\r\n ListChecksIcon,\r\n ListIcon,\r\n PinIcon,\r\n PinOffIcon,\r\n TextInitialIcon,\r\n XIcon,\r\n} from \"lucide-react\";\r\nimport * as React from \"react\";\r\n\r\nimport {\r\n DropdownMenu,\r\n DropdownMenuCheckboxItem,\r\n DropdownMenuContent,\r\n DropdownMenuItem,\r\n DropdownMenuSeparator,\r\n DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipTrigger,\r\n} from \"@/components/ui/tooltip\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport type { Cell } from \"@/types/data-grid\";\r\n\r\nfunction getColumnVariant(variant?: Cell[\"variant\"]): {\r\n icon: React.ComponentType>;\r\n label: string;\r\n} | null {\r\n switch (variant) {\r\n case \"short-text\":\r\n return { icon: BaselineIcon, label: \"Short text\" };\r\n case \"long-text\":\r\n return { icon: TextInitialIcon, label: \"Long text\" };\r\n case \"number\":\r\n return { icon: HashIcon, label: \"Number\" };\r\n case \"select\":\r\n return { icon: ListIcon, label: \"Select\" };\r\n case \"multi-select\":\r\n return { icon: ListChecksIcon, label: \"Multi-select\" };\r\n case \"checkbox\":\r\n return { icon: CheckSquareIcon, label: \"Checkbox\" };\r\n case \"date\":\r\n return { icon: CalendarIcon, label: \"Date\" };\r\n default:\r\n return null;\r\n }\r\n}\r\n\r\ninterface DataGridColumnHeaderProps\r\n extends React.ComponentProps {\r\n header: Header;\r\n table: Table;\r\n}\r\n\r\nexport function DataGridColumnHeader({\r\n header,\r\n table,\r\n className,\r\n onPointerDown,\r\n ...props\r\n}: DataGridColumnHeaderProps) {\r\n const column = header.column;\r\n const label = column.columnDef.meta?.label\r\n ? column.columnDef.meta.label\r\n : typeof column.columnDef.header === \"string\"\r\n ? column.columnDef.header\r\n : column.id;\r\n\r\n const isAnyColumnResizing =\r\n table.getState().columnSizingInfo.isResizingColumn;\r\n\r\n const cellVariant = column.columnDef.meta?.cell;\r\n const columnVariant = getColumnVariant(cellVariant?.variant);\r\n\r\n const pinnedPosition = column.getIsPinned();\r\n const isPinnedLeft = pinnedPosition === \"left\";\r\n const isPinnedRight = pinnedPosition === \"right\";\r\n\r\n const onSortingChange = React.useCallback(\r\n (direction: SortDirection) => {\r\n table.setSorting((prev: SortingState) => {\r\n const existingSortIndex = prev.findIndex(\r\n (sort) => sort.id === column.id,\r\n );\r\n const newSort: ColumnSort = {\r\n id: column.id,\r\n desc: direction === \"desc\",\r\n };\r\n\r\n if (existingSortIndex >= 0) {\r\n const updated = [...prev];\r\n updated[existingSortIndex] = newSort;\r\n return updated;\r\n } else {\r\n return [...prev, newSort];\r\n }\r\n });\r\n },\r\n [column.id, table],\r\n );\r\n\r\n const onSortRemove = React.useCallback(() => {\r\n table.setSorting((prev: SortingState) =>\r\n prev.filter((sort) => sort.id !== column.id),\r\n );\r\n }, [column.id, table]);\r\n\r\n const onLeftPin = React.useCallback(() => {\r\n column.pin(\"left\");\r\n }, [column]);\r\n\r\n const onRightPin = React.useCallback(() => {\r\n column.pin(\"right\");\r\n }, [column]);\r\n\r\n const onUnpin = React.useCallback(() => {\r\n column.pin(false);\r\n }, [column]);\r\n\r\n const onTriggerPointerDown = React.useCallback(\r\n (event: React.PointerEvent) => {\r\n onPointerDown?.(event);\r\n if (event.defaultPrevented) return;\r\n\r\n if (event.button !== 0) {\r\n return;\r\n }\r\n table.options.meta?.onColumnClick?.(column.id);\r\n },\r\n [table.options.meta, column.id, onPointerDown],\r\n );\r\n\r\n return (\r\n <>\r\n \r\n \r\n
\r\n {columnVariant && (\r\n \r\n \r\n \r\n \r\n \r\n

{columnVariant.label}

\r\n
\r\n
\r\n )}\r\n {label}\r\n
\r\n \r\n \r\n \r\n {column.getCanSort() && (\r\n <>\r\n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\r\n checked={column.getIsSorted() === \"asc\"}\r\n onClick={() => onSortingChange(\"asc\")}\r\n >\r\n \r\n Sort asc\r\n \r\n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\r\n checked={column.getIsSorted() === \"desc\"}\r\n onClick={() => onSortingChange(\"desc\")}\r\n >\r\n \r\n Sort desc\r\n \r\n {column.getIsSorted() && (\r\n \r\n \r\n Remove sort\r\n \r\n )}\r\n \r\n )}\r\n {column.getCanPin() && (\r\n <>\r\n {column.getCanSort() && }\r\n\r\n {isPinnedLeft ? (\r\n \r\n \r\n Unpin from left\r\n \r\n ) : (\r\n \r\n \r\n Pin to left\r\n \r\n )}\r\n {isPinnedRight ? (\r\n \r\n \r\n Unpin from right\r\n \r\n ) : (\r\n \r\n \r\n Pin to right\r\n \r\n )}\r\n \r\n )}\r\n {column.getCanHide() && (\r\n <>\r\n \r\n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\r\n checked={!column.getIsVisible()}\r\n onClick={() => column.toggleVisibility(false)}\r\n >\r\n \r\n Hide column\r\n \r\n \r\n )}\r\n \r\n
\r\n {header.column.getCanResize() && (\r\n \r\n )}\r\n \r\n );\r\n}\r\n\r\nconst DataGridColumnResizer = React.memo(\r\n DataGridColumnResizerImpl,\r\n (prev, next) => {\r\n const prevColumn = prev.header.column;\r\n const nextColumn = next.header.column;\r\n\r\n if (\r\n prevColumn.getIsResizing() !== nextColumn.getIsResizing() ||\r\n prevColumn.getSize() !== nextColumn.getSize()\r\n ) {\r\n return false;\r\n }\r\n\r\n if (prev.label !== next.label) return false;\r\n\r\n return true;\r\n },\r\n) as typeof DataGridColumnResizerImpl;\r\n\r\ninterface DataGridColumnResizerProps\r\n extends DataGridColumnHeaderProps {\r\n label: string;\r\n}\r\n\r\nfunction DataGridColumnResizerImpl({\r\n header,\r\n table,\r\n label,\r\n}: DataGridColumnResizerProps) {\r\n const defaultColumnDef = table._getDefaultColumnDef();\r\n\r\n const onDoubleClick = React.useCallback(() => {\r\n header.column.resetSize();\r\n }, [header.column]);\r\n\r\n return (\r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type {\n ColumnSort,\n Header,\n SortDirection,\n SortingState,\n Table,\n} from \"@tanstack/react-table\";\nimport {\n BaselineIcon,\n CalendarIcon,\n CheckSquareIcon,\n ChevronDownIcon,\n ChevronUpIcon,\n EyeOffIcon,\n HashIcon,\n ListChecksIcon,\n ListIcon,\n PinIcon,\n PinOffIcon,\n TextInitialIcon,\n XIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport type { Cell } from \"@/types/data-grid\";\n\nfunction getColumnVariant(variant?: Cell[\"variant\"]): {\n icon: React.ComponentType>;\n label: string;\n} | null {\n switch (variant) {\n case \"short-text\":\n return { icon: BaselineIcon, label: \"Short text\" };\n case \"long-text\":\n return { icon: TextInitialIcon, label: \"Long text\" };\n case \"number\":\n return { icon: HashIcon, label: \"Number\" };\n case \"select\":\n return { icon: ListIcon, label: \"Select\" };\n case \"multi-select\":\n return { icon: ListChecksIcon, label: \"Multi-select\" };\n case \"checkbox\":\n return { icon: CheckSquareIcon, label: \"Checkbox\" };\n case \"date\":\n return { icon: CalendarIcon, label: \"Date\" };\n default:\n return null;\n }\n}\n\ninterface DataGridColumnHeaderProps\n extends React.ComponentProps {\n header: Header;\n table: Table;\n}\n\nexport function DataGridColumnHeader({\n header,\n table,\n className,\n onPointerDown,\n ...props\n}: DataGridColumnHeaderProps) {\n const column = header.column;\n const label = column.columnDef.meta?.label\n ? column.columnDef.meta.label\n : typeof column.columnDef.header === \"string\"\n ? column.columnDef.header\n : column.id;\n\n const isAnyColumnResizing =\n table.getState().columnSizingInfo.isResizingColumn;\n\n const cellVariant = column.columnDef.meta?.cell;\n const columnVariant = getColumnVariant(cellVariant?.variant);\n\n const pinnedPosition = column.getIsPinned();\n const isPinnedLeft = pinnedPosition === \"left\";\n const isPinnedRight = pinnedPosition === \"right\";\n\n const onSortingChange = React.useCallback(\n (direction: SortDirection) => {\n table.setSorting((prev: SortingState) => {\n const existingSortIndex = prev.findIndex(\n (sort) => sort.id === column.id,\n );\n const newSort: ColumnSort = {\n id: column.id,\n desc: direction === \"desc\",\n };\n\n if (existingSortIndex >= 0) {\n const updated = [...prev];\n updated[existingSortIndex] = newSort;\n return updated;\n } else {\n return [...prev, newSort];\n }\n });\n },\n [column.id, table],\n );\n\n const onSortRemove = React.useCallback(() => {\n table.setSorting((prev: SortingState) =>\n prev.filter((sort) => sort.id !== column.id),\n );\n }, [column.id, table]);\n\n const onLeftPin = React.useCallback(() => {\n column.pin(\"left\");\n }, [column]);\n\n const onRightPin = React.useCallback(() => {\n column.pin(\"right\");\n }, [column]);\n\n const onUnpin = React.useCallback(() => {\n column.pin(false);\n }, [column]);\n\n const onTriggerPointerDown = React.useCallback(\n (event: React.PointerEvent) => {\n onPointerDown?.(event);\n if (event.defaultPrevented) return;\n\n if (event.button !== 0) {\n return;\n }\n table.options.meta?.onColumnClick?.(column.id);\n },\n [table.options.meta, column.id, onPointerDown],\n );\n\n return (\n <>\n \n \n
\n {columnVariant && (\n \n \n \n \n \n

{columnVariant.label}

\n
\n
\n )}\n {label}\n
\n \n \n \n {column.getCanSort() && (\n <>\n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"asc\"}\n onClick={() => onSortingChange(\"asc\")}\n >\n \n Sort asc\n \n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"desc\"}\n onClick={() => onSortingChange(\"desc\")}\n >\n \n Sort desc\n \n {column.getIsSorted() && (\n \n \n Remove sort\n \n )}\n \n )}\n {column.getCanPin() && (\n <>\n {column.getCanSort() && }\n\n {isPinnedLeft ? (\n \n \n Unpin from left\n \n ) : (\n \n \n Pin to left\n \n )}\n {isPinnedRight ? (\n \n \n Unpin from right\n \n ) : (\n \n \n Pin to right\n \n )}\n \n )}\n {column.getCanHide() && (\n <>\n \n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={!column.getIsVisible()}\n onClick={() => column.toggleVisibility(false)}\n >\n \n Hide column\n \n \n )}\n \n
\n {header.column.getCanResize() && (\n \n )}\n \n );\n}\n\nconst DataGridColumnResizer = React.memo(\n DataGridColumnResizerImpl,\n (prev, next) => {\n const prevColumn = prev.header.column;\n const nextColumn = next.header.column;\n\n if (\n prevColumn.getIsResizing() !== nextColumn.getIsResizing() ||\n prevColumn.getSize() !== nextColumn.getSize()\n ) {\n return false;\n }\n\n if (prev.label !== next.label) return false;\n\n return true;\n },\n) as typeof DataGridColumnResizerImpl;\n\ninterface DataGridColumnResizerProps\n extends DataGridColumnHeaderProps {\n label: string;\n}\n\nfunction DataGridColumnResizerImpl({\n header,\n table,\n label,\n}: DataGridColumnResizerProps) {\n const defaultColumnDef = table._getDefaultColumnDef();\n\n const onDoubleClick = React.useCallback(() => {\n header.column.resetSize();\n }, [header.column]);\n\n return (\n \n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-column-header.tsx" }, { "path": "src/components/data-grid/data-grid-context-menu.tsx", - "content": "\"use client\";\r\n\r\nimport type { Table, TableMeta } from \"@tanstack/react-table\";\r\nimport { CopyIcon, EraserIcon } from \"lucide-react\";\r\nimport * as React from \"react\";\r\nimport { toast } from \"sonner\";\r\nimport {\r\n DropdownMenu,\r\n DropdownMenuContent,\r\n DropdownMenuItem,\r\n DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport { parseCellKey } from \"@/lib/data-grid\";\r\nimport type { UpdateCell } from \"@/types/data-grid\";\r\n\r\ninterface DataGridContextMenuProps {\r\n table: Table;\r\n}\r\n\r\nexport function DataGridContextMenu({\r\n table,\r\n}: DataGridContextMenuProps) {\r\n const meta = table.options.meta;\r\n const contextMenu = meta?.contextMenu;\r\n const onContextMenuOpenChange = meta?.onContextMenuOpenChange;\r\n const selectionState = meta?.selectionState;\r\n const dataGridRef = meta?.dataGridRef;\r\n const onDataUpdate = meta?.onDataUpdate;\r\n\r\n if (!contextMenu) return null;\r\n\r\n return (\r\n \r\n );\r\n}\r\n\r\ninterface ContextMenuProps\r\n extends Pick<\r\n TableMeta,\r\n | \"dataGridRef\"\r\n | \"onContextMenuOpenChange\"\r\n | \"selectionState\"\r\n | \"onDataUpdate\"\r\n >,\r\n Required, \"contextMenu\">> {\r\n table: Table;\r\n}\r\n\r\nconst ContextMenu = React.memo(ContextMenuImpl, (prev, next) => {\r\n if (prev.contextMenu.open !== next.contextMenu.open) return false;\r\n if (!next.contextMenu.open) return true;\r\n if (prev.contextMenu.x !== next.contextMenu.x) return false;\r\n if (prev.contextMenu.y !== next.contextMenu.y) return false;\r\n\r\n const prevSize = prev.selectionState?.selectedCells?.size ?? 0;\r\n const nextSize = next.selectionState?.selectedCells?.size ?? 0;\r\n if (prevSize !== nextSize) return false;\r\n\r\n return true;\r\n}) as typeof ContextMenuImpl;\r\n\r\nfunction ContextMenuImpl({\r\n table,\r\n dataGridRef,\r\n contextMenu,\r\n onContextMenuOpenChange,\r\n selectionState,\r\n onDataUpdate,\r\n}: ContextMenuProps) {\r\n const triggerStyle = React.useMemo(\r\n () => ({\r\n position: \"fixed\",\r\n left: `${contextMenu.x}px`,\r\n top: `${contextMenu.y}px`,\r\n width: \"1px\",\r\n height: \"1px\",\r\n padding: 0,\r\n margin: 0,\r\n border: \"none\",\r\n background: \"transparent\",\r\n pointerEvents: \"none\",\r\n opacity: 0,\r\n }),\r\n [contextMenu.x, contextMenu.y],\r\n );\r\n\r\n const onCloseAutoFocus: NonNullable<\r\n React.ComponentProps[\"onCloseAutoFocus\"]\r\n > = React.useCallback(\r\n (event) => {\r\n event.preventDefault();\r\n dataGridRef?.current?.focus();\r\n },\r\n [dataGridRef],\r\n );\r\n\r\n const onCopy = React.useCallback(() => {\r\n if (\r\n !selectionState?.selectedCells ||\r\n selectionState.selectedCells.size === 0\r\n )\r\n return;\r\n\r\n const rows = table.getRowModel().rows;\r\n const columnIds: string[] = [];\r\n\r\n // Collect all unique column IDs from selected cells\r\n const selectedCellsArray = Array.from(selectionState.selectedCells);\r\n for (const cellKey of selectedCellsArray) {\r\n const { columnId } = parseCellKey(cellKey);\r\n if (columnId && !columnIds.includes(columnId)) {\r\n columnIds.push(columnId);\r\n }\r\n }\r\n\r\n // Build grid of selected cells\r\n const cellData = new Map();\r\n for (const cellKey of selectedCellsArray) {\r\n const { rowIndex, columnId } = parseCellKey(cellKey);\r\n const row = rows[rowIndex];\r\n if (row) {\r\n const cell = row\r\n .getVisibleCells()\r\n .find((c) => c.column.id === columnId);\r\n if (cell) {\r\n const value = cell.getValue();\r\n cellData.set(cellKey, String(value ?? \"\"));\r\n }\r\n }\r\n }\r\n\r\n // Get row indices and column indices\r\n const rowIndices = new Set();\r\n const colIndices = new Set();\r\n\r\n for (const cellKey of selectedCellsArray) {\r\n const { rowIndex, columnId } = parseCellKey(cellKey);\r\n rowIndices.add(rowIndex);\r\n const colIndex = columnIds.indexOf(columnId);\r\n if (colIndex >= 0) {\r\n colIndices.add(colIndex);\r\n }\r\n }\r\n\r\n const sortedRowIndices = Array.from(rowIndices).sort((a, b) => a - b);\r\n const sortedColIndices = Array.from(colIndices).sort((a, b) => a - b);\r\n const sortedColumnIds = sortedColIndices.map((i) => columnIds[i]);\r\n\r\n // Build TSV (tab-separated values)\r\n const tsvData = sortedRowIndices\r\n .map((rowIndex) =>\r\n sortedColumnIds\r\n .map((columnId) => {\r\n const cellKey = `${rowIndex}:${columnId}`;\r\n return cellData.get(cellKey) ?? \"\";\r\n })\r\n .join(\"\\t\"),\r\n )\r\n .join(\"\\n\");\r\n\r\n navigator.clipboard.writeText(tsvData);\r\n toast.success(\r\n `${selectionState.selectedCells.size} cell${selectionState.selectedCells.size !== 1 ? \"s\" : \"\"} copied`,\r\n );\r\n }, [table, selectionState]);\r\n\r\n const onClear = React.useCallback(() => {\r\n if (\r\n !selectionState?.selectedCells ||\r\n selectionState.selectedCells.size === 0\r\n )\r\n return;\r\n\r\n const updates: Array = [];\r\n\r\n for (const cellKey of selectionState.selectedCells) {\r\n const { rowIndex, columnId } = parseCellKey(cellKey);\r\n updates.push({ rowIndex, columnId, value: \"\" });\r\n }\r\n\r\n onDataUpdate?.(updates);\r\n\r\n toast.success(\r\n `${updates.length} cell${updates.length !== 1 ? \"s\" : \"\"} cleared`,\r\n );\r\n }, [onDataUpdate, selectionState]);\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n \r\n Copy\r\n \r\n \r\n \r\n Clear\r\n \r\n \r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Table, TableMeta } from \"@tanstack/react-table\";\nimport { CopyIcon, EraserIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { parseCellKey } from \"@/lib/data-grid\";\nimport type { UpdateCell } from \"@/types/data-grid\";\n\ninterface DataGridContextMenuProps {\n table: Table;\n}\n\nexport function DataGridContextMenu({\n table,\n}: DataGridContextMenuProps) {\n const meta = table.options.meta;\n const contextMenu = meta?.contextMenu;\n const onContextMenuOpenChange = meta?.onContextMenuOpenChange;\n const selectionState = meta?.selectionState;\n const dataGridRef = meta?.dataGridRef;\n const onDataUpdate = meta?.onDataUpdate;\n\n if (!contextMenu) return null;\n\n return (\n \n );\n}\n\ninterface ContextMenuProps\n extends Pick<\n TableMeta,\n | \"dataGridRef\"\n | \"onContextMenuOpenChange\"\n | \"selectionState\"\n | \"onDataUpdate\"\n >,\n Required, \"contextMenu\">> {\n table: Table;\n}\n\nconst ContextMenu = React.memo(ContextMenuImpl, (prev, next) => {\n if (prev.contextMenu.open !== next.contextMenu.open) return false;\n if (!next.contextMenu.open) return true;\n if (prev.contextMenu.x !== next.contextMenu.x) return false;\n if (prev.contextMenu.y !== next.contextMenu.y) return false;\n\n const prevSize = prev.selectionState?.selectedCells?.size ?? 0;\n const nextSize = next.selectionState?.selectedCells?.size ?? 0;\n if (prevSize !== nextSize) return false;\n\n return true;\n}) as typeof ContextMenuImpl;\n\nfunction ContextMenuImpl({\n table,\n dataGridRef,\n contextMenu,\n onContextMenuOpenChange,\n selectionState,\n onDataUpdate,\n}: ContextMenuProps) {\n const triggerStyle = React.useMemo(\n () => ({\n position: \"fixed\",\n left: `${contextMenu.x}px`,\n top: `${contextMenu.y}px`,\n width: \"1px\",\n height: \"1px\",\n padding: 0,\n margin: 0,\n border: \"none\",\n background: \"transparent\",\n pointerEvents: \"none\",\n opacity: 0,\n }),\n [contextMenu.x, contextMenu.y],\n );\n\n const onCloseAutoFocus: NonNullable<\n React.ComponentProps[\"onCloseAutoFocus\"]\n > = React.useCallback(\n (event) => {\n event.preventDefault();\n dataGridRef?.current?.focus();\n },\n [dataGridRef],\n );\n\n const onCopy = React.useCallback(() => {\n if (\n !selectionState?.selectedCells ||\n selectionState.selectedCells.size === 0\n )\n return;\n\n const rows = table.getRowModel().rows;\n const columnIds: string[] = [];\n\n // Collect all unique column IDs from selected cells\n const selectedCellsArray = Array.from(selectionState.selectedCells);\n for (const cellKey of selectedCellsArray) {\n const { columnId } = parseCellKey(cellKey);\n if (columnId && !columnIds.includes(columnId)) {\n columnIds.push(columnId);\n }\n }\n\n // Build grid of selected cells\n const cellData = new Map();\n for (const cellKey of selectedCellsArray) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n const row = rows[rowIndex];\n if (row) {\n const cell = row\n .getVisibleCells()\n .find((c) => c.column.id === columnId);\n if (cell) {\n const value = cell.getValue();\n cellData.set(cellKey, String(value ?? \"\"));\n }\n }\n }\n\n // Get row indices and column indices\n const rowIndices = new Set();\n const colIndices = new Set();\n\n for (const cellKey of selectedCellsArray) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n rowIndices.add(rowIndex);\n const colIndex = columnIds.indexOf(columnId);\n if (colIndex >= 0) {\n colIndices.add(colIndex);\n }\n }\n\n const sortedRowIndices = Array.from(rowIndices).sort((a, b) => a - b);\n const sortedColIndices = Array.from(colIndices).sort((a, b) => a - b);\n const sortedColumnIds = sortedColIndices.map((i) => columnIds[i]);\n\n // Build TSV (tab-separated values)\n const tsvData = sortedRowIndices\n .map((rowIndex) =>\n sortedColumnIds\n .map((columnId) => {\n const cellKey = `${rowIndex}:${columnId}`;\n return cellData.get(cellKey) ?? \"\";\n })\n .join(\"\\t\"),\n )\n .join(\"\\n\");\n\n navigator.clipboard.writeText(tsvData);\n toast.success(\n `${selectionState.selectedCells.size} cell${selectionState.selectedCells.size !== 1 ? \"s\" : \"\"} copied`,\n );\n }, [table, selectionState]);\n\n const onClear = React.useCallback(() => {\n if (\n !selectionState?.selectedCells ||\n selectionState.selectedCells.size === 0\n )\n return;\n\n const updates: Array = [];\n\n for (const cellKey of selectionState.selectedCells) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n updates.push({ rowIndex, columnId, value: \"\" });\n }\n\n onDataUpdate?.(updates);\n\n toast.success(\n `${updates.length} cell${updates.length !== 1 ? \"s\" : \"\"} cleared`,\n );\n }, [onDataUpdate, selectionState]);\n\n return (\n \n \n \n \n \n Copy\n \n \n \n Clear\n \n \n \n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-context-menu.tsx" }, { "path": "src/components/data-grid/data-grid-keyboard-shortcuts.tsx", - "content": "\"use client\";\r\n\r\nimport { SearchIcon, XIcon } from \"lucide-react\";\r\nimport * as React from \"react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Dialog,\r\n DialogClose,\r\n DialogContent,\r\n DialogDescription,\r\n DialogHeader,\r\n DialogTitle,\r\n} from \"@/components/ui/dialog\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Kbd, KbdGroup } from \"@/components/ui/kbd\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\n\r\nconst SHORTCUT_KEY = \"/\";\r\n\r\ninterface ShortcutGroup {\r\n title: string;\r\n shortcuts: Array<{\r\n keys: string[];\r\n description: string;\r\n }>;\r\n}\r\n\r\ninterface DataGridKeyboardShortcutsProps {\r\n enableSearch?: boolean;\r\n}\r\n\r\nexport const DataGridKeyboardShortcuts = React.memo(\r\n DataGridKeyboardShortcutsImpl,\r\n (prev, next) => {\r\n return prev.enableSearch === next.enableSearch;\r\n },\r\n);\r\n\r\nfunction DataGridKeyboardShortcutsImpl({\r\n enableSearch = false,\r\n}: DataGridKeyboardShortcutsProps) {\r\n const [open, setOpen] = React.useState(false);\r\n const [input, setInput] = React.useState(\"\");\r\n const inputRef = React.useRef(null);\r\n\r\n const isMac =\r\n typeof navigator !== \"undefined\"\r\n ? /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)\r\n : false;\r\n\r\n const modKey = isMac ? \"⌘\" : \"Ctrl\";\r\n\r\n const onOpenChange = React.useCallback((isOpen: boolean) => {\r\n setOpen(isOpen);\r\n if (!isOpen) {\r\n setInput(\"\");\r\n }\r\n }, []);\r\n\r\n const onOpenAutoFocus = React.useCallback((event: Event) => {\r\n event.preventDefault();\r\n inputRef.current?.focus();\r\n }, []);\r\n\r\n const onInputChange = React.useCallback(\r\n (event: React.ChangeEvent) => {\r\n setInput(event.target.value);\r\n },\r\n [],\r\n );\r\n\r\n const shortcutGroups: ShortcutGroup[] = React.useMemo(\r\n () => [\r\n {\r\n title: \"Navigation\",\r\n shortcuts: [\r\n {\r\n keys: [\"↑\", \"↓\", \"←\", \"→\"],\r\n description: \"Navigate between cells\",\r\n },\r\n {\r\n keys: [\"Tab\"],\r\n description: \"Move to next cell\",\r\n },\r\n {\r\n keys: [\"Shift\", \"Tab\"],\r\n description: \"Move to previous cell\",\r\n },\r\n {\r\n keys: [\"Home\"],\r\n description: \"Move to first column\",\r\n },\r\n {\r\n keys: [\"End\"],\r\n description: \"Move to last column\",\r\n },\r\n {\r\n keys: [modKey, \"Home\"],\r\n description: \"Move to first cell\",\r\n },\r\n {\r\n keys: [modKey, \"End\"],\r\n description: \"Move to last cell\",\r\n },\r\n {\r\n keys: [\"PgUp\"],\r\n description: \"Move up one page\",\r\n },\r\n {\r\n keys: [\"PgDn\"],\r\n description: \"Move down one page\",\r\n },\r\n ],\r\n },\r\n {\r\n title: \"Selection\",\r\n shortcuts: [\r\n {\r\n keys: [\"Shift\", \"↑↓←→\"],\r\n description: \"Extend selection\",\r\n },\r\n {\r\n keys: [modKey, \"A\"],\r\n description: \"Select all cells\",\r\n },\r\n {\r\n keys: [modKey, \"Click\"],\r\n description: \"Toggle cell selection\",\r\n },\r\n {\r\n keys: [\"Shift\", \"Click\"],\r\n description: \"Select range\",\r\n },\r\n {\r\n keys: [\"Esc\"],\r\n description: \"Clear selection\",\r\n },\r\n ],\r\n },\r\n {\r\n title: \"Editing\",\r\n shortcuts: [\r\n {\r\n keys: [\"Enter\"],\r\n description: \"Start editing cell\",\r\n },\r\n {\r\n keys: [\"Double Click\"],\r\n description: \"Start editing cell\",\r\n },\r\n {\r\n keys: [\"Delete\"],\r\n description: \"Clear selected cells\",\r\n },\r\n {\r\n keys: [\"Backspace\"],\r\n description: \"Clear selected cells\",\r\n },\r\n ],\r\n },\r\n ...(enableSearch\r\n ? [\r\n {\r\n title: \"Search\",\r\n shortcuts: [\r\n {\r\n keys: [modKey, \"F\"],\r\n description: \"Open search\",\r\n },\r\n {\r\n keys: [\"Enter\"],\r\n description: \"Next match\",\r\n },\r\n {\r\n keys: [\"Shift\", \"Enter\"],\r\n description: \"Previous match\",\r\n },\r\n {\r\n keys: [\"Esc\"],\r\n description: \"Close search\",\r\n },\r\n ],\r\n },\r\n ]\r\n : []),\r\n {\r\n title: \"Sorting\",\r\n shortcuts: [\r\n {\r\n keys: [modKey, \"Shift\", \"S\"],\r\n description: \"Toggle the sort menu\",\r\n },\r\n {\r\n keys: [\"Backspace\"],\r\n description: \"Remove sort (when focused)\",\r\n },\r\n {\r\n keys: [\"Delete\"],\r\n description: \"Remove sort (when focused)\",\r\n },\r\n ],\r\n },\r\n {\r\n title: \"General\",\r\n shortcuts: [\r\n {\r\n keys: [modKey, \"/\"],\r\n description: \"Show keyboard shortcuts\",\r\n },\r\n ],\r\n },\r\n ],\r\n [modKey, enableSearch],\r\n );\r\n\r\n const filteredGroups = React.useMemo(() => {\r\n if (!input.trim()) return shortcutGroups;\r\n\r\n const query = input.toLowerCase();\r\n return shortcutGroups\r\n .map((group) => ({\r\n ...group,\r\n shortcuts: group.shortcuts.filter(\r\n (shortcut) =>\r\n shortcut.description.toLowerCase().includes(query) ||\r\n shortcut.keys.some((key) => key.toLowerCase().includes(query)),\r\n ),\r\n }))\r\n .filter((group) => group.shortcuts.length > 0);\r\n }, [shortcutGroups, input]);\r\n\r\n React.useEffect(() => {\r\n function onKeyDown(event: KeyboardEvent) {\r\n if ((event.ctrlKey || event.metaKey) && event.key === SHORTCUT_KEY) {\r\n event.preventDefault();\r\n setOpen(true);\r\n }\r\n }\r\n\r\n window.addEventListener(\"keydown\", onKeyDown);\r\n return () => {\r\n window.removeEventListener(\"keydown\", onKeyDown);\r\n };\r\n }, []);\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n \r\n \r\n Keyboard shortcuts\r\n \r\n Use these keyboard shortcuts to navigate and interact with the data\r\n grid more efficiently.\r\n \r\n \r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n \r\n
\r\n {filteredGroups.length === 0 ? (\r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n No shortcuts found\r\n
\r\n

\r\n Try searching for a different term.\r\n

\r\n
\r\n
\r\n ) : (\r\n
\r\n {filteredGroups.map((shortcutGroup) => (\r\n
\r\n

\r\n {shortcutGroup.title}\r\n

\r\n
\r\n {shortcutGroup.shortcuts.map((shortcut, index) => (\r\n \r\n ))}\r\n
\r\n
\r\n ))}\r\n
\r\n )}\r\n
\r\n \r\n
\r\n );\r\n}\r\n\r\nfunction ShortcutCard({\r\n keys,\r\n description,\r\n}: ShortcutGroup[\"shortcuts\"][number]) {\r\n return (\r\n
\r\n {description}\r\n \r\n {keys.map((key, index) => (\r\n \r\n {index > 0 && (\r\n +\r\n )}\r\n {key}\r\n \r\n ))}\r\n \r\n
\r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport { SearchIcon, XIcon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Kbd, KbdGroup } from \"@/components/ui/kbd\";\nimport { Separator } from \"@/components/ui/separator\";\n\nconst SHORTCUT_KEY = \"/\";\n\ninterface ShortcutGroup {\n title: string;\n shortcuts: Array<{\n keys: string[];\n description: string;\n }>;\n}\n\ninterface DataGridKeyboardShortcutsProps {\n enableSearch?: boolean;\n}\n\nexport const DataGridKeyboardShortcuts = React.memo(\n DataGridKeyboardShortcutsImpl,\n (prev, next) => {\n return prev.enableSearch === next.enableSearch;\n },\n);\n\nfunction DataGridKeyboardShortcutsImpl({\n enableSearch = false,\n}: DataGridKeyboardShortcutsProps) {\n const [open, setOpen] = React.useState(false);\n const [input, setInput] = React.useState(\"\");\n const inputRef = React.useRef(null);\n\n const isMac =\n typeof navigator !== \"undefined\"\n ? /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)\n : false;\n\n const modKey = isMac ? \"⌘\" : \"Ctrl\";\n\n const onOpenChange = React.useCallback((isOpen: boolean) => {\n setOpen(isOpen);\n if (!isOpen) {\n setInput(\"\");\n }\n }, []);\n\n const onOpenAutoFocus = React.useCallback((event: Event) => {\n event.preventDefault();\n inputRef.current?.focus();\n }, []);\n\n const onInputChange = React.useCallback(\n (event: React.ChangeEvent) => {\n setInput(event.target.value);\n },\n [],\n );\n\n const shortcutGroups: ShortcutGroup[] = React.useMemo(\n () => [\n {\n title: \"Navigation\",\n shortcuts: [\n {\n keys: [\"↑\", \"↓\", \"←\", \"→\"],\n description: \"Navigate between cells\",\n },\n {\n keys: [\"Tab\"],\n description: \"Move to next cell\",\n },\n {\n keys: [\"Shift\", \"Tab\"],\n description: \"Move to previous cell\",\n },\n {\n keys: [\"Home\"],\n description: \"Move to first column\",\n },\n {\n keys: [\"End\"],\n description: \"Move to last column\",\n },\n {\n keys: [modKey, \"Home\"],\n description: \"Move to first cell\",\n },\n {\n keys: [modKey, \"End\"],\n description: \"Move to last cell\",\n },\n {\n keys: [\"PgUp\"],\n description: \"Move up one page\",\n },\n {\n keys: [\"PgDn\"],\n description: \"Move down one page\",\n },\n ],\n },\n {\n title: \"Selection\",\n shortcuts: [\n {\n keys: [\"Shift\", \"↑↓←→\"],\n description: \"Extend selection\",\n },\n {\n keys: [modKey, \"A\"],\n description: \"Select all cells\",\n },\n {\n keys: [modKey, \"Click\"],\n description: \"Toggle cell selection\",\n },\n {\n keys: [\"Shift\", \"Click\"],\n description: \"Select range\",\n },\n {\n keys: [\"Esc\"],\n description: \"Clear selection\",\n },\n ],\n },\n {\n title: \"Editing\",\n shortcuts: [\n {\n keys: [\"Enter\"],\n description: \"Start editing cell\",\n },\n {\n keys: [\"Double Click\"],\n description: \"Start editing cell\",\n },\n {\n keys: [\"Delete\"],\n description: \"Clear selected cells\",\n },\n {\n keys: [\"Backspace\"],\n description: \"Clear selected cells\",\n },\n ],\n },\n ...(enableSearch\n ? [\n {\n title: \"Search\",\n shortcuts: [\n {\n keys: [modKey, \"F\"],\n description: \"Open search\",\n },\n {\n keys: [\"Enter\"],\n description: \"Next match\",\n },\n {\n keys: [\"Shift\", \"Enter\"],\n description: \"Previous match\",\n },\n {\n keys: [\"Esc\"],\n description: \"Close search\",\n },\n ],\n },\n ]\n : []),\n {\n title: \"Sorting\",\n shortcuts: [\n {\n keys: [modKey, \"Shift\", \"S\"],\n description: \"Toggle the sort menu\",\n },\n {\n keys: [\"Backspace\"],\n description: \"Remove sort (when focused)\",\n },\n {\n keys: [\"Delete\"],\n description: \"Remove sort (when focused)\",\n },\n ],\n },\n {\n title: \"General\",\n shortcuts: [\n {\n keys: [modKey, \"/\"],\n description: \"Show keyboard shortcuts\",\n },\n ],\n },\n ],\n [modKey, enableSearch],\n );\n\n const filteredGroups = React.useMemo(() => {\n if (!input.trim()) return shortcutGroups;\n\n const query = input.toLowerCase();\n return shortcutGroups\n .map((group) => ({\n ...group,\n shortcuts: group.shortcuts.filter(\n (shortcut) =>\n shortcut.description.toLowerCase().includes(query) ||\n shortcut.keys.some((key) => key.toLowerCase().includes(query)),\n ),\n }))\n .filter((group) => group.shortcuts.length > 0);\n }, [shortcutGroups, input]);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if ((event.ctrlKey || event.metaKey) && event.key === SHORTCUT_KEY) {\n event.preventDefault();\n setOpen(true);\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => {\n window.removeEventListener(\"keydown\", onKeyDown);\n };\n }, []);\n\n return (\n \n \n \n \n \n \n Keyboard shortcuts\n \n Use these keyboard shortcuts to navigate and interact with the data\n grid more efficiently.\n \n \n
\n
\n \n \n
\n
\n \n
\n {filteredGroups.length === 0 ? (\n
\n
\n \n
\n
\n
\n No shortcuts found\n
\n

\n Try searching for a different term.\n

\n
\n
\n ) : (\n
\n {filteredGroups.map((shortcutGroup) => (\n
\n

\n {shortcutGroup.title}\n

\n
\n {shortcutGroup.shortcuts.map((shortcut, index) => (\n \n ))}\n
\n
\n ))}\n
\n )}\n
\n \n
\n );\n}\n\nfunction ShortcutCard({\n keys,\n description,\n}: ShortcutGroup[\"shortcuts\"][number]) {\n return (\n
\n {description}\n \n {keys.map((key, index) => (\n \n {index > 0 && (\n +\n )}\n {key}\n \n ))}\n \n
\n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-keyboard-shortcuts.tsx" }, { "path": "src/components/data-grid/data-grid-row.tsx", - "content": "\"use client\";\r\n\r\nimport { flexRender, type Row } from \"@tanstack/react-table\";\r\nimport type { Virtualizer } from \"@tanstack/react-virtual\";\r\nimport * as React from \"react\";\r\nimport { useComposedRefs } from \"@/lib/compose-refs\";\r\nimport { getRowHeightValue } from \"@/lib/data-grid\";\r\nimport { getCommonPinningStyles } from \"@/lib/data-table\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport type { CellPosition, RowHeightValue } from \"@/types/data-grid\";\r\n\r\ninterface DataGridRowProps extends React.ComponentProps<\"div\"> {\r\n row: Row;\r\n rowVirtualizer: Virtualizer;\r\n virtualRowIndex: number;\r\n rowMapRef: React.RefObject>;\r\n rowHeight: RowHeightValue;\r\n focusedCell: CellPosition | null;\r\n}\r\n\r\nexport const DataGridRow = React.memo(DataGridRowImpl, (prev, next) => {\r\n if (prev.row.id !== next.row.id) {\r\n return false;\r\n }\r\n\r\n const prevRowIndex = prev.virtualRowIndex;\r\n const nextRowIndex = next.virtualRowIndex;\r\n\r\n const prevHasFocus = prev.focusedCell?.rowIndex === prevRowIndex;\r\n const nextHasFocus = next.focusedCell?.rowIndex === nextRowIndex;\r\n\r\n if (prevHasFocus !== nextHasFocus) {\r\n return false;\r\n }\r\n\r\n if (nextHasFocus && prevHasFocus) {\r\n const prevFocusedCol = prev.focusedCell?.columnId;\r\n const nextFocusedCol = next.focusedCell?.columnId;\r\n if (prevFocusedCol !== nextFocusedCol) {\r\n return false;\r\n }\r\n }\r\n\r\n if (next.rowVirtualizer.isScrolling) {\r\n return true;\r\n }\r\n\r\n return false;\r\n}) as typeof DataGridRowImpl;\r\n\r\nfunction DataGridRowImpl({\r\n row,\r\n virtualRowIndex,\r\n rowVirtualizer,\r\n rowMapRef,\r\n rowHeight,\r\n focusedCell,\r\n ref,\r\n className,\r\n ...props\r\n}: DataGridRowProps) {\r\n const rowRef = useComposedRefs(ref, (node) => {\r\n if (node && typeof virtualRowIndex !== \"undefined\") {\r\n rowVirtualizer.measureElement(node);\r\n rowMapRef.current.set(virtualRowIndex, node);\r\n }\r\n });\r\n\r\n const isRowSelected = row.getIsSelected();\r\n\r\n return (\r\n \r\n {row.getVisibleCells().map((cell, colIndex) => {\r\n const isCellFocused =\r\n focusedCell?.rowIndex === virtualRowIndex &&\r\n focusedCell?.columnId === cell.column.id;\r\n\r\n return (\r\n \r\n {typeof cell.column.columnDef.header === \"function\" ? (\r\n \r\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\r\n \r\n ) : (\r\n flexRender(cell.column.columnDef.cell, cell.getContext())\r\n )}\r\n \r\n );\r\n })}\r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport { flexRender, type Row } from \"@tanstack/react-table\";\nimport type { Virtualizer } from \"@tanstack/react-virtual\";\nimport * as React from \"react\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { getRowHeightValue } from \"@/lib/data-grid\";\nimport { getCommonPinningStyles } from \"@/lib/data-table\";\nimport { cn } from \"@/lib/utils\";\nimport type { CellPosition, RowHeightValue } from \"@/types/data-grid\";\n\ninterface DataGridRowProps extends React.ComponentProps<\"div\"> {\n row: Row;\n rowVirtualizer: Virtualizer;\n virtualRowIndex: number;\n rowMapRef: React.RefObject>;\n rowHeight: RowHeightValue;\n focusedCell: CellPosition | null;\n}\n\nexport const DataGridRow = React.memo(DataGridRowImpl, (prev, next) => {\n if (prev.row.id !== next.row.id) {\n return false;\n }\n\n const prevRowIndex = prev.virtualRowIndex;\n const nextRowIndex = next.virtualRowIndex;\n\n const prevHasFocus = prev.focusedCell?.rowIndex === prevRowIndex;\n const nextHasFocus = next.focusedCell?.rowIndex === nextRowIndex;\n\n if (prevHasFocus !== nextHasFocus) {\n return false;\n }\n\n if (nextHasFocus && prevHasFocus) {\n const prevFocusedCol = prev.focusedCell?.columnId;\n const nextFocusedCol = next.focusedCell?.columnId;\n if (prevFocusedCol !== nextFocusedCol) {\n return false;\n }\n }\n\n if (next.rowVirtualizer.isScrolling) {\n return true;\n }\n\n return false;\n}) as typeof DataGridRowImpl;\n\nfunction DataGridRowImpl({\n row,\n virtualRowIndex,\n rowVirtualizer,\n rowMapRef,\n rowHeight,\n focusedCell,\n ref,\n className,\n ...props\n}: DataGridRowProps) {\n const rowRef = useComposedRefs(ref, (node) => {\n if (node && typeof virtualRowIndex !== \"undefined\") {\n rowVirtualizer.measureElement(node);\n rowMapRef.current.set(virtualRowIndex, node);\n }\n });\n\n const isRowSelected = row.getIsSelected();\n\n return (\n \n {row.getVisibleCells().map((cell, colIndex) => {\n const isCellFocused =\n focusedCell?.rowIndex === virtualRowIndex &&\n focusedCell?.columnId === cell.column.id;\n\n return (\n \n {typeof cell.column.columnDef.header === \"function\" ? (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ) : (\n flexRender(cell.column.columnDef.cell, cell.getContext())\n )}\n \n );\n })}\n \n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-row.tsx" }, { "path": "src/components/data-grid/data-grid-search.tsx", - "content": "\"use client\";\r\n\r\nimport { ChevronDown, ChevronUp, X } from \"lucide-react\";\r\nimport * as React from \"react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\r\nimport type { SearchState } from \"@/types/data-grid\";\r\n\r\ninterface DataGridSearchProps extends SearchState {}\r\n\r\nexport const DataGridSearch = React.memo(DataGridSearchImpl, (prev, next) => {\r\n if (prev.searchOpen !== next.searchOpen) return false;\r\n\r\n if (!next.searchOpen) return true;\r\n\r\n if (\r\n prev.searchQuery !== next.searchQuery ||\r\n prev.matchIndex !== next.matchIndex\r\n ) {\r\n return false;\r\n }\r\n\r\n if (prev.searchMatches.length !== next.searchMatches.length) return false;\r\n\r\n for (let i = 0; i < prev.searchMatches.length; i++) {\r\n const prevMatch = prev.searchMatches[i];\r\n const nextMatch = next.searchMatches[i];\r\n\r\n if (!prevMatch || !nextMatch) return false;\r\n\r\n if (\r\n prevMatch.rowIndex !== nextMatch.rowIndex ||\r\n prevMatch.columnId !== nextMatch.columnId\r\n ) {\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n});\r\n\r\nfunction DataGridSearchImpl({\r\n searchMatches,\r\n matchIndex,\r\n searchOpen,\r\n onSearchOpenChange,\r\n searchQuery,\r\n onSearchQueryChange,\r\n onSearch,\r\n onNavigateToNextMatch,\r\n onNavigateToPrevMatch,\r\n}: DataGridSearchProps) {\r\n const inputRef = React.useRef(null);\r\n\r\n React.useEffect(() => {\r\n if (searchOpen) {\r\n requestAnimationFrame(() => {\r\n inputRef.current?.focus();\r\n });\r\n }\r\n }, [searchOpen]);\r\n\r\n React.useEffect(() => {\r\n if (!searchOpen) return;\r\n\r\n function onEscape(event: KeyboardEvent) {\r\n if (event.key === \"Escape\") {\r\n event.preventDefault();\r\n onSearchOpenChange(false);\r\n }\r\n }\r\n\r\n document.addEventListener(\"keydown\", onEscape);\r\n return () => document.removeEventListener(\"keydown\", onEscape);\r\n }, [searchOpen, onSearchOpenChange]);\r\n\r\n const onKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n event.stopPropagation();\r\n\r\n if (event.key === \"Enter\") {\r\n event.preventDefault();\r\n if (event.shiftKey) {\r\n onNavigateToPrevMatch();\r\n } else {\r\n onNavigateToNextMatch();\r\n }\r\n }\r\n },\r\n [onNavigateToNextMatch, onNavigateToPrevMatch],\r\n );\r\n\r\n const debouncedSearch = useDebouncedCallback((query: string) => {\r\n onSearch(query);\r\n }, 150);\r\n\r\n const onChange = React.useCallback(\r\n (event: React.ChangeEvent) => {\r\n const value = event.target.value;\r\n onSearchQueryChange(value);\r\n debouncedSearch(value);\r\n },\r\n [onSearchQueryChange, debouncedSearch],\r\n );\r\n\r\n const onTriggerPointerDown = React.useCallback(\r\n (event: React.PointerEvent) => {\r\n // prevent implicit pointer capture\r\n const target = event.target;\r\n if (!(target instanceof HTMLElement)) return;\r\n if (target.hasPointerCapture(event.pointerId)) {\r\n target.releasePointerCapture(event.pointerId);\r\n }\r\n\r\n // Only prevent default if we're not clicking on the input\r\n // This allows text selection in the input while still preventing focus stealing elsewhere\r\n if (\r\n event.button === 0 &&\r\n event.ctrlKey === false &&\r\n event.pointerType === \"mouse\" &&\r\n !(event.target instanceof HTMLInputElement)\r\n ) {\r\n event.preventDefault();\r\n }\r\n },\r\n [],\r\n );\r\n\r\n const onPrevMatchPointerDown = React.useCallback(\r\n (event: React.PointerEvent) =>\r\n onTriggerPointerDown(event),\r\n [onTriggerPointerDown],\r\n );\r\n\r\n const onNextMatchPointerDown = React.useCallback(\r\n (event: React.PointerEvent) =>\r\n onTriggerPointerDown(event),\r\n [onTriggerPointerDown],\r\n );\r\n\r\n const onClose = React.useCallback(() => {\r\n onSearchOpenChange(false);\r\n }, [onSearchOpenChange]);\r\n\r\n if (!searchOpen) return null;\r\n\r\n return (\r\n \r\n
\r\n \r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n
\r\n
\r\n {searchMatches.length > 0 ? (\r\n \r\n {matchIndex + 1} of {searchMatches.length}\r\n \r\n ) : searchQuery ? (\r\n No results\r\n ) : (\r\n Type to search\r\n )}\r\n
\r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport { ChevronDown, ChevronUp, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport type { SearchState } from \"@/types/data-grid\";\n\ninterface DataGridSearchProps extends SearchState {}\n\nexport const DataGridSearch = React.memo(DataGridSearchImpl, (prev, next) => {\n if (prev.searchOpen !== next.searchOpen) return false;\n\n if (!next.searchOpen) return true;\n\n if (\n prev.searchQuery !== next.searchQuery ||\n prev.matchIndex !== next.matchIndex\n ) {\n return false;\n }\n\n if (prev.searchMatches.length !== next.searchMatches.length) return false;\n\n for (let i = 0; i < prev.searchMatches.length; i++) {\n const prevMatch = prev.searchMatches[i];\n const nextMatch = next.searchMatches[i];\n\n if (!prevMatch || !nextMatch) return false;\n\n if (\n prevMatch.rowIndex !== nextMatch.rowIndex ||\n prevMatch.columnId !== nextMatch.columnId\n ) {\n return false;\n }\n }\n\n return true;\n});\n\nfunction DataGridSearchImpl({\n searchMatches,\n matchIndex,\n searchOpen,\n onSearchOpenChange,\n searchQuery,\n onSearchQueryChange,\n onSearch,\n onNavigateToNextMatch,\n onNavigateToPrevMatch,\n}: DataGridSearchProps) {\n const inputRef = React.useRef(null);\n\n React.useEffect(() => {\n if (searchOpen) {\n requestAnimationFrame(() => {\n inputRef.current?.focus();\n });\n }\n }, [searchOpen]);\n\n React.useEffect(() => {\n if (!searchOpen) return;\n\n function onEscape(event: KeyboardEvent) {\n if (event.key === \"Escape\") {\n event.preventDefault();\n onSearchOpenChange(false);\n }\n }\n\n document.addEventListener(\"keydown\", onEscape);\n return () => document.removeEventListener(\"keydown\", onEscape);\n }, [searchOpen, onSearchOpenChange]);\n\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n event.stopPropagation();\n\n if (event.key === \"Enter\") {\n event.preventDefault();\n if (event.shiftKey) {\n onNavigateToPrevMatch();\n } else {\n onNavigateToNextMatch();\n }\n }\n },\n [onNavigateToNextMatch, onNavigateToPrevMatch],\n );\n\n const debouncedSearch = useDebouncedCallback((query: string) => {\n onSearch(query);\n }, 150);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const value = event.target.value;\n onSearchQueryChange(value);\n debouncedSearch(value);\n },\n [onSearchQueryChange, debouncedSearch],\n );\n\n const onTriggerPointerDown = React.useCallback(\n (event: React.PointerEvent) => {\n // prevent implicit pointer capture\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n if (target.hasPointerCapture(event.pointerId)) {\n target.releasePointerCapture(event.pointerId);\n }\n\n // Only prevent default if we're not clicking on the input\n // This allows text selection in the input while still preventing focus stealing elsewhere\n if (\n event.button === 0 &&\n event.ctrlKey === false &&\n event.pointerType === \"mouse\" &&\n !(event.target instanceof HTMLInputElement)\n ) {\n event.preventDefault();\n }\n },\n [],\n );\n\n const onPrevMatchPointerDown = React.useCallback(\n (event: React.PointerEvent) =>\n onTriggerPointerDown(event),\n [onTriggerPointerDown],\n );\n\n const onNextMatchPointerDown = React.useCallback(\n (event: React.PointerEvent) =>\n onTriggerPointerDown(event),\n [onTriggerPointerDown],\n );\n\n const onClose = React.useCallback(() => {\n onSearchOpenChange(false);\n }, [onSearchOpenChange]);\n\n if (!searchOpen) return null;\n\n return (\n \n
\n \n
\n \n \n \n \n \n \n \n \n \n
\n
\n
\n {searchMatches.length > 0 ? (\n \n {matchIndex + 1} of {searchMatches.length}\n \n ) : searchQuery ? (\n No results\n ) : (\n Type to search\n )}\n
\n \n );\n}\n", "type": "registry:component", "target": "src/components/data-grid/data-grid-search.tsx" }, @@ -88,7 +88,7 @@ }, { "path": "src/hooks/use-data-grid.tsx", - "content": "\"use client\";\r\n\r\nimport {\r\n type ColumnDef,\r\n getCoreRowModel,\r\n getSortedRowModel,\r\n type RowSelectionState,\r\n type SortingState,\r\n type TableOptions,\r\n type Updater,\r\n useReactTable,\r\n} from \"@tanstack/react-table\";\r\nimport { useVirtualizer, type Virtualizer } from \"@tanstack/react-virtual\";\r\nimport * as React from \"react\";\r\nimport { DataGridCell } from \"@/components/data-grid/data-grid-cell\";\r\nimport { getCellKey, getRowHeightValue, parseCellKey } from \"@/lib/data-grid\";\r\nimport type {\r\n CellPosition,\r\n ContextMenuState,\r\n NavigationDirection,\r\n RowHeightValue,\r\n SearchState,\r\n SelectionState,\r\n UpdateCell,\r\n} from \"@/types/data-grid\";\r\n\r\nconst DEFAULT_ROW_HEIGHT = \"short\";\r\nconst OVERSCAN = 3;\r\nconst VIEWPORT_OFFSET = 1;\r\nconst MIN_COLUMN_SIZE = 60;\r\nconst MAX_COLUMN_SIZE = 800;\r\nconst SEARCH_SHORTCUT_KEY = \"f\";\r\nconst NON_NAVIGABLE_COLUMN_IDS = [\"select\", \"actions\"] as const;\r\n\r\nconst useIsomorphicLayoutEffect =\r\n typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\r\n\r\nfunction useLazyRef(fn: () => T): React.RefObject {\r\n const ref = React.useRef(null);\r\n if (ref.current === null) {\r\n ref.current = fn();\r\n }\r\n return ref as React.RefObject;\r\n}\r\n\r\nfunction useAsRef(data: T) {\r\n const ref = React.useRef(data);\r\n\r\n useIsomorphicLayoutEffect(() => {\r\n ref.current = data;\r\n });\r\n\r\n return ref;\r\n}\r\n\r\ninterface DataGridState {\r\n sorting: SortingState;\r\n rowHeight: RowHeightValue;\r\n rowSelection: RowSelectionState;\r\n selectionState: SelectionState;\r\n focusedCell: CellPosition | null;\r\n editingCell: CellPosition | null;\r\n contextMenu: ContextMenuState;\r\n searchQuery: string;\r\n searchMatches: CellPosition[];\r\n matchIndex: number;\r\n searchOpen: boolean;\r\n lastClickedRowIndex: number | null;\r\n isScrolling: boolean;\r\n}\r\n\r\ninterface DataGridStore {\r\n subscribe: (callback: () => void) => () => void;\r\n getState: () => DataGridState;\r\n setState: (\r\n key: K,\r\n value: DataGridState[K],\r\n ) => void;\r\n notify: () => void;\r\n batch: (fn: () => void) => void;\r\n}\r\n\r\nfunction useStore(\r\n store: DataGridStore,\r\n selector: (state: DataGridState) => T,\r\n): T {\r\n const getSnapshot = React.useCallback(\r\n () => selector(store.getState()),\r\n [store, selector],\r\n );\r\n\r\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\r\n}\r\n\r\ninterface UseDataGridProps\r\n extends Omit, \"pageCount\" | \"getCoreRowModel\"> {\r\n onDataChange?: (data: TData[]) => void;\r\n rowHeight?: RowHeightValue;\r\n overscan?: number;\r\n autoFocus?: boolean | Partial;\r\n enableColumnSelection?: boolean;\r\n enableSearch?: boolean;\r\n}\r\n\r\nexport function useDataGrid({\r\n columns,\r\n data,\r\n onDataChange,\r\n rowHeight: rowHeightProp = DEFAULT_ROW_HEIGHT,\r\n overscan = OVERSCAN,\r\n initialState,\r\n autoFocus = false,\r\n enableColumnSelection = false,\r\n enableSearch = false,\r\n ...dataGridProps\r\n}: UseDataGridProps) {\r\n const dataGridRef = React.useRef(null);\r\n const tableRef = React.useRef>>(null);\r\n const rowVirtualizerRef =\r\n React.useRef>(null);\r\n const headerRef = React.useRef(null);\r\n const rowMapRef = React.useRef>(new Map());\r\n const footerRef = React.useRef(null);\r\n\r\n const dataGridPropsRef = useAsRef(dataGridProps);\r\n const listenersRef = useLazyRef(() => new Set<() => void>());\r\n\r\n const stateRef = useLazyRef(() => {\r\n return {\r\n sorting: initialState?.sorting ?? [],\r\n rowHeight: rowHeightProp,\r\n rowSelection: initialState?.rowSelection ?? {},\r\n selectionState: {\r\n selectedCells: new Set(),\r\n selectionRange: null,\r\n isSelecting: false,\r\n },\r\n focusedCell: null,\r\n editingCell: null,\r\n contextMenu: {\r\n open: false,\r\n x: 0,\r\n y: 0,\r\n },\r\n searchQuery: \"\",\r\n searchMatches: [],\r\n matchIndex: -1,\r\n searchOpen: false,\r\n lastClickedRowIndex: null,\r\n isScrolling: false,\r\n };\r\n });\r\n\r\n const store = React.useMemo(() => {\r\n let isBatching = false;\r\n let pendingNotification = false;\r\n\r\n return {\r\n subscribe: (callback) => {\r\n listenersRef.current.add(callback);\r\n return () => listenersRef.current.delete(callback);\r\n },\r\n getState: () => stateRef.current,\r\n setState: (key, value) => {\r\n if (Object.is(stateRef.current[key], value)) return;\r\n stateRef.current[key] = value;\r\n\r\n if (isBatching) {\r\n pendingNotification = true;\r\n } else {\r\n if (!pendingNotification) {\r\n pendingNotification = true;\r\n queueMicrotask(() => {\r\n pendingNotification = false;\r\n store.notify();\r\n });\r\n }\r\n }\r\n },\r\n notify: () => {\r\n for (const listener of listenersRef.current) {\r\n listener();\r\n }\r\n },\r\n batch: (fn) => {\r\n if (isBatching) {\r\n fn();\r\n return;\r\n }\r\n\r\n isBatching = true;\r\n const wasPending = pendingNotification;\r\n pendingNotification = false;\r\n\r\n try {\r\n fn();\r\n } finally {\r\n isBatching = false;\r\n if (pendingNotification || wasPending) {\r\n pendingNotification = false;\r\n store.notify();\r\n }\r\n }\r\n },\r\n };\r\n }, [listenersRef, stateRef]);\r\n\r\n const focusedCell = useStore(store, (state) => state.focusedCell);\r\n const editingCell = useStore(store, (state) => state.editingCell);\r\n const selectionState = useStore(store, (state) => state.selectionState);\r\n const searchQuery = useStore(store, (state) => state.searchQuery);\r\n const searchMatches = useStore(store, (state) => state.searchMatches);\r\n const matchIndex = useStore(store, (state) => state.matchIndex);\r\n const searchOpen = useStore(store, (state) => state.searchOpen);\r\n const sorting = useStore(store, (state) => state.sorting);\r\n const rowSelection = useStore(store, (state) => state.rowSelection);\r\n const contextMenu = useStore(store, (state) => state.contextMenu);\r\n const rowHeight = useStore(store, (state) => state.rowHeight);\r\n const isScrolling = useStore(store, (state) => state.isScrolling);\r\n\r\n const rowHeightValue = getRowHeightValue(rowHeight);\r\n\r\n const columnIds = React.useMemo(() => {\r\n return columns\r\n .map((c) => {\r\n if (c.id) return c.id;\r\n if (\"accessorKey\" in c) return c.accessorKey as string;\r\n return undefined;\r\n })\r\n .filter((id): id is string => Boolean(id));\r\n }, [columns]);\r\n\r\n const navigableColumnIds = React.useMemo(() => {\r\n return columnIds.filter((c) => !NON_NAVIGABLE_COLUMN_IDS.includes(c));\r\n }, [columnIds]);\r\n\r\n const onDataUpdate = React.useCallback(\r\n (updates: UpdateCell | Array) => {\r\n const updateArray = Array.isArray(updates) ? updates : [updates];\r\n\r\n if (updateArray.length === 0) return;\r\n\r\n const currentTable = tableRef.current;\r\n const rows = currentTable?.getRowModel().rows;\r\n\r\n const rowUpdatesMap = new Map<\r\n number,\r\n Array>\r\n >();\r\n\r\n for (const update of updateArray) {\r\n if (!rows || !currentTable) {\r\n const existingUpdates = rowUpdatesMap.get(update.rowIndex) ?? [];\r\n existingUpdates.push({\r\n columnId: update.columnId,\r\n value: update.value,\r\n });\r\n rowUpdatesMap.set(update.rowIndex, existingUpdates);\r\n } else {\r\n const row = rows[update.rowIndex];\r\n if (!row) continue;\r\n\r\n const originalData = row.original;\r\n const originalRowIndex = data.indexOf(originalData);\r\n if (originalRowIndex === -1) continue;\r\n\r\n const existingUpdates = rowUpdatesMap.get(originalRowIndex) ?? [];\r\n existingUpdates.push({\r\n columnId: update.columnId,\r\n value: update.value,\r\n });\r\n rowUpdatesMap.set(originalRowIndex, existingUpdates);\r\n }\r\n }\r\n\r\n const newData = data.map((row, index) => {\r\n const updates = rowUpdatesMap.get(index);\r\n if (!updates) return row;\r\n\r\n const updatedRow = { ...row } as Record;\r\n for (const { columnId, value } of updates) {\r\n updatedRow[columnId] = value;\r\n }\r\n return updatedRow as TData;\r\n });\r\n\r\n onDataChange?.(newData);\r\n },\r\n [data, onDataChange],\r\n );\r\n\r\n const getIsCellSelected = React.useCallback(\r\n (rowIndex: number, columnId: string) => {\r\n return selectionState.selectedCells.has(getCellKey(rowIndex, columnId));\r\n },\r\n [selectionState.selectedCells],\r\n );\r\n\r\n const clearSelection = React.useCallback(() => {\r\n store.batch(() => {\r\n store.setState(\"selectionState\", {\r\n selectedCells: new Set(),\r\n selectionRange: null,\r\n isSelecting: false,\r\n });\r\n store.setState(\"rowSelection\", {});\r\n });\r\n }, [store]);\r\n\r\n const selectAll = React.useCallback(() => {\r\n const allCells = new Set();\r\n const currentTable = tableRef.current;\r\n const rows = currentTable?.getRowModel().rows ?? [];\r\n const rowCount = rows.length ?? data.length;\r\n\r\n for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {\r\n for (const columnId of columnIds) {\r\n allCells.add(getCellKey(rowIndex, columnId));\r\n }\r\n }\r\n\r\n const firstColumnId = columnIds[0];\r\n const lastColumnId = columnIds[columnIds.length - 1];\r\n\r\n store.setState(\"selectionState\", {\r\n selectedCells: allCells,\r\n selectionRange:\r\n columnIds.length > 0 && rowCount > 0 && firstColumnId && lastColumnId\r\n ? {\r\n start: { rowIndex: 0, columnId: firstColumnId },\r\n end: { rowIndex: rowCount - 1, columnId: lastColumnId },\r\n }\r\n : null,\r\n isSelecting: false,\r\n });\r\n }, [columnIds, data.length, store]);\r\n\r\n const selectColumn = React.useCallback(\r\n (columnId: string) => {\r\n const currentTable = tableRef.current;\r\n const rows = currentTable?.getRowModel().rows ?? [];\r\n const rowCount = rows.length ?? data.length;\r\n\r\n if (rowCount === 0) return;\r\n\r\n const selectedCells = new Set();\r\n\r\n for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {\r\n selectedCells.add(getCellKey(rowIndex, columnId));\r\n }\r\n\r\n store.setState(\"selectionState\", {\r\n selectedCells,\r\n selectionRange: {\r\n start: { rowIndex: 0, columnId },\r\n end: { rowIndex: rowCount - 1, columnId },\r\n },\r\n isSelecting: false,\r\n });\r\n },\r\n [data.length, store],\r\n );\r\n\r\n const selectRange = React.useCallback(\r\n (start: CellPosition, end: CellPosition, isSelecting = false) => {\r\n const startColIndex = columnIds.indexOf(start.columnId);\r\n const endColIndex = columnIds.indexOf(end.columnId);\r\n\r\n const minRow = Math.min(start.rowIndex, end.rowIndex);\r\n const maxRow = Math.max(start.rowIndex, end.rowIndex);\r\n const minCol = Math.min(startColIndex, endColIndex);\r\n const maxCol = Math.max(startColIndex, endColIndex);\r\n\r\n const selectedCells = new Set();\r\n\r\n for (let rowIndex = minRow; rowIndex <= maxRow; rowIndex++) {\r\n for (let colIndex = minCol; colIndex <= maxCol; colIndex++) {\r\n const columnId = columnIds[colIndex];\r\n if (columnId) {\r\n selectedCells.add(getCellKey(rowIndex, columnId));\r\n }\r\n }\r\n }\r\n\r\n store.setState(\"selectionState\", {\r\n selectedCells,\r\n selectionRange: { start, end },\r\n isSelecting,\r\n });\r\n },\r\n [columnIds, store],\r\n );\r\n\r\n const focusCell = React.useCallback(\r\n (rowIndex: number, columnId: string) => {\r\n store.batch(() => {\r\n store.setState(\"focusedCell\", { rowIndex, columnId });\r\n store.setState(\"editingCell\", null);\r\n });\r\n\r\n const currentState = store.getState();\r\n\r\n if (currentState.searchOpen) return;\r\n\r\n if (\r\n dataGridRef.current &&\r\n document.activeElement !== dataGridRef.current\r\n ) {\r\n dataGridRef.current.focus();\r\n }\r\n },\r\n [store],\r\n );\r\n\r\n const navigateCell = React.useCallback(\r\n (direction: NavigationDirection) => {\r\n const currentState = store.getState();\r\n if (!currentState.focusedCell) return;\r\n\r\n const { rowIndex, columnId } = currentState.focusedCell;\r\n const currentColIndex = navigableColumnIds.indexOf(columnId);\r\n const rowVirtualizer = rowVirtualizerRef.current;\r\n const currentTable = tableRef.current;\r\n const rows = currentTable?.getRowModel().rows ?? [];\r\n const rowCount = rows.length ?? data.length;\r\n\r\n let newRowIndex = rowIndex;\r\n let newColumnId = columnId;\r\n\r\n switch (direction) {\r\n case \"up\":\r\n newRowIndex = Math.max(0, rowIndex - 1);\r\n break;\r\n case \"down\":\r\n newRowIndex = Math.min(rowCount - 1, rowIndex + 1);\r\n break;\r\n case \"left\":\r\n if (currentColIndex > 0) {\r\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\r\n if (prevColumnId) newColumnId = prevColumnId;\r\n }\r\n break;\r\n case \"right\":\r\n if (currentColIndex < navigableColumnIds.length - 1) {\r\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\r\n if (nextColumnId) newColumnId = nextColumnId;\r\n }\r\n break;\r\n case \"home\":\r\n if (navigableColumnIds.length > 0) {\r\n newColumnId = navigableColumnIds[0] ?? columnId;\r\n }\r\n break;\r\n case \"end\":\r\n if (navigableColumnIds.length > 0) {\r\n newColumnId =\r\n navigableColumnIds[navigableColumnIds.length - 1] ?? columnId;\r\n }\r\n break;\r\n case \"ctrl+home\":\r\n newRowIndex = 0;\r\n if (navigableColumnIds.length > 0) {\r\n newColumnId = navigableColumnIds[0] ?? columnId;\r\n }\r\n break;\r\n case \"ctrl+end\":\r\n newRowIndex = Math.max(0, rowCount - 1);\r\n if (navigableColumnIds.length > 0) {\r\n newColumnId =\r\n navigableColumnIds[navigableColumnIds.length - 1] ?? columnId;\r\n }\r\n break;\r\n case \"pageup\":\r\n if (rowVirtualizer) {\r\n const visibleRange = rowVirtualizer.getVirtualItems();\r\n const pageSize = visibleRange.length ?? 10;\r\n newRowIndex = Math.max(0, rowIndex - pageSize);\r\n } else {\r\n newRowIndex = Math.max(0, rowIndex - 10);\r\n }\r\n break;\r\n case \"pagedown\":\r\n if (rowVirtualizer) {\r\n const visibleRange = rowVirtualizer.getVirtualItems();\r\n const pageSize = visibleRange.length ?? 10;\r\n newRowIndex = Math.min(rowCount - 1, rowIndex + pageSize);\r\n } else {\r\n newRowIndex = Math.min(rowCount - 1, rowIndex + 10);\r\n }\r\n break;\r\n }\r\n\r\n if (newRowIndex !== rowIndex || newColumnId !== columnId) {\r\n const rowDiff = newRowIndex - rowIndex;\r\n\r\n // For single-row vertical navigation (up/down arrows)\r\n if (\r\n Math.abs(rowDiff) === 1 &&\r\n (direction === \"up\" || direction === \"down\")\r\n ) {\r\n const container = dataGridRef.current;\r\n const currentRow = rowMapRef.current.get(rowIndex);\r\n const targetRow = rowMapRef.current.get(newRowIndex);\r\n\r\n if (!container || !currentRow) {\r\n // Fallback to simple focus if we can't find elements\r\n focusCell(newRowIndex, newColumnId);\r\n return;\r\n }\r\n\r\n // Check viewport boundaries\r\n const containerRect = container.getBoundingClientRect();\r\n const headerHeight =\r\n headerRef.current?.getBoundingClientRect().height ?? 0;\r\n const footerHeight =\r\n footerRef.current?.getBoundingClientRect().height ?? 0;\r\n\r\n const viewportTop =\r\n containerRect.top + headerHeight + VIEWPORT_OFFSET;\r\n const viewportBottom =\r\n containerRect.bottom - footerHeight - VIEWPORT_OFFSET;\r\n\r\n // If target row already exists, check if it's visible\r\n if (targetRow) {\r\n const rowRect = targetRow.getBoundingClientRect();\r\n const isFullyVisible =\r\n rowRect.top >= viewportTop && rowRect.bottom <= viewportBottom;\r\n\r\n if (isFullyVisible) {\r\n // Row is fully visible, just focus it\r\n focusCell(newRowIndex, newColumnId);\r\n return;\r\n }\r\n\r\n // Row exists but not fully visible, scroll it into view\r\n focusCell(newRowIndex, newColumnId);\r\n\r\n if (direction === \"down\") {\r\n // Scroll just enough to show the row at the bottom\r\n const scrollNeeded = rowRect.bottom - viewportBottom;\r\n container.scrollTop += scrollNeeded;\r\n } else {\r\n // Scroll just enough to show the row at the top\r\n const scrollNeeded = viewportTop - rowRect.top;\r\n container.scrollTop -= scrollNeeded;\r\n }\r\n return;\r\n }\r\n\r\n // Target row is not rendered yet\r\n // Focus immediately so the ring appears as the row is revealed\r\n focusCell(newRowIndex, newColumnId);\r\n\r\n // Scroll by exactly one row height to reveal it smoothly\r\n if (direction === \"down\") {\r\n container.scrollTop += rowHeightValue;\r\n } else {\r\n // For arrow up, ensure we don't go below 0\r\n const currentScrollTop = container.scrollTop;\r\n const targetScrollTop = Math.max(\r\n 0,\r\n currentScrollTop - rowHeightValue,\r\n );\r\n container.scrollTop = targetScrollTop;\r\n }\r\n return;\r\n }\r\n\r\n // For larger jumps (page up/down, ctrl+home/end, etc.)\r\n if (rowVirtualizer && Math.abs(rowDiff) > 1) {\r\n const align =\r\n direction === \"pageup\" || direction === \"ctrl+home\"\r\n ? \"start\"\r\n : direction === \"pagedown\" || direction === \"ctrl+end\"\r\n ? \"end\"\r\n : \"center\";\r\n rowVirtualizer.scrollToIndex(newRowIndex, { align });\r\n requestAnimationFrame(() => {\r\n focusCell(newRowIndex, newColumnId);\r\n });\r\n return;\r\n }\r\n\r\n // For horizontal navigation or when row is already visible\r\n focusCell(newRowIndex, newColumnId);\r\n }\r\n },\r\n [store, navigableColumnIds, focusCell, data.length, rowHeightValue],\r\n );\r\n\r\n const onCellEditingStart = React.useCallback(\r\n (rowIndex: number, columnId: string) => {\r\n store.batch(() => {\r\n store.setState(\"focusedCell\", { rowIndex, columnId });\r\n store.setState(\"editingCell\", { rowIndex, columnId });\r\n });\r\n },\r\n [store],\r\n );\r\n\r\n const onCellEditingStop = React.useCallback(\r\n (opts?: { moveToNextRow?: boolean; direction?: NavigationDirection }) => {\r\n const currentState = store.getState();\r\n const currentEditing = currentState.editingCell;\r\n\r\n store.setState(\"editingCell\", null);\r\n\r\n if (opts?.moveToNextRow && currentEditing) {\r\n const { rowIndex, columnId } = currentEditing;\r\n const currentTable = tableRef.current;\r\n const rows = currentTable?.getRowModel().rows ?? [];\r\n const rowCount = rows.length ?? data.length;\r\n\r\n const nextRowIndex = rowIndex + 1;\r\n if (nextRowIndex < rowCount) {\r\n requestAnimationFrame(() => {\r\n focusCell(nextRowIndex, columnId);\r\n });\r\n }\r\n } else if (opts?.direction && currentEditing) {\r\n // Focus the current editing cell first, then navigate\r\n const { rowIndex, columnId } = currentEditing;\r\n focusCell(rowIndex, columnId);\r\n requestAnimationFrame(() => {\r\n navigateCell(opts.direction ?? \"right\");\r\n });\r\n }\r\n },\r\n [store, data.length, focusCell, navigateCell],\r\n );\r\n\r\n const onSearchOpenChange = React.useCallback(\r\n (open: boolean) => {\r\n if (open) {\r\n store.setState(\"searchOpen\", true);\r\n return;\r\n }\r\n\r\n const currentState = store.getState();\r\n const currentMatch =\r\n currentState.matchIndex >= 0 &&\r\n currentState.searchMatches[currentState.matchIndex];\r\n\r\n store.batch(() => {\r\n store.setState(\"searchOpen\", false);\r\n store.setState(\"searchQuery\", \"\");\r\n store.setState(\"searchMatches\", []);\r\n store.setState(\"matchIndex\", -1);\r\n\r\n if (currentMatch) {\r\n store.setState(\"focusedCell\", {\r\n rowIndex: currentMatch.rowIndex,\r\n columnId: currentMatch.columnId,\r\n });\r\n }\r\n });\r\n\r\n if (\r\n dataGridRef.current &&\r\n document.activeElement !== dataGridRef.current\r\n ) {\r\n dataGridRef.current.focus();\r\n }\r\n },\r\n [store],\r\n );\r\n\r\n const onSearch = React.useCallback(\r\n (query: string) => {\r\n if (!query.trim()) {\r\n store.batch(() => {\r\n store.setState(\"searchMatches\", []);\r\n store.setState(\"matchIndex\", -1);\r\n });\r\n return;\r\n }\r\n\r\n const matches: CellPosition[] = [];\r\n const currentTable = tableRef.current;\r\n const rows = currentTable?.getRowModel().rows ?? [];\r\n\r\n const lowerQuery = query.toLowerCase();\r\n\r\n for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {\r\n const row = rows[rowIndex];\r\n if (!row) continue;\r\n\r\n for (const columnId of columnIds) {\r\n const cell = row\r\n .getVisibleCells()\r\n .find((c) => c.column.id === columnId);\r\n if (!cell) continue;\r\n\r\n const value = cell.getValue();\r\n const stringValue = String(value ?? \"\").toLowerCase();\r\n\r\n if (stringValue.includes(lowerQuery)) {\r\n matches.push({ rowIndex, columnId });\r\n }\r\n }\r\n }\r\n\r\n store.batch(() => {\r\n store.setState(\"searchMatches\", matches);\r\n store.setState(\"matchIndex\", matches.length > 0 ? 0 : -1);\r\n });\r\n\r\n // Scroll to first match but don't focus it (to keep focus in search input)\r\n if (matches.length > 0 && matches[0]) {\r\n const firstMatch = matches[0];\r\n rowVirtualizerRef.current?.scrollToIndex(firstMatch.rowIndex, {\r\n align: \"center\",\r\n });\r\n }\r\n },\r\n [columnIds, store],\r\n );\r\n\r\n const onSearchQueryChange = React.useCallback(\r\n (query: string) => store.setState(\"searchQuery\", query),\r\n [store],\r\n );\r\n\r\n const onNavigateToPrevMatch = React.useCallback(() => {\r\n const currentState = store.getState();\r\n if (currentState.searchMatches.length === 0) return;\r\n\r\n const prevIndex =\r\n currentState.matchIndex - 1 < 0\r\n ? currentState.searchMatches.length - 1\r\n : currentState.matchIndex - 1;\r\n const match = currentState.searchMatches[prevIndex];\r\n\r\n if (match) {\r\n rowVirtualizerRef.current?.scrollToIndex(match.rowIndex, {\r\n align: \"center\",\r\n });\r\n\r\n requestAnimationFrame(() => {\r\n store.setState(\"matchIndex\", prevIndex);\r\n requestAnimationFrame(() => {\r\n focusCell(match.rowIndex, match.columnId);\r\n });\r\n });\r\n }\r\n }, [store, focusCell]);\r\n\r\n const onNavigateToNextMatch = React.useCallback(() => {\r\n const currentState = store.getState();\r\n if (currentState.searchMatches.length === 0) return;\r\n\r\n const nextIndex =\r\n (currentState.matchIndex + 1) % currentState.searchMatches.length;\r\n const match = currentState.searchMatches[nextIndex];\r\n\r\n if (match) {\r\n rowVirtualizerRef.current?.scrollToIndex(match.rowIndex, {\r\n align: \"center\",\r\n });\r\n\r\n requestAnimationFrame(() => {\r\n store.setState(\"matchIndex\", nextIndex);\r\n requestAnimationFrame(() => {\r\n focusCell(match.rowIndex, match.columnId);\r\n });\r\n });\r\n }\r\n }, [store, focusCell]);\r\n\r\n const getIsSearchMatch = React.useCallback(\r\n (rowIndex: number, columnId: string) => {\r\n return searchMatches.some(\r\n (match) => match.rowIndex === rowIndex && match.columnId === columnId,\r\n );\r\n },\r\n [searchMatches],\r\n );\r\n\r\n const getIsActiveSearchMatch = React.useCallback(\r\n (rowIndex: number, columnId: string) => {\r\n if (matchIndex < 0) return false;\r\n const currentMatch = searchMatches[matchIndex];\r\n return (\r\n currentMatch?.rowIndex === rowIndex &&\r\n currentMatch?.columnId === columnId\r\n );\r\n },\r\n [searchMatches, matchIndex],\r\n );\r\n\r\n const blurCell = React.useCallback(() => {\r\n const currentState = store.getState();\r\n if (\r\n currentState.editingCell &&\r\n document.activeElement instanceof HTMLElement\r\n ) {\r\n document.activeElement.blur();\r\n }\r\n\r\n store.batch(() => {\r\n store.setState(\"focusedCell\", null);\r\n store.setState(\"editingCell\", null);\r\n });\r\n }, [store]);\r\n\r\n const onCellClick = React.useCallback(\r\n (rowIndex: number, columnId: string, event?: React.MouseEvent) => {\r\n // Ignore right-click (button 2) - let onCellContextMenu handle it\r\n if (event?.button === 2) {\r\n return;\r\n }\r\n\r\n const currentState = store.getState();\r\n const currentFocused = currentState.focusedCell;\r\n\r\n if (event) {\r\n if (event.ctrlKey || event.metaKey) {\r\n event.preventDefault();\r\n const cellKey = getCellKey(rowIndex, columnId);\r\n const newSelectedCells = new Set(\r\n currentState.selectionState.selectedCells,\r\n );\r\n\r\n if (newSelectedCells.has(cellKey)) {\r\n newSelectedCells.delete(cellKey);\r\n } else {\r\n newSelectedCells.add(cellKey);\r\n }\r\n\r\n store.setState(\"selectionState\", {\r\n selectedCells: newSelectedCells,\r\n selectionRange: null,\r\n isSelecting: false,\r\n });\r\n focusCell(rowIndex, columnId);\r\n return;\r\n }\r\n\r\n if (event.shiftKey && currentState.focusedCell) {\r\n event.preventDefault();\r\n selectRange(currentState.focusedCell, { rowIndex, columnId });\r\n return;\r\n }\r\n }\r\n\r\n // Clear selection if there are selected cells or rows\r\n const hasSelectedCells =\r\n currentState.selectionState.selectedCells.size > 0;\r\n const hasSelectedRows = Object.keys(currentState.rowSelection).length > 0;\r\n\r\n if (hasSelectedCells && !currentState.selectionState.isSelecting) {\r\n // If there's a cell selection but we're not actively selecting (drag just finished),\r\n // don't clear it - keep the selection\r\n // Only clear if clicking elsewhere\r\n const cellKey = getCellKey(rowIndex, columnId);\r\n const isClickingSelectedCell =\r\n currentState.selectionState.selectedCells.has(cellKey);\r\n\r\n if (!isClickingSelectedCell) {\r\n clearSelection();\r\n } else {\r\n // Clicking on an already selected cell - just focus it\r\n focusCell(rowIndex, columnId);\r\n return;\r\n }\r\n } else if (hasSelectedRows && columnId !== \"select\") {\r\n // If there are selected rows but we're clicking on a non-checkbox cell, clear selections\r\n clearSelection();\r\n }\r\n\r\n if (\r\n currentFocused?.rowIndex === rowIndex &&\r\n currentFocused?.columnId === columnId\r\n ) {\r\n onCellEditingStart(rowIndex, columnId);\r\n } else {\r\n focusCell(rowIndex, columnId);\r\n }\r\n },\r\n [store, focusCell, onCellEditingStart, selectRange, clearSelection],\r\n );\r\n\r\n const onCellDoubleClick = React.useCallback(\r\n (rowIndex: number, columnId: string, event?: React.MouseEvent) => {\r\n if (event?.defaultPrevented) return;\r\n\r\n onCellEditingStart(rowIndex, columnId);\r\n },\r\n [onCellEditingStart],\r\n );\r\n\r\n const onCellMouseDown = React.useCallback(\r\n (rowIndex: number, columnId: string, event: React.MouseEvent) => {\r\n // Ignore right-click (button 2) - let onCellContextMenu handle it\r\n if (event.button === 2) {\r\n return;\r\n }\r\n\r\n event.preventDefault();\r\n\r\n // Only start drag selection if no modifier keys are pressed\r\n // Clear any existing selection and prepare for potential drag\r\n if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {\r\n store.batch(() => {\r\n store.setState(\"selectionState\", {\r\n selectedCells: new Set(),\r\n selectionRange: {\r\n start: { rowIndex, columnId },\r\n end: { rowIndex, columnId },\r\n },\r\n isSelecting: true,\r\n });\r\n store.setState(\"rowSelection\", {});\r\n });\r\n }\r\n },\r\n [store],\r\n );\r\n\r\n const onCellMouseEnter = React.useCallback(\r\n (rowIndex: number, columnId: string, _event: React.MouseEvent) => {\r\n const currentState = store.getState();\r\n if (\r\n currentState.selectionState.isSelecting &&\r\n currentState.selectionState.selectionRange\r\n ) {\r\n const start = currentState.selectionState.selectionRange.start;\r\n const end = { rowIndex, columnId };\r\n\r\n if (\r\n currentState.focusedCell?.rowIndex !== start.rowIndex ||\r\n currentState.focusedCell?.columnId !== start.columnId\r\n ) {\r\n focusCell(start.rowIndex, start.columnId);\r\n }\r\n\r\n selectRange(start, end, true);\r\n }\r\n },\r\n [store, selectRange, focusCell],\r\n );\r\n\r\n const onCellMouseUp = React.useCallback(() => {\r\n const currentState = store.getState();\r\n store.setState(\"selectionState\", {\r\n ...currentState.selectionState,\r\n isSelecting: false,\r\n });\r\n }, [store]);\r\n\r\n const onCellContextMenu = React.useCallback(\r\n (rowIndex: number, columnId: string, event: React.MouseEvent) => {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n\r\n const currentState = store.getState();\r\n const cellKey = getCellKey(rowIndex, columnId);\r\n const isTargetCellSelected =\r\n currentState.selectionState.selectedCells.has(cellKey);\r\n\r\n // If right-clicking on a non-selected cell, select only that cell\r\n if (!isTargetCellSelected) {\r\n store.batch(() => {\r\n store.setState(\"selectionState\", {\r\n selectedCells: new Set([cellKey]),\r\n selectionRange: {\r\n start: { rowIndex, columnId },\r\n end: { rowIndex, columnId },\r\n },\r\n isSelecting: false,\r\n });\r\n store.setState(\"focusedCell\", { rowIndex, columnId });\r\n });\r\n }\r\n\r\n // Open context menu at cursor position\r\n store.setState(\"contextMenu\", {\r\n open: true,\r\n x: event.clientX,\r\n y: event.clientY,\r\n });\r\n },\r\n [store],\r\n );\r\n\r\n const onContextMenuOpenChange = React.useCallback(\r\n (open: boolean) => {\r\n if (!open) {\r\n const currentMenu = store.getState().contextMenu;\r\n store.setState(\"contextMenu\", {\r\n open: false,\r\n x: currentMenu.x,\r\n y: currentMenu.y,\r\n });\r\n }\r\n },\r\n [store],\r\n );\r\n\r\n const onDataGridKeyDown = React.useCallback(\r\n (event: KeyboardEvent) => {\r\n const currentState = store.getState();\r\n const { key, ctrlKey, metaKey, shiftKey } = event;\r\n const isCtrlPressed = ctrlKey || metaKey;\r\n\r\n // Handle Cmd+F / Ctrl+F to open search (highest priority, works even when editing)\r\n if (enableSearch && isCtrlPressed && key === SEARCH_SHORTCUT_KEY) {\r\n event.preventDefault();\r\n onSearchOpenChange(true);\r\n return;\r\n }\r\n\r\n // Handle search navigation when search is open\r\n if (\r\n enableSearch &&\r\n currentState.searchOpen &&\r\n !currentState.editingCell\r\n ) {\r\n if (key === \"Enter\") {\r\n event.preventDefault();\r\n if (shiftKey) {\r\n onNavigateToPrevMatch();\r\n } else {\r\n onNavigateToNextMatch();\r\n }\r\n return;\r\n }\r\n if (key === \"Escape\") {\r\n event.preventDefault();\r\n onSearchOpenChange(false);\r\n return;\r\n }\r\n // When search is open, don't let data grid handle any other keys\r\n // (they should only affect the search input)\r\n return;\r\n }\r\n\r\n if (currentState.editingCell) return;\r\n\r\n if (!currentState.focusedCell) return;\r\n\r\n let direction: NavigationDirection | null = null;\r\n\r\n if (isCtrlPressed && key === \"a\") {\r\n event.preventDefault();\r\n selectAll();\r\n return;\r\n }\r\n\r\n if (key === \"Delete\" || key === \"Backspace\") {\r\n if (currentState.selectionState.selectedCells.size > 0) {\r\n event.preventDefault();\r\n const updates: Array<{\r\n rowIndex: number;\r\n columnId: string;\r\n value: unknown;\r\n }> = [];\r\n\r\n currentState.selectionState.selectedCells.forEach((cellKey) => {\r\n const { rowIndex, columnId } = parseCellKey(cellKey);\r\n updates.push({ rowIndex, columnId, value: \"\" });\r\n });\r\n\r\n onDataUpdate(updates);\r\n clearSelection();\r\n }\r\n return;\r\n }\r\n\r\n switch (key) {\r\n case \"ArrowUp\":\r\n direction = \"up\";\r\n break;\r\n case \"ArrowDown\":\r\n direction = \"down\";\r\n break;\r\n case \"ArrowLeft\":\r\n direction = \"left\";\r\n break;\r\n case \"ArrowRight\":\r\n direction = \"right\";\r\n break;\r\n case \"Home\":\r\n direction = isCtrlPressed ? \"ctrl+home\" : \"home\";\r\n break;\r\n case \"End\":\r\n direction = isCtrlPressed ? \"ctrl+end\" : \"end\";\r\n break;\r\n case \"PageUp\":\r\n direction = \"pageup\";\r\n break;\r\n case \"PageDown\":\r\n direction = \"pagedown\";\r\n break;\r\n case \"Escape\":\r\n event.preventDefault();\r\n if (\r\n currentState.selectionState.selectedCells.size > 0 ||\r\n Object.keys(currentState.rowSelection).length > 0\r\n ) {\r\n clearSelection();\r\n } else {\r\n blurCell();\r\n }\r\n return;\r\n case \"Tab\":\r\n event.preventDefault();\r\n direction = event.shiftKey ? \"left\" : \"right\";\r\n break;\r\n }\r\n\r\n if (direction) {\r\n event.preventDefault();\r\n\r\n // Tab navigation should not trigger selection, even with Shift\r\n if (shiftKey && key !== \"Tab\" && currentState.focusedCell) {\r\n const currentColIndex = navigableColumnIds.indexOf(\r\n currentState.focusedCell.columnId,\r\n );\r\n let newRowIndex = currentState.focusedCell.rowIndex;\r\n let newColumnId = currentState.focusedCell.columnId;\r\n\r\n switch (direction) {\r\n case \"up\":\r\n newRowIndex = Math.max(0, currentState.focusedCell.rowIndex - 1);\r\n break;\r\n case \"down\":\r\n newRowIndex = Math.min(\r\n (tableRef.current?.getRowModel().rows.length || data.length) -\r\n 1,\r\n currentState.focusedCell.rowIndex + 1,\r\n );\r\n break;\r\n case \"left\":\r\n if (currentColIndex > 0) {\r\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\r\n if (prevColumnId) newColumnId = prevColumnId;\r\n }\r\n break;\r\n case \"right\":\r\n if (currentColIndex < navigableColumnIds.length - 1) {\r\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\r\n if (nextColumnId) newColumnId = nextColumnId;\r\n }\r\n break;\r\n }\r\n\r\n const selectionStart =\r\n currentState.selectionState.selectionRange?.start ||\r\n currentState.focusedCell;\r\n selectRange(selectionStart, {\r\n rowIndex: newRowIndex,\r\n columnId: newColumnId,\r\n });\r\n focusCell(newRowIndex, newColumnId);\r\n } else {\r\n if (currentState.selectionState.selectedCells.size > 0) {\r\n clearSelection();\r\n }\r\n navigateCell(direction);\r\n }\r\n }\r\n },\r\n [\r\n store,\r\n blurCell,\r\n navigateCell,\r\n selectAll,\r\n onDataUpdate,\r\n clearSelection,\r\n navigableColumnIds,\r\n data.length,\r\n selectRange,\r\n focusCell,\r\n onSearchOpenChange,\r\n onNavigateToNextMatch,\r\n onNavigateToPrevMatch,\r\n enableSearch,\r\n ],\r\n );\r\n\r\n const onSortingChange = React.useCallback(\r\n (updater: Updater) => {\r\n const currentState = store.getState();\r\n const newSorting =\r\n typeof updater === \"function\" ? updater(currentState.sorting) : updater;\r\n store.setState(\"sorting\", newSorting);\r\n },\r\n [store],\r\n );\r\n\r\n const onRowSelectionChange = React.useCallback(\r\n (updater: Updater) => {\r\n const currentState = store.getState();\r\n const newRowSelection =\r\n typeof updater === \"function\"\r\n ? updater(currentState.rowSelection)\r\n : updater;\r\n\r\n const selectedRows = Object.keys(newRowSelection).filter(\r\n (key) => newRowSelection[key],\r\n );\r\n\r\n const selectedCells = new Set();\r\n const rows = tableRef.current?.getRowModel().rows ?? [];\r\n\r\n for (const rowId of selectedRows) {\r\n const rowIndex = rows.findIndex((r) => r.id === rowId);\r\n if (rowIndex === -1) continue;\r\n\r\n for (const columnId of columnIds) {\r\n selectedCells.add(getCellKey(rowIndex, columnId));\r\n }\r\n }\r\n\r\n store.batch(() => {\r\n store.setState(\"rowSelection\", newRowSelection);\r\n store.setState(\"selectionState\", {\r\n selectedCells,\r\n selectionRange: null,\r\n isSelecting: false,\r\n });\r\n store.setState(\"focusedCell\", null);\r\n store.setState(\"editingCell\", null);\r\n });\r\n },\r\n [store, columnIds],\r\n );\r\n\r\n const onRowSelect = React.useCallback(\r\n (rowIndex: number, selected: boolean, shiftKey: boolean) => {\r\n const currentState = store.getState();\r\n const rows = tableRef.current?.getRowModel().rows ?? [];\r\n const currentRow = rows[rowIndex];\r\n if (!currentRow) return;\r\n\r\n if (shiftKey && currentState.lastClickedRowIndex !== null) {\r\n const startIndex = Math.min(currentState.lastClickedRowIndex, rowIndex);\r\n const endIndex = Math.max(currentState.lastClickedRowIndex, rowIndex);\r\n\r\n const newRowSelection: RowSelectionState = {\r\n ...currentState.rowSelection,\r\n };\r\n\r\n for (let i = startIndex; i <= endIndex; i++) {\r\n const row = rows[i];\r\n if (row) {\r\n newRowSelection[row.id] = selected;\r\n }\r\n }\r\n\r\n onRowSelectionChange(newRowSelection);\r\n } else {\r\n onRowSelectionChange({\r\n ...currentState.rowSelection,\r\n [currentRow.id]: selected,\r\n });\r\n }\r\n\r\n store.setState(\"lastClickedRowIndex\", rowIndex);\r\n },\r\n [store, onRowSelectionChange],\r\n );\r\n\r\n const onRowHeightChange = React.useCallback(\r\n (updater: Updater) => {\r\n const currentState = store.getState();\r\n const newRowHeight =\r\n typeof updater === \"function\"\r\n ? updater(currentState.rowHeight)\r\n : updater;\r\n store.setState(\"rowHeight\", newRowHeight);\r\n },\r\n [store],\r\n );\r\n\r\n const onColumnClick = React.useCallback(\r\n (columnId: string) => {\r\n if (!enableColumnSelection) {\r\n clearSelection();\r\n return;\r\n }\r\n\r\n selectColumn(columnId);\r\n },\r\n [enableColumnSelection, selectColumn, clearSelection],\r\n );\r\n\r\n const defaultColumn: Partial> = React.useMemo(\r\n () => ({\r\n cell: DataGridCell,\r\n minSize: MIN_COLUMN_SIZE,\r\n maxSize: MAX_COLUMN_SIZE,\r\n }),\r\n [],\r\n );\r\n\r\n const tableOptions = React.useMemo>(\r\n () => ({\r\n ...dataGridPropsRef.current,\r\n data,\r\n columns,\r\n defaultColumn,\r\n initialState,\r\n state: {\r\n ...dataGridPropsRef.current.state,\r\n sorting,\r\n rowSelection,\r\n },\r\n onRowSelectionChange,\r\n onSortingChange,\r\n columnResizeMode: \"onChange\",\r\n getCoreRowModel: getCoreRowModel(),\r\n getSortedRowModel: getSortedRowModel(),\r\n meta: {\r\n ...dataGridPropsRef.current.meta,\r\n dataGridRef,\r\n focusedCell,\r\n editingCell,\r\n selectionState,\r\n searchOpen,\r\n isScrolling,\r\n getIsCellSelected,\r\n getIsSearchMatch,\r\n getIsActiveSearchMatch,\r\n onDataUpdate,\r\n onColumnClick,\r\n onCellClick,\r\n onCellDoubleClick,\r\n onCellMouseDown,\r\n onCellMouseEnter,\r\n onCellMouseUp,\r\n onCellContextMenu,\r\n onCellEditingStart,\r\n onCellEditingStop,\r\n contextMenu,\r\n onContextMenuOpenChange,\r\n rowHeight,\r\n onRowHeightChange,\r\n onRowSelect,\r\n },\r\n }),\r\n [\r\n dataGridPropsRef,\r\n data,\r\n columns,\r\n defaultColumn,\r\n initialState,\r\n sorting,\r\n rowSelection,\r\n onRowSelectionChange,\r\n onSortingChange,\r\n focusedCell,\r\n editingCell,\r\n selectionState,\r\n searchOpen,\r\n isScrolling,\r\n getIsCellSelected,\r\n getIsSearchMatch,\r\n getIsActiveSearchMatch,\r\n onDataUpdate,\r\n onColumnClick,\r\n onCellClick,\r\n onCellDoubleClick,\r\n onCellMouseDown,\r\n onCellMouseEnter,\r\n onCellMouseUp,\r\n onCellContextMenu,\r\n onCellEditingStart,\r\n onCellEditingStop,\r\n contextMenu,\r\n onContextMenuOpenChange,\r\n rowHeight,\r\n onRowHeightChange,\r\n onRowSelect,\r\n ],\r\n );\r\n\r\n const table = useReactTable(tableOptions);\r\n\r\n if (!tableRef.current) {\r\n tableRef.current = table;\r\n }\r\n\r\n // biome-ignore lint/correctness/useExhaustiveDependencies: we need to memoize the column size vars\r\n const columnSizeVars = React.useMemo(() => {\r\n const headers = table.getFlatHeaders();\r\n const colSizes: { [key: string]: number } = {};\r\n for (const header of headers) {\r\n colSizes[`--header-${header.id}-size`] = header.getSize();\r\n colSizes[`--col-${header.column.id}-size`] = header.column.getSize();\r\n }\r\n return colSizes;\r\n }, [table.getState().columnSizingInfo, table.getState().columnSizing]);\r\n\r\n const rowVirtualizer = useVirtualizer({\r\n count: table.getRowModel().rows.length,\r\n getScrollElement: () => dataGridRef.current,\r\n estimateSize: () => rowHeightValue,\r\n overscan,\r\n measureElement:\r\n typeof window !== \"undefined\" &&\r\n navigator.userAgent.indexOf(\"Firefox\") === -1\r\n ? (element) => element?.getBoundingClientRect().height\r\n : undefined,\r\n onChange: (instance) => {\r\n // Sync virtualizer's isScrolling state to our store\r\n const virtualizerIsScrolling = instance.isScrolling;\r\n const currentIsScrolling = store.getState().isScrolling;\r\n\r\n if (virtualizerIsScrolling !== currentIsScrolling) {\r\n store.setState(\"isScrolling\", virtualizerIsScrolling);\r\n }\r\n\r\n // Batch DOM updates in a single animation frame\r\n const virtualItems = instance.getVirtualItems();\r\n if (virtualItems.length === 0) return;\r\n\r\n requestAnimationFrame(() => {\r\n for (let i = 0; i < virtualItems.length; i++) {\r\n const virtualRow = virtualItems[i];\r\n if (!virtualRow) continue;\r\n const rowRef = rowMapRef.current.get(virtualRow.index);\r\n if (rowRef) {\r\n rowRef.style.transform = `translateY(${virtualRow.start}px)`;\r\n }\r\n }\r\n });\r\n },\r\n });\r\n\r\n if (!rowVirtualizerRef.current) {\r\n rowVirtualizerRef.current = rowVirtualizer;\r\n }\r\n\r\n const scrollToRow = React.useCallback(\r\n async (opts: Partial) => {\r\n const rowIndex = opts?.rowIndex ?? 0;\r\n const columnId = opts?.columnId;\r\n\r\n rowVirtualizer.scrollToIndex(rowIndex, {\r\n align: \"center\",\r\n });\r\n\r\n const targetColumnId = columnId ?? navigableColumnIds[0];\r\n\r\n if (!targetColumnId) return;\r\n\r\n queueMicrotask(() => {\r\n requestAnimationFrame(() => {\r\n requestAnimationFrame(() => {\r\n store.batch(() => {\r\n store.setState(\"focusedCell\", {\r\n rowIndex,\r\n columnId: targetColumnId,\r\n });\r\n store.setState(\"editingCell\", null);\r\n });\r\n });\r\n });\r\n });\r\n },\r\n [rowVirtualizer, navigableColumnIds, store],\r\n );\r\n\r\n const searchState = React.useMemo(() => {\r\n if (!enableSearch) return undefined;\r\n\r\n return {\r\n searchMatches,\r\n matchIndex,\r\n searchOpen,\r\n onSearchOpenChange,\r\n searchQuery,\r\n onSearchQueryChange,\r\n onSearch,\r\n onNavigateToNextMatch,\r\n onNavigateToPrevMatch,\r\n };\r\n }, [\r\n enableSearch,\r\n searchMatches,\r\n matchIndex,\r\n searchOpen,\r\n onSearchOpenChange,\r\n searchQuery,\r\n onSearchQueryChange,\r\n onSearch,\r\n onNavigateToNextMatch,\r\n onNavigateToPrevMatch,\r\n ]);\r\n\r\n React.useEffect(() => {\r\n const dataGridElement = dataGridRef.current;\r\n if (!dataGridElement) return;\r\n\r\n dataGridElement.addEventListener(\"keydown\", onDataGridKeyDown);\r\n return () => {\r\n dataGridElement.removeEventListener(\"keydown\", onDataGridKeyDown);\r\n };\r\n }, [onDataGridKeyDown]);\r\n\r\n React.useEffect(() => {\r\n function onGlobalKeyDown(event: KeyboardEvent) {\r\n const dataGridElement = dataGridRef.current;\r\n if (!dataGridElement) return;\r\n\r\n const target = event.target;\r\n if (!(target instanceof HTMLElement)) return;\r\n\r\n const { key, ctrlKey, metaKey } = event;\r\n const isCtrlPressed = ctrlKey || metaKey;\r\n\r\n if (enableSearch && isCtrlPressed && key === SEARCH_SHORTCUT_KEY) {\r\n const isInInput =\r\n target.tagName === \"INPUT\" || target.tagName === \"TEXTAREA\";\r\n const isInDataGrid = dataGridElement.contains(target);\r\n const isInSearchInput = target.closest('[role=\"search\"]') !== null;\r\n\r\n if (isInDataGrid || isInSearchInput || !isInInput) {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n onSearchOpenChange(true);\r\n\r\n if (!isInDataGrid && !isInSearchInput) {\r\n requestAnimationFrame(() => {\r\n dataGridElement.focus();\r\n });\r\n }\r\n return;\r\n }\r\n }\r\n\r\n const isInDataGrid = dataGridElement.contains(target);\r\n if (!isInDataGrid) return;\r\n\r\n if (key === \"Escape\") {\r\n const currentState = store.getState();\r\n const hasSelections =\r\n currentState.selectionState.selectedCells.size > 0 ||\r\n Object.keys(currentState.rowSelection).length > 0;\r\n\r\n if (hasSelections) {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n clearSelection();\r\n }\r\n }\r\n }\r\n\r\n window.addEventListener(\"keydown\", onGlobalKeyDown, true);\r\n return () => {\r\n window.removeEventListener(\"keydown\", onGlobalKeyDown, true);\r\n };\r\n }, [enableSearch, onSearchOpenChange, store, clearSelection]);\r\n\r\n React.useEffect(() => {\r\n const currentState = store.getState();\r\n if (\r\n autoFocus &&\r\n data.length > 0 &&\r\n columns.length > 0 &&\r\n !currentState.focusedCell\r\n ) {\r\n if (navigableColumnIds.length > 0) {\r\n const rafId = requestAnimationFrame(() => {\r\n if (typeof autoFocus === \"object\") {\r\n const { rowIndex, columnId } = autoFocus;\r\n if (columnId) {\r\n focusCell(rowIndex ?? 0, columnId);\r\n }\r\n return;\r\n }\r\n\r\n const firstColumnId = navigableColumnIds[0];\r\n if (firstColumnId) {\r\n focusCell(0, firstColumnId);\r\n }\r\n });\r\n return () => cancelAnimationFrame(rafId);\r\n }\r\n }\r\n }, [\r\n autoFocus,\r\n data.length,\r\n columns.length,\r\n store,\r\n navigableColumnIds,\r\n focusCell,\r\n ]);\r\n\r\n React.useEffect(() => {\r\n function onOutsideClick(event: MouseEvent) {\r\n if (event.button === 2) {\r\n return;\r\n }\r\n\r\n if (\r\n dataGridRef.current &&\r\n !dataGridRef.current.contains(event.target as Node)\r\n ) {\r\n const target = event.target;\r\n const isInsidePopover =\r\n target instanceof HTMLElement &&\r\n (target.closest(\"[data-grid-cell-editor]\") ||\r\n target.closest(\"[data-grid-popover]\"));\r\n\r\n if (!isInsidePopover) {\r\n blurCell();\r\n const currentState = store.getState();\r\n if (\r\n currentState.selectionState.selectedCells.size > 0 ||\r\n Object.keys(currentState.rowSelection).length > 0\r\n ) {\r\n clearSelection();\r\n }\r\n }\r\n }\r\n }\r\n\r\n document.addEventListener(\"mousedown\", onOutsideClick);\r\n return () => {\r\n document.removeEventListener(\"mousedown\", onOutsideClick);\r\n };\r\n }, [store, blurCell, clearSelection]);\r\n\r\n React.useEffect(() => {\r\n function cleanup() {\r\n document.removeEventListener(\"selectstart\", preventSelection);\r\n document.removeEventListener(\"contextmenu\", preventContextMenu);\r\n document.body.style.userSelect = \"\";\r\n }\r\n\r\n function preventSelection(event: Event) {\r\n event.preventDefault();\r\n }\r\n function preventContextMenu(event: Event) {\r\n event.preventDefault();\r\n }\r\n\r\n const unsubscribe = store.subscribe(() => {\r\n const currentState = store.getState();\r\n if (currentState.selectionState.isSelecting) {\r\n document.addEventListener(\"selectstart\", preventSelection);\r\n document.addEventListener(\"contextmenu\", preventContextMenu);\r\n document.body.style.userSelect = \"none\";\r\n } else {\r\n cleanup();\r\n }\r\n });\r\n\r\n return () => {\r\n cleanup();\r\n unsubscribe();\r\n };\r\n }, [store]);\r\n\r\n useIsomorphicLayoutEffect(() => {\r\n const rafId = requestAnimationFrame(() => {\r\n rowVirtualizer.measure();\r\n });\r\n return () => cancelAnimationFrame(rafId);\r\n }, [\r\n table.getState().columnFilters,\r\n table.getState().columnOrder,\r\n table.getState().columnPinning,\r\n table.getState().columnSizing,\r\n table.getState().columnVisibility,\r\n table.getState().expanded,\r\n table.getState().globalFilter,\r\n table.getState().grouping,\r\n table.getState().rowSelection,\r\n table.getState().sorting,\r\n rowHeight,\r\n ]);\r\n\r\n return {\r\n dataGridRef,\r\n headerRef,\r\n rowMapRef,\r\n footerRef,\r\n table,\r\n rowVirtualizer,\r\n searchState,\r\n columnSizeVars,\r\n scrollToRow,\r\n };\r\n}\r\n", + "content": "\"use client\";\n\nimport {\n type ColumnDef,\n getCoreRowModel,\n getSortedRowModel,\n type RowSelectionState,\n type SortingState,\n type TableOptions,\n type Updater,\n useReactTable,\n} from \"@tanstack/react-table\";\nimport { useVirtualizer, type Virtualizer } from \"@tanstack/react-virtual\";\nimport * as React from \"react\";\nimport { DataGridCell } from \"@/components/data-grid/data-grid-cell\";\nimport { getCellKey, getRowHeightValue, parseCellKey } from \"@/lib/data-grid\";\nimport type {\n CellPosition,\n ContextMenuState,\n NavigationDirection,\n RowHeightValue,\n SearchState,\n SelectionState,\n UpdateCell,\n} from \"@/types/data-grid\";\n\nconst DEFAULT_ROW_HEIGHT = \"short\";\nconst OVERSCAN = 3;\nconst VIEWPORT_OFFSET = 1;\nconst MIN_COLUMN_SIZE = 60;\nconst MAX_COLUMN_SIZE = 800;\nconst SEARCH_SHORTCUT_KEY = \"f\";\nconst NON_NAVIGABLE_COLUMN_IDS = [\"select\", \"actions\"];\n\nconst useIsomorphicLayoutEffect =\n typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\nfunction useLazyRef(fn: () => T): React.RefObject {\n const ref = React.useRef(null);\n if (ref.current === null) {\n ref.current = fn();\n }\n return ref as React.RefObject;\n}\n\nfunction useAsRef(data: T) {\n const ref = React.useRef(data);\n\n useIsomorphicLayoutEffect(() => {\n ref.current = data;\n });\n\n return ref;\n}\n\ninterface DataGridState {\n sorting: SortingState;\n rowHeight: RowHeightValue;\n rowSelection: RowSelectionState;\n selectionState: SelectionState;\n focusedCell: CellPosition | null;\n editingCell: CellPosition | null;\n contextMenu: ContextMenuState;\n searchQuery: string;\n searchMatches: CellPosition[];\n matchIndex: number;\n searchOpen: boolean;\n lastClickedRowIndex: number | null;\n isScrolling: boolean;\n}\n\ninterface DataGridStore {\n subscribe: (callback: () => void) => () => void;\n getState: () => DataGridState;\n setState: (\n key: K,\n value: DataGridState[K],\n ) => void;\n notify: () => void;\n batch: (fn: () => void) => void;\n}\n\nfunction useStore(\n store: DataGridStore,\n selector: (state: DataGridState) => T,\n): T {\n const getSnapshot = React.useCallback(\n () => selector(store.getState()),\n [store, selector],\n );\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\ninterface UseDataGridProps\n extends Omit, \"pageCount\" | \"getCoreRowModel\"> {\n onDataChange?: (data: TData[]) => void;\n rowHeight?: RowHeightValue;\n overscan?: number;\n autoFocus?: boolean | Partial;\n enableColumnSelection?: boolean;\n enableSearch?: boolean;\n}\n\nexport function useDataGrid({\n columns,\n data,\n onDataChange,\n rowHeight: rowHeightProp = DEFAULT_ROW_HEIGHT,\n overscan = OVERSCAN,\n initialState,\n autoFocus = false,\n enableColumnSelection = false,\n enableSearch = false,\n ...dataGridProps\n}: UseDataGridProps) {\n const dataGridRef = React.useRef(null);\n const tableRef = React.useRef>>(null);\n const rowVirtualizerRef =\n React.useRef>(null);\n const headerRef = React.useRef(null);\n const rowMapRef = React.useRef>(new Map());\n const footerRef = React.useRef(null);\n\n const dataGridPropsRef = useAsRef(dataGridProps);\n const listenersRef = useLazyRef(() => new Set<() => void>());\n\n const stateRef = useLazyRef(() => {\n return {\n sorting: initialState?.sorting ?? [],\n rowHeight: rowHeightProp,\n rowSelection: initialState?.rowSelection ?? {},\n selectionState: {\n selectedCells: new Set(),\n selectionRange: null,\n isSelecting: false,\n },\n focusedCell: null,\n editingCell: null,\n contextMenu: {\n open: false,\n x: 0,\n y: 0,\n },\n searchQuery: \"\",\n searchMatches: [],\n matchIndex: -1,\n searchOpen: false,\n lastClickedRowIndex: null,\n isScrolling: false,\n };\n });\n\n const store = React.useMemo(() => {\n let isBatching = false;\n let pendingNotification = false;\n\n return {\n subscribe: (callback) => {\n listenersRef.current.add(callback);\n return () => listenersRef.current.delete(callback);\n },\n getState: () => stateRef.current,\n setState: (key, value) => {\n if (Object.is(stateRef.current[key], value)) return;\n stateRef.current[key] = value;\n\n if (isBatching) {\n pendingNotification = true;\n } else {\n if (!pendingNotification) {\n pendingNotification = true;\n queueMicrotask(() => {\n pendingNotification = false;\n store.notify();\n });\n }\n }\n },\n notify: () => {\n for (const listener of listenersRef.current) {\n listener();\n }\n },\n batch: (fn) => {\n if (isBatching) {\n fn();\n return;\n }\n\n isBatching = true;\n const wasPending = pendingNotification;\n pendingNotification = false;\n\n try {\n fn();\n } finally {\n isBatching = false;\n if (pendingNotification || wasPending) {\n pendingNotification = false;\n store.notify();\n }\n }\n },\n };\n }, [listenersRef, stateRef]);\n\n const focusedCell = useStore(store, (state) => state.focusedCell);\n const editingCell = useStore(store, (state) => state.editingCell);\n const selectionState = useStore(store, (state) => state.selectionState);\n const searchQuery = useStore(store, (state) => state.searchQuery);\n const searchMatches = useStore(store, (state) => state.searchMatches);\n const matchIndex = useStore(store, (state) => state.matchIndex);\n const searchOpen = useStore(store, (state) => state.searchOpen);\n const sorting = useStore(store, (state) => state.sorting);\n const rowSelection = useStore(store, (state) => state.rowSelection);\n const contextMenu = useStore(store, (state) => state.contextMenu);\n const rowHeight = useStore(store, (state) => state.rowHeight);\n const isScrolling = useStore(store, (state) => state.isScrolling);\n\n const rowHeightValue = getRowHeightValue(rowHeight);\n\n const columnIds = React.useMemo(() => {\n return columns\n .map((c) => {\n if (c.id) return c.id;\n if (\"accessorKey\" in c) return c.accessorKey as string;\n return undefined;\n })\n .filter((id): id is string => Boolean(id));\n }, [columns]);\n\n const navigableColumnIds = React.useMemo(() => {\n return columnIds.filter((c) => !NON_NAVIGABLE_COLUMN_IDS.includes(c));\n }, [columnIds]);\n\n const onDataUpdate = React.useCallback(\n (updates: UpdateCell | Array) => {\n const updateArray = Array.isArray(updates) ? updates : [updates];\n\n if (updateArray.length === 0) return;\n\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows;\n\n const rowUpdatesMap = new Map<\n number,\n Array>\n >();\n\n for (const update of updateArray) {\n if (!rows || !currentTable) {\n const existingUpdates = rowUpdatesMap.get(update.rowIndex) ?? [];\n existingUpdates.push({\n columnId: update.columnId,\n value: update.value,\n });\n rowUpdatesMap.set(update.rowIndex, existingUpdates);\n } else {\n const row = rows[update.rowIndex];\n if (!row) continue;\n\n const originalData = row.original;\n const originalRowIndex = data.indexOf(originalData);\n if (originalRowIndex === -1) continue;\n\n const existingUpdates = rowUpdatesMap.get(originalRowIndex) ?? [];\n existingUpdates.push({\n columnId: update.columnId,\n value: update.value,\n });\n rowUpdatesMap.set(originalRowIndex, existingUpdates);\n }\n }\n\n const newData = data.map((row, index) => {\n const updates = rowUpdatesMap.get(index);\n if (!updates) return row;\n\n const updatedRow = { ...row } as Record;\n for (const { columnId, value } of updates) {\n updatedRow[columnId] = value;\n }\n return updatedRow as TData;\n });\n\n onDataChange?.(newData);\n },\n [data, onDataChange],\n );\n\n const getIsCellSelected = React.useCallback(\n (rowIndex: number, columnId: string) => {\n return selectionState.selectedCells.has(getCellKey(rowIndex, columnId));\n },\n [selectionState.selectedCells],\n );\n\n const clearSelection = React.useCallback(() => {\n store.batch(() => {\n store.setState(\"selectionState\", {\n selectedCells: new Set(),\n selectionRange: null,\n isSelecting: false,\n });\n store.setState(\"rowSelection\", {});\n });\n }, [store]);\n\n const selectAll = React.useCallback(() => {\n const allCells = new Set();\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n const rowCount = rows.length ?? data.length;\n\n for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {\n for (const columnId of columnIds) {\n allCells.add(getCellKey(rowIndex, columnId));\n }\n }\n\n const firstColumnId = columnIds[0];\n const lastColumnId = columnIds[columnIds.length - 1];\n\n store.setState(\"selectionState\", {\n selectedCells: allCells,\n selectionRange:\n columnIds.length > 0 && rowCount > 0 && firstColumnId && lastColumnId\n ? {\n start: { rowIndex: 0, columnId: firstColumnId },\n end: { rowIndex: rowCount - 1, columnId: lastColumnId },\n }\n : null,\n isSelecting: false,\n });\n }, [columnIds, data.length, store]);\n\n const selectColumn = React.useCallback(\n (columnId: string) => {\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n const rowCount = rows.length ?? data.length;\n\n if (rowCount === 0) return;\n\n const selectedCells = new Set();\n\n for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {\n selectedCells.add(getCellKey(rowIndex, columnId));\n }\n\n store.setState(\"selectionState\", {\n selectedCells,\n selectionRange: {\n start: { rowIndex: 0, columnId },\n end: { rowIndex: rowCount - 1, columnId },\n },\n isSelecting: false,\n });\n },\n [data.length, store],\n );\n\n const selectRange = React.useCallback(\n (start: CellPosition, end: CellPosition, isSelecting = false) => {\n const startColIndex = columnIds.indexOf(start.columnId);\n const endColIndex = columnIds.indexOf(end.columnId);\n\n const minRow = Math.min(start.rowIndex, end.rowIndex);\n const maxRow = Math.max(start.rowIndex, end.rowIndex);\n const minCol = Math.min(startColIndex, endColIndex);\n const maxCol = Math.max(startColIndex, endColIndex);\n\n const selectedCells = new Set();\n\n for (let rowIndex = minRow; rowIndex <= maxRow; rowIndex++) {\n for (let colIndex = minCol; colIndex <= maxCol; colIndex++) {\n const columnId = columnIds[colIndex];\n if (columnId) {\n selectedCells.add(getCellKey(rowIndex, columnId));\n }\n }\n }\n\n store.setState(\"selectionState\", {\n selectedCells,\n selectionRange: { start, end },\n isSelecting,\n });\n },\n [columnIds, store],\n );\n\n const focusCell = React.useCallback(\n (rowIndex: number, columnId: string) => {\n store.batch(() => {\n store.setState(\"focusedCell\", { rowIndex, columnId });\n store.setState(\"editingCell\", null);\n });\n\n const currentState = store.getState();\n\n if (currentState.searchOpen) return;\n\n if (\n dataGridRef.current &&\n document.activeElement !== dataGridRef.current\n ) {\n dataGridRef.current.focus();\n }\n },\n [store],\n );\n\n const navigateCell = React.useCallback(\n (direction: NavigationDirection) => {\n const currentState = store.getState();\n if (!currentState.focusedCell) return;\n\n const { rowIndex, columnId } = currentState.focusedCell;\n const currentColIndex = navigableColumnIds.indexOf(columnId);\n const rowVirtualizer = rowVirtualizerRef.current;\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n const rowCount = rows.length ?? data.length;\n\n let newRowIndex = rowIndex;\n let newColumnId = columnId;\n\n switch (direction) {\n case \"up\":\n newRowIndex = Math.max(0, rowIndex - 1);\n break;\n case \"down\":\n newRowIndex = Math.min(rowCount - 1, rowIndex + 1);\n break;\n case \"left\":\n if (currentColIndex > 0) {\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\n if (prevColumnId) newColumnId = prevColumnId;\n }\n break;\n case \"right\":\n if (currentColIndex < navigableColumnIds.length - 1) {\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\n if (nextColumnId) newColumnId = nextColumnId;\n }\n break;\n case \"home\":\n if (navigableColumnIds.length > 0) {\n newColumnId = navigableColumnIds[0] ?? columnId;\n }\n break;\n case \"end\":\n if (navigableColumnIds.length > 0) {\n newColumnId =\n navigableColumnIds[navigableColumnIds.length - 1] ?? columnId;\n }\n break;\n case \"ctrl+home\":\n newRowIndex = 0;\n if (navigableColumnIds.length > 0) {\n newColumnId = navigableColumnIds[0] ?? columnId;\n }\n break;\n case \"ctrl+end\":\n newRowIndex = Math.max(0, rowCount - 1);\n if (navigableColumnIds.length > 0) {\n newColumnId =\n navigableColumnIds[navigableColumnIds.length - 1] ?? columnId;\n }\n break;\n case \"pageup\":\n if (rowVirtualizer) {\n const visibleRange = rowVirtualizer.getVirtualItems();\n const pageSize = visibleRange.length ?? 10;\n newRowIndex = Math.max(0, rowIndex - pageSize);\n } else {\n newRowIndex = Math.max(0, rowIndex - 10);\n }\n break;\n case \"pagedown\":\n if (rowVirtualizer) {\n const visibleRange = rowVirtualizer.getVirtualItems();\n const pageSize = visibleRange.length ?? 10;\n newRowIndex = Math.min(rowCount - 1, rowIndex + pageSize);\n } else {\n newRowIndex = Math.min(rowCount - 1, rowIndex + 10);\n }\n break;\n }\n\n if (newRowIndex !== rowIndex || newColumnId !== columnId) {\n const rowDiff = newRowIndex - rowIndex;\n\n // For single-row vertical navigation (up/down arrows)\n if (\n Math.abs(rowDiff) === 1 &&\n (direction === \"up\" || direction === \"down\")\n ) {\n const container = dataGridRef.current;\n const currentRow = rowMapRef.current.get(rowIndex);\n const targetRow = rowMapRef.current.get(newRowIndex);\n\n if (!container || !currentRow) {\n // Fallback to simple focus if we can't find elements\n focusCell(newRowIndex, newColumnId);\n return;\n }\n\n // Check viewport boundaries\n const containerRect = container.getBoundingClientRect();\n const headerHeight =\n headerRef.current?.getBoundingClientRect().height ?? 0;\n const footerHeight =\n footerRef.current?.getBoundingClientRect().height ?? 0;\n\n const viewportTop =\n containerRect.top + headerHeight + VIEWPORT_OFFSET;\n const viewportBottom =\n containerRect.bottom - footerHeight - VIEWPORT_OFFSET;\n\n // If target row already exists, check if it's visible\n if (targetRow) {\n const rowRect = targetRow.getBoundingClientRect();\n const isFullyVisible =\n rowRect.top >= viewportTop && rowRect.bottom <= viewportBottom;\n\n if (isFullyVisible) {\n // Row is fully visible, just focus it\n focusCell(newRowIndex, newColumnId);\n return;\n }\n\n // Row exists but not fully visible, scroll it into view\n focusCell(newRowIndex, newColumnId);\n\n if (direction === \"down\") {\n // Scroll just enough to show the row at the bottom\n const scrollNeeded = rowRect.bottom - viewportBottom;\n container.scrollTop += scrollNeeded;\n } else {\n // Scroll just enough to show the row at the top\n const scrollNeeded = viewportTop - rowRect.top;\n container.scrollTop -= scrollNeeded;\n }\n return;\n }\n\n // Target row is not rendered yet\n // Focus immediately so the ring appears as the row is revealed\n focusCell(newRowIndex, newColumnId);\n\n // Scroll by exactly one row height to reveal it smoothly\n if (direction === \"down\") {\n container.scrollTop += rowHeightValue;\n } else {\n // For arrow up, ensure we don't go below 0\n const currentScrollTop = container.scrollTop;\n const targetScrollTop = Math.max(\n 0,\n currentScrollTop - rowHeightValue,\n );\n container.scrollTop = targetScrollTop;\n }\n return;\n }\n\n // For larger jumps (page up/down, ctrl+home/end, etc.)\n if (rowVirtualizer && Math.abs(rowDiff) > 1) {\n const align =\n direction === \"pageup\" || direction === \"ctrl+home\"\n ? \"start\"\n : direction === \"pagedown\" || direction === \"ctrl+end\"\n ? \"end\"\n : \"center\";\n rowVirtualizer.scrollToIndex(newRowIndex, { align });\n requestAnimationFrame(() => {\n focusCell(newRowIndex, newColumnId);\n });\n return;\n }\n\n // For horizontal navigation or when row is already visible\n focusCell(newRowIndex, newColumnId);\n }\n },\n [store, navigableColumnIds, focusCell, data.length, rowHeightValue],\n );\n\n const onCellEditingStart = React.useCallback(\n (rowIndex: number, columnId: string) => {\n store.batch(() => {\n store.setState(\"focusedCell\", { rowIndex, columnId });\n store.setState(\"editingCell\", { rowIndex, columnId });\n });\n },\n [store],\n );\n\n const onCellEditingStop = React.useCallback(\n (opts?: { moveToNextRow?: boolean; direction?: NavigationDirection }) => {\n const currentState = store.getState();\n const currentEditing = currentState.editingCell;\n\n store.setState(\"editingCell\", null);\n\n if (opts?.moveToNextRow && currentEditing) {\n const { rowIndex, columnId } = currentEditing;\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n const rowCount = rows.length ?? data.length;\n\n const nextRowIndex = rowIndex + 1;\n if (nextRowIndex < rowCount) {\n requestAnimationFrame(() => {\n focusCell(nextRowIndex, columnId);\n });\n }\n } else if (opts?.direction && currentEditing) {\n // Focus the current editing cell first, then navigate\n const { rowIndex, columnId } = currentEditing;\n focusCell(rowIndex, columnId);\n requestAnimationFrame(() => {\n navigateCell(opts.direction ?? \"right\");\n });\n }\n },\n [store, data.length, focusCell, navigateCell],\n );\n\n const onSearchOpenChange = React.useCallback(\n (open: boolean) => {\n if (open) {\n store.setState(\"searchOpen\", true);\n return;\n }\n\n const currentState = store.getState();\n const currentMatch =\n currentState.matchIndex >= 0 &&\n currentState.searchMatches[currentState.matchIndex];\n\n store.batch(() => {\n store.setState(\"searchOpen\", false);\n store.setState(\"searchQuery\", \"\");\n store.setState(\"searchMatches\", []);\n store.setState(\"matchIndex\", -1);\n\n if (currentMatch) {\n store.setState(\"focusedCell\", {\n rowIndex: currentMatch.rowIndex,\n columnId: currentMatch.columnId,\n });\n }\n });\n\n if (\n dataGridRef.current &&\n document.activeElement !== dataGridRef.current\n ) {\n dataGridRef.current.focus();\n }\n },\n [store],\n );\n\n const onSearch = React.useCallback(\n (query: string) => {\n if (!query.trim()) {\n store.batch(() => {\n store.setState(\"searchMatches\", []);\n store.setState(\"matchIndex\", -1);\n });\n return;\n }\n\n const matches: CellPosition[] = [];\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n\n const lowerQuery = query.toLowerCase();\n\n for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {\n const row = rows[rowIndex];\n if (!row) continue;\n\n for (const columnId of columnIds) {\n const cell = row\n .getVisibleCells()\n .find((c) => c.column.id === columnId);\n if (!cell) continue;\n\n const value = cell.getValue();\n const stringValue = String(value ?? \"\").toLowerCase();\n\n if (stringValue.includes(lowerQuery)) {\n matches.push({ rowIndex, columnId });\n }\n }\n }\n\n store.batch(() => {\n store.setState(\"searchMatches\", matches);\n store.setState(\"matchIndex\", matches.length > 0 ? 0 : -1);\n });\n\n // Scroll to first match but don't focus it (to keep focus in search input)\n if (matches.length > 0 && matches[0]) {\n const firstMatch = matches[0];\n rowVirtualizerRef.current?.scrollToIndex(firstMatch.rowIndex, {\n align: \"center\",\n });\n }\n },\n [columnIds, store],\n );\n\n const onSearchQueryChange = React.useCallback(\n (query: string) => store.setState(\"searchQuery\", query),\n [store],\n );\n\n const onNavigateToPrevMatch = React.useCallback(() => {\n const currentState = store.getState();\n if (currentState.searchMatches.length === 0) return;\n\n const prevIndex =\n currentState.matchIndex - 1 < 0\n ? currentState.searchMatches.length - 1\n : currentState.matchIndex - 1;\n const match = currentState.searchMatches[prevIndex];\n\n if (match) {\n rowVirtualizerRef.current?.scrollToIndex(match.rowIndex, {\n align: \"center\",\n });\n\n requestAnimationFrame(() => {\n store.setState(\"matchIndex\", prevIndex);\n requestAnimationFrame(() => {\n focusCell(match.rowIndex, match.columnId);\n });\n });\n }\n }, [store, focusCell]);\n\n const onNavigateToNextMatch = React.useCallback(() => {\n const currentState = store.getState();\n if (currentState.searchMatches.length === 0) return;\n\n const nextIndex =\n (currentState.matchIndex + 1) % currentState.searchMatches.length;\n const match = currentState.searchMatches[nextIndex];\n\n if (match) {\n rowVirtualizerRef.current?.scrollToIndex(match.rowIndex, {\n align: \"center\",\n });\n\n requestAnimationFrame(() => {\n store.setState(\"matchIndex\", nextIndex);\n requestAnimationFrame(() => {\n focusCell(match.rowIndex, match.columnId);\n });\n });\n }\n }, [store, focusCell]);\n\n const getIsSearchMatch = React.useCallback(\n (rowIndex: number, columnId: string) => {\n return searchMatches.some(\n (match) => match.rowIndex === rowIndex && match.columnId === columnId,\n );\n },\n [searchMatches],\n );\n\n const getIsActiveSearchMatch = React.useCallback(\n (rowIndex: number, columnId: string) => {\n if (matchIndex < 0) return false;\n const currentMatch = searchMatches[matchIndex];\n return (\n currentMatch?.rowIndex === rowIndex &&\n currentMatch?.columnId === columnId\n );\n },\n [searchMatches, matchIndex],\n );\n\n const blurCell = React.useCallback(() => {\n const currentState = store.getState();\n if (\n currentState.editingCell &&\n document.activeElement instanceof HTMLElement\n ) {\n document.activeElement.blur();\n }\n\n store.batch(() => {\n store.setState(\"focusedCell\", null);\n store.setState(\"editingCell\", null);\n });\n }, [store]);\n\n const onCellClick = React.useCallback(\n (rowIndex: number, columnId: string, event?: React.MouseEvent) => {\n // Ignore right-click (button 2) - let onCellContextMenu handle it\n if (event?.button === 2) {\n return;\n }\n\n const currentState = store.getState();\n const currentFocused = currentState.focusedCell;\n\n if (event) {\n if (event.ctrlKey || event.metaKey) {\n event.preventDefault();\n const cellKey = getCellKey(rowIndex, columnId);\n const newSelectedCells = new Set(\n currentState.selectionState.selectedCells,\n );\n\n if (newSelectedCells.has(cellKey)) {\n newSelectedCells.delete(cellKey);\n } else {\n newSelectedCells.add(cellKey);\n }\n\n store.setState(\"selectionState\", {\n selectedCells: newSelectedCells,\n selectionRange: null,\n isSelecting: false,\n });\n focusCell(rowIndex, columnId);\n return;\n }\n\n if (event.shiftKey && currentState.focusedCell) {\n event.preventDefault();\n selectRange(currentState.focusedCell, { rowIndex, columnId });\n return;\n }\n }\n\n // Clear selection if there are selected cells or rows\n const hasSelectedCells =\n currentState.selectionState.selectedCells.size > 0;\n const hasSelectedRows = Object.keys(currentState.rowSelection).length > 0;\n\n if (hasSelectedCells && !currentState.selectionState.isSelecting) {\n // If there's a cell selection but we're not actively selecting (drag just finished),\n // don't clear it - keep the selection\n // Only clear if clicking elsewhere\n const cellKey = getCellKey(rowIndex, columnId);\n const isClickingSelectedCell =\n currentState.selectionState.selectedCells.has(cellKey);\n\n if (!isClickingSelectedCell) {\n clearSelection();\n } else {\n // Clicking on an already selected cell - just focus it\n focusCell(rowIndex, columnId);\n return;\n }\n } else if (hasSelectedRows && columnId !== \"select\") {\n // If there are selected rows but we're clicking on a non-checkbox cell, clear selections\n clearSelection();\n }\n\n if (\n currentFocused?.rowIndex === rowIndex &&\n currentFocused?.columnId === columnId\n ) {\n onCellEditingStart(rowIndex, columnId);\n } else {\n focusCell(rowIndex, columnId);\n }\n },\n [store, focusCell, onCellEditingStart, selectRange, clearSelection],\n );\n\n const onCellDoubleClick = React.useCallback(\n (rowIndex: number, columnId: string, event?: React.MouseEvent) => {\n if (event?.defaultPrevented) return;\n\n onCellEditingStart(rowIndex, columnId);\n },\n [onCellEditingStart],\n );\n\n const onCellMouseDown = React.useCallback(\n (rowIndex: number, columnId: string, event: React.MouseEvent) => {\n // Ignore right-click (button 2) - let onCellContextMenu handle it\n if (event.button === 2) {\n return;\n }\n\n event.preventDefault();\n\n // Only start drag selection if no modifier keys are pressed\n // Clear any existing selection and prepare for potential drag\n if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {\n store.batch(() => {\n store.setState(\"selectionState\", {\n selectedCells: new Set(),\n selectionRange: {\n start: { rowIndex, columnId },\n end: { rowIndex, columnId },\n },\n isSelecting: true,\n });\n store.setState(\"rowSelection\", {});\n });\n }\n },\n [store],\n );\n\n const onCellMouseEnter = React.useCallback(\n (rowIndex: number, columnId: string, _event: React.MouseEvent) => {\n const currentState = store.getState();\n if (\n currentState.selectionState.isSelecting &&\n currentState.selectionState.selectionRange\n ) {\n const start = currentState.selectionState.selectionRange.start;\n const end = { rowIndex, columnId };\n\n if (\n currentState.focusedCell?.rowIndex !== start.rowIndex ||\n currentState.focusedCell?.columnId !== start.columnId\n ) {\n focusCell(start.rowIndex, start.columnId);\n }\n\n selectRange(start, end, true);\n }\n },\n [store, selectRange, focusCell],\n );\n\n const onCellMouseUp = React.useCallback(() => {\n const currentState = store.getState();\n store.setState(\"selectionState\", {\n ...currentState.selectionState,\n isSelecting: false,\n });\n }, [store]);\n\n const onCellContextMenu = React.useCallback(\n (rowIndex: number, columnId: string, event: React.MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n\n const currentState = store.getState();\n const cellKey = getCellKey(rowIndex, columnId);\n const isTargetCellSelected =\n currentState.selectionState.selectedCells.has(cellKey);\n\n // If right-clicking on a non-selected cell, select only that cell\n if (!isTargetCellSelected) {\n store.batch(() => {\n store.setState(\"selectionState\", {\n selectedCells: new Set([cellKey]),\n selectionRange: {\n start: { rowIndex, columnId },\n end: { rowIndex, columnId },\n },\n isSelecting: false,\n });\n store.setState(\"focusedCell\", { rowIndex, columnId });\n });\n }\n\n // Open context menu at cursor position\n store.setState(\"contextMenu\", {\n open: true,\n x: event.clientX,\n y: event.clientY,\n });\n },\n [store],\n );\n\n const onContextMenuOpenChange = React.useCallback(\n (open: boolean) => {\n if (!open) {\n const currentMenu = store.getState().contextMenu;\n store.setState(\"contextMenu\", {\n open: false,\n x: currentMenu.x,\n y: currentMenu.y,\n });\n }\n },\n [store],\n );\n\n const onDataGridKeyDown = React.useCallback(\n (event: KeyboardEvent) => {\n const currentState = store.getState();\n const { key, ctrlKey, metaKey, shiftKey } = event;\n const isCtrlPressed = ctrlKey || metaKey;\n\n // Handle Cmd+F / Ctrl+F to open search (highest priority, works even when editing)\n if (enableSearch && isCtrlPressed && key === SEARCH_SHORTCUT_KEY) {\n event.preventDefault();\n onSearchOpenChange(true);\n return;\n }\n\n // Handle search navigation when search is open\n if (\n enableSearch &&\n currentState.searchOpen &&\n !currentState.editingCell\n ) {\n if (key === \"Enter\") {\n event.preventDefault();\n if (shiftKey) {\n onNavigateToPrevMatch();\n } else {\n onNavigateToNextMatch();\n }\n return;\n }\n if (key === \"Escape\") {\n event.preventDefault();\n onSearchOpenChange(false);\n return;\n }\n // When search is open, don't let data grid handle any other keys\n // (they should only affect the search input)\n return;\n }\n\n if (currentState.editingCell) return;\n\n if (!currentState.focusedCell) return;\n\n let direction: NavigationDirection | null = null;\n\n if (isCtrlPressed && key === \"a\") {\n event.preventDefault();\n selectAll();\n return;\n }\n\n if (key === \"Delete\" || key === \"Backspace\") {\n if (currentState.selectionState.selectedCells.size > 0) {\n event.preventDefault();\n const updates: Array<{\n rowIndex: number;\n columnId: string;\n value: unknown;\n }> = [];\n\n currentState.selectionState.selectedCells.forEach((cellKey) => {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n updates.push({ rowIndex, columnId, value: \"\" });\n });\n\n onDataUpdate(updates);\n clearSelection();\n }\n return;\n }\n\n switch (key) {\n case \"ArrowUp\":\n direction = \"up\";\n break;\n case \"ArrowDown\":\n direction = \"down\";\n break;\n case \"ArrowLeft\":\n direction = \"left\";\n break;\n case \"ArrowRight\":\n direction = \"right\";\n break;\n case \"Home\":\n direction = isCtrlPressed ? \"ctrl+home\" : \"home\";\n break;\n case \"End\":\n direction = isCtrlPressed ? \"ctrl+end\" : \"end\";\n break;\n case \"PageUp\":\n direction = \"pageup\";\n break;\n case \"PageDown\":\n direction = \"pagedown\";\n break;\n case \"Escape\":\n event.preventDefault();\n if (\n currentState.selectionState.selectedCells.size > 0 ||\n Object.keys(currentState.rowSelection).length > 0\n ) {\n clearSelection();\n } else {\n blurCell();\n }\n return;\n case \"Tab\":\n event.preventDefault();\n direction = event.shiftKey ? \"left\" : \"right\";\n break;\n }\n\n if (direction) {\n event.preventDefault();\n\n // Tab navigation should not trigger selection, even with Shift\n if (shiftKey && key !== \"Tab\" && currentState.focusedCell) {\n const currentColIndex = navigableColumnIds.indexOf(\n currentState.focusedCell.columnId,\n );\n let newRowIndex = currentState.focusedCell.rowIndex;\n let newColumnId = currentState.focusedCell.columnId;\n\n switch (direction) {\n case \"up\":\n newRowIndex = Math.max(0, currentState.focusedCell.rowIndex - 1);\n break;\n case \"down\":\n newRowIndex = Math.min(\n (tableRef.current?.getRowModel().rows.length || data.length) -\n 1,\n currentState.focusedCell.rowIndex + 1,\n );\n break;\n case \"left\":\n if (currentColIndex > 0) {\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\n if (prevColumnId) newColumnId = prevColumnId;\n }\n break;\n case \"right\":\n if (currentColIndex < navigableColumnIds.length - 1) {\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\n if (nextColumnId) newColumnId = nextColumnId;\n }\n break;\n }\n\n const selectionStart =\n currentState.selectionState.selectionRange?.start ||\n currentState.focusedCell;\n selectRange(selectionStart, {\n rowIndex: newRowIndex,\n columnId: newColumnId,\n });\n focusCell(newRowIndex, newColumnId);\n } else {\n if (currentState.selectionState.selectedCells.size > 0) {\n clearSelection();\n }\n navigateCell(direction);\n }\n }\n },\n [\n store,\n blurCell,\n navigateCell,\n selectAll,\n onDataUpdate,\n clearSelection,\n navigableColumnIds,\n data.length,\n selectRange,\n focusCell,\n onSearchOpenChange,\n onNavigateToNextMatch,\n onNavigateToPrevMatch,\n enableSearch,\n ],\n );\n\n const onSortingChange = React.useCallback(\n (updater: Updater) => {\n const currentState = store.getState();\n const newSorting =\n typeof updater === \"function\" ? updater(currentState.sorting) : updater;\n store.setState(\"sorting\", newSorting);\n },\n [store],\n );\n\n const onRowSelectionChange = React.useCallback(\n (updater: Updater) => {\n const currentState = store.getState();\n const newRowSelection =\n typeof updater === \"function\"\n ? updater(currentState.rowSelection)\n : updater;\n\n const selectedRows = Object.keys(newRowSelection).filter(\n (key) => newRowSelection[key],\n );\n\n const selectedCells = new Set();\n const rows = tableRef.current?.getRowModel().rows ?? [];\n\n for (const rowId of selectedRows) {\n const rowIndex = rows.findIndex((r) => r.id === rowId);\n if (rowIndex === -1) continue;\n\n for (const columnId of columnIds) {\n selectedCells.add(getCellKey(rowIndex, columnId));\n }\n }\n\n store.batch(() => {\n store.setState(\"rowSelection\", newRowSelection);\n store.setState(\"selectionState\", {\n selectedCells,\n selectionRange: null,\n isSelecting: false,\n });\n store.setState(\"focusedCell\", null);\n store.setState(\"editingCell\", null);\n });\n },\n [store, columnIds],\n );\n\n const onRowSelect = React.useCallback(\n (rowIndex: number, selected: boolean, shiftKey: boolean) => {\n const currentState = store.getState();\n const rows = tableRef.current?.getRowModel().rows ?? [];\n const currentRow = rows[rowIndex];\n if (!currentRow) return;\n\n if (shiftKey && currentState.lastClickedRowIndex !== null) {\n const startIndex = Math.min(currentState.lastClickedRowIndex, rowIndex);\n const endIndex = Math.max(currentState.lastClickedRowIndex, rowIndex);\n\n const newRowSelection: RowSelectionState = {\n ...currentState.rowSelection,\n };\n\n for (let i = startIndex; i <= endIndex; i++) {\n const row = rows[i];\n if (row) {\n newRowSelection[row.id] = selected;\n }\n }\n\n onRowSelectionChange(newRowSelection);\n } else {\n onRowSelectionChange({\n ...currentState.rowSelection,\n [currentRow.id]: selected,\n });\n }\n\n store.setState(\"lastClickedRowIndex\", rowIndex);\n },\n [store, onRowSelectionChange],\n );\n\n const onRowHeightChange = React.useCallback(\n (updater: Updater) => {\n const currentState = store.getState();\n const newRowHeight =\n typeof updater === \"function\"\n ? updater(currentState.rowHeight)\n : updater;\n store.setState(\"rowHeight\", newRowHeight);\n },\n [store],\n );\n\n const onColumnClick = React.useCallback(\n (columnId: string) => {\n if (!enableColumnSelection) {\n clearSelection();\n return;\n }\n\n selectColumn(columnId);\n },\n [enableColumnSelection, selectColumn, clearSelection],\n );\n\n const defaultColumn: Partial> = React.useMemo(\n () => ({\n cell: DataGridCell,\n minSize: MIN_COLUMN_SIZE,\n maxSize: MAX_COLUMN_SIZE,\n }),\n [],\n );\n\n const tableOptions = React.useMemo>(\n () => ({\n ...dataGridPropsRef.current,\n data,\n columns,\n defaultColumn,\n initialState,\n state: {\n ...dataGridPropsRef.current.state,\n sorting,\n rowSelection,\n },\n onRowSelectionChange,\n onSortingChange,\n columnResizeMode: \"onChange\",\n getCoreRowModel: getCoreRowModel(),\n getSortedRowModel: getSortedRowModel(),\n meta: {\n ...dataGridPropsRef.current.meta,\n dataGridRef,\n focusedCell,\n editingCell,\n selectionState,\n searchOpen,\n isScrolling,\n getIsCellSelected,\n getIsSearchMatch,\n getIsActiveSearchMatch,\n onDataUpdate,\n onColumnClick,\n onCellClick,\n onCellDoubleClick,\n onCellMouseDown,\n onCellMouseEnter,\n onCellMouseUp,\n onCellContextMenu,\n onCellEditingStart,\n onCellEditingStop,\n contextMenu,\n onContextMenuOpenChange,\n rowHeight,\n onRowHeightChange,\n onRowSelect,\n },\n }),\n [\n dataGridPropsRef,\n data,\n columns,\n defaultColumn,\n initialState,\n sorting,\n rowSelection,\n onRowSelectionChange,\n onSortingChange,\n focusedCell,\n editingCell,\n selectionState,\n searchOpen,\n isScrolling,\n getIsCellSelected,\n getIsSearchMatch,\n getIsActiveSearchMatch,\n onDataUpdate,\n onColumnClick,\n onCellClick,\n onCellDoubleClick,\n onCellMouseDown,\n onCellMouseEnter,\n onCellMouseUp,\n onCellContextMenu,\n onCellEditingStart,\n onCellEditingStop,\n contextMenu,\n onContextMenuOpenChange,\n rowHeight,\n onRowHeightChange,\n onRowSelect,\n ],\n );\n\n const table = useReactTable(tableOptions);\n\n if (!tableRef.current) {\n tableRef.current = table;\n }\n\n // biome-ignore lint/correctness/useExhaustiveDependencies: we need to memoize the column size vars\n const columnSizeVars = React.useMemo(() => {\n const headers = table.getFlatHeaders();\n const colSizes: { [key: string]: number } = {};\n for (const header of headers) {\n colSizes[`--header-${header.id}-size`] = header.getSize();\n colSizes[`--col-${header.column.id}-size`] = header.column.getSize();\n }\n return colSizes;\n }, [table.getState().columnSizingInfo, table.getState().columnSizing]);\n\n const rowVirtualizer = useVirtualizer({\n count: table.getRowModel().rows.length,\n getScrollElement: () => dataGridRef.current,\n estimateSize: () => rowHeightValue,\n overscan,\n measureElement:\n typeof window !== \"undefined\" &&\n navigator.userAgent.indexOf(\"Firefox\") === -1\n ? (element) => element?.getBoundingClientRect().height\n : undefined,\n onChange: (instance) => {\n // Sync virtualizer's isScrolling state to our store\n const virtualizerIsScrolling = instance.isScrolling;\n const currentIsScrolling = store.getState().isScrolling;\n\n if (virtualizerIsScrolling !== currentIsScrolling) {\n store.setState(\"isScrolling\", virtualizerIsScrolling);\n }\n\n // Batch DOM updates in a single animation frame\n const virtualItems = instance.getVirtualItems();\n if (virtualItems.length === 0) return;\n\n requestAnimationFrame(() => {\n for (let i = 0; i < virtualItems.length; i++) {\n const virtualRow = virtualItems[i];\n if (!virtualRow) continue;\n const rowRef = rowMapRef.current.get(virtualRow.index);\n if (rowRef) {\n rowRef.style.transform = `translateY(${virtualRow.start}px)`;\n }\n }\n });\n },\n });\n\n if (!rowVirtualizerRef.current) {\n rowVirtualizerRef.current = rowVirtualizer;\n }\n\n const scrollToRow = React.useCallback(\n async (opts: Partial) => {\n const rowIndex = opts?.rowIndex ?? 0;\n const columnId = opts?.columnId;\n\n rowVirtualizer.scrollToIndex(rowIndex, {\n align: \"center\",\n });\n\n const targetColumnId = columnId ?? navigableColumnIds[0];\n\n if (!targetColumnId) return;\n\n queueMicrotask(() => {\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n store.batch(() => {\n store.setState(\"focusedCell\", {\n rowIndex,\n columnId: targetColumnId,\n });\n store.setState(\"editingCell\", null);\n });\n });\n });\n });\n },\n [rowVirtualizer, navigableColumnIds, store],\n );\n\n const searchState = React.useMemo(() => {\n if (!enableSearch) return undefined;\n\n return {\n searchMatches,\n matchIndex,\n searchOpen,\n onSearchOpenChange,\n searchQuery,\n onSearchQueryChange,\n onSearch,\n onNavigateToNextMatch,\n onNavigateToPrevMatch,\n };\n }, [\n enableSearch,\n searchMatches,\n matchIndex,\n searchOpen,\n onSearchOpenChange,\n searchQuery,\n onSearchQueryChange,\n onSearch,\n onNavigateToNextMatch,\n onNavigateToPrevMatch,\n ]);\n\n React.useEffect(() => {\n const dataGridElement = dataGridRef.current;\n if (!dataGridElement) return;\n\n dataGridElement.addEventListener(\"keydown\", onDataGridKeyDown);\n return () => {\n dataGridElement.removeEventListener(\"keydown\", onDataGridKeyDown);\n };\n }, [onDataGridKeyDown]);\n\n React.useEffect(() => {\n function onGlobalKeyDown(event: KeyboardEvent) {\n const dataGridElement = dataGridRef.current;\n if (!dataGridElement) return;\n\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n\n const { key, ctrlKey, metaKey } = event;\n const isCtrlPressed = ctrlKey || metaKey;\n\n if (enableSearch && isCtrlPressed && key === SEARCH_SHORTCUT_KEY) {\n const isInInput =\n target.tagName === \"INPUT\" || target.tagName === \"TEXTAREA\";\n const isInDataGrid = dataGridElement.contains(target);\n const isInSearchInput = target.closest('[role=\"search\"]') !== null;\n\n if (isInDataGrid || isInSearchInput || !isInInput) {\n event.preventDefault();\n event.stopPropagation();\n onSearchOpenChange(true);\n\n if (!isInDataGrid && !isInSearchInput) {\n requestAnimationFrame(() => {\n dataGridElement.focus();\n });\n }\n return;\n }\n }\n\n const isInDataGrid = dataGridElement.contains(target);\n if (!isInDataGrid) return;\n\n if (key === \"Escape\") {\n const currentState = store.getState();\n const hasSelections =\n currentState.selectionState.selectedCells.size > 0 ||\n Object.keys(currentState.rowSelection).length > 0;\n\n if (hasSelections) {\n event.preventDefault();\n event.stopPropagation();\n clearSelection();\n }\n }\n }\n\n window.addEventListener(\"keydown\", onGlobalKeyDown, true);\n return () => {\n window.removeEventListener(\"keydown\", onGlobalKeyDown, true);\n };\n }, [enableSearch, onSearchOpenChange, store, clearSelection]);\n\n React.useEffect(() => {\n const currentState = store.getState();\n if (\n autoFocus &&\n data.length > 0 &&\n columns.length > 0 &&\n !currentState.focusedCell\n ) {\n if (navigableColumnIds.length > 0) {\n const rafId = requestAnimationFrame(() => {\n if (typeof autoFocus === \"object\") {\n const { rowIndex, columnId } = autoFocus;\n if (columnId) {\n focusCell(rowIndex ?? 0, columnId);\n }\n return;\n }\n\n const firstColumnId = navigableColumnIds[0];\n if (firstColumnId) {\n focusCell(0, firstColumnId);\n }\n });\n return () => cancelAnimationFrame(rafId);\n }\n }\n }, [\n autoFocus,\n data.length,\n columns.length,\n store,\n navigableColumnIds,\n focusCell,\n ]);\n\n React.useEffect(() => {\n function onOutsideClick(event: MouseEvent) {\n if (event.button === 2) {\n return;\n }\n\n if (\n dataGridRef.current &&\n !dataGridRef.current.contains(event.target as Node)\n ) {\n const target = event.target;\n const isInsidePopover =\n target instanceof HTMLElement &&\n (target.closest(\"[data-grid-cell-editor]\") ||\n target.closest(\"[data-grid-popover]\"));\n\n if (!isInsidePopover) {\n blurCell();\n const currentState = store.getState();\n if (\n currentState.selectionState.selectedCells.size > 0 ||\n Object.keys(currentState.rowSelection).length > 0\n ) {\n clearSelection();\n }\n }\n }\n }\n\n document.addEventListener(\"mousedown\", onOutsideClick);\n return () => {\n document.removeEventListener(\"mousedown\", onOutsideClick);\n };\n }, [store, blurCell, clearSelection]);\n\n React.useEffect(() => {\n function cleanup() {\n document.removeEventListener(\"selectstart\", preventSelection);\n document.removeEventListener(\"contextmenu\", preventContextMenu);\n document.body.style.userSelect = \"\";\n }\n\n function preventSelection(event: Event) {\n event.preventDefault();\n }\n function preventContextMenu(event: Event) {\n event.preventDefault();\n }\n\n const unsubscribe = store.subscribe(() => {\n const currentState = store.getState();\n if (currentState.selectionState.isSelecting) {\n document.addEventListener(\"selectstart\", preventSelection);\n document.addEventListener(\"contextmenu\", preventContextMenu);\n document.body.style.userSelect = \"none\";\n } else {\n cleanup();\n }\n });\n\n return () => {\n cleanup();\n unsubscribe();\n };\n }, [store]);\n\n useIsomorphicLayoutEffect(() => {\n const rafId = requestAnimationFrame(() => {\n rowVirtualizer.measure();\n });\n return () => cancelAnimationFrame(rafId);\n }, [\n table.getState().columnFilters,\n table.getState().columnOrder,\n table.getState().columnPinning,\n table.getState().columnSizing,\n table.getState().columnVisibility,\n table.getState().expanded,\n table.getState().globalFilter,\n table.getState().grouping,\n table.getState().rowSelection,\n table.getState().sorting,\n rowHeight,\n ]);\n\n return {\n dataGridRef,\n headerRef,\n rowMapRef,\n footerRef,\n table,\n rowVirtualizer,\n searchState,\n columnSizeVars,\n scrollToRow,\n };\n}\n", "type": "registry:hook" }, { @@ -103,12 +103,12 @@ }, { "path": "src/lib/data-grid.ts", - "content": "import type { CellPosition, RowHeightValue } from \"@/types/data-grid\";\r\n\r\nexport function getCellKey(rowIndex: number, columnId: string) {\r\n return `${rowIndex}:${columnId}`;\r\n}\r\n\r\nexport function parseCellKey(cellKey: string): Required {\r\n const parts = cellKey.split(\":\");\r\n const rowIndexStr = parts[0];\r\n const columnId = parts[1];\r\n if (rowIndexStr && columnId) {\r\n const rowIndex = parseInt(rowIndexStr, 10);\r\n if (!Number.isNaN(rowIndex)) {\r\n return { rowIndex, columnId };\r\n }\r\n }\r\n return { rowIndex: 0, columnId: \"\" };\r\n}\r\n\r\nexport function getRowHeightValue(rowHeight: RowHeightValue): number {\r\n const rowHeightMap: Record = {\r\n short: 36,\r\n medium: 56,\r\n tall: 76,\r\n \"extra-tall\": 96,\r\n };\r\n\r\n return rowHeightMap[rowHeight];\r\n}\r\n\r\nexport function getLineCount(rowHeight: RowHeightValue): number {\r\n const lineCountMap: Record = {\r\n short: 1,\r\n medium: 2,\r\n tall: 3,\r\n \"extra-tall\": 4,\r\n };\r\n\r\n return lineCountMap[rowHeight];\r\n}\r\n", + "content": "import type { CellPosition, RowHeightValue } from \"@/types/data-grid\";\n\nexport function getCellKey(rowIndex: number, columnId: string) {\n return `${rowIndex}:${columnId}`;\n}\n\nexport function parseCellKey(cellKey: string): Required {\n const parts = cellKey.split(\":\");\n const rowIndexStr = parts[0];\n const columnId = parts[1];\n if (rowIndexStr && columnId) {\n const rowIndex = parseInt(rowIndexStr, 10);\n if (!Number.isNaN(rowIndex)) {\n return { rowIndex, columnId };\n }\n }\n return { rowIndex: 0, columnId: \"\" };\n}\n\nexport function getRowHeightValue(rowHeight: RowHeightValue): number {\n const rowHeightMap: Record = {\n short: 36,\n medium: 56,\n tall: 76,\n \"extra-tall\": 96,\n };\n\n return rowHeightMap[rowHeight];\n}\n\nexport function getLineCount(rowHeight: RowHeightValue): number {\n const lineCountMap: Record = {\n short: 1,\n medium: 2,\n tall: 3,\n \"extra-tall\": 4,\n };\n\n return lineCountMap[rowHeight];\n}\n", "type": "registry:lib" }, { "path": "src/types/data-grid.ts", - "content": "import type { RowData } from \"@tanstack/react-table\";\r\n\r\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\r\n\r\nexport interface CellSelectOption {\r\n label: string;\r\n value: string;\r\n}\r\n\r\nexport type Cell =\r\n | {\r\n variant: \"short-text\";\r\n }\r\n | {\r\n variant: \"long-text\";\r\n }\r\n | {\r\n variant: \"number\";\r\n min?: number;\r\n max?: number;\r\n step?: number;\r\n }\r\n | {\r\n variant: \"select\";\r\n options: CellSelectOption[];\r\n }\r\n | {\r\n variant: \"multi-select\";\r\n options: CellSelectOption[];\r\n }\r\n | {\r\n variant: \"checkbox\";\r\n }\r\n | {\r\n variant: \"date\";\r\n };\r\n\r\nexport interface UpdateCell {\r\n rowIndex: number;\r\n columnId: string;\r\n value: unknown;\r\n}\r\n\r\ndeclare module \"@tanstack/react-table\" {\r\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\r\n interface ColumnMeta {\r\n label?: string;\r\n cell?: Cell;\r\n }\r\n\r\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\r\n interface TableMeta {\r\n dataGridRef?: React.RefObject;\r\n focusedCell?: CellPosition | null;\r\n editingCell?: CellPosition | null;\r\n selectionState?: SelectionState;\r\n searchOpen?: boolean;\r\n isScrolling?: boolean;\r\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\r\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\r\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\r\n onDataUpdate?: (props: UpdateCell | Array) => void;\r\n onColumnClick?: (columnId: string) => void;\r\n onCellClick?: (\r\n rowIndex: number,\r\n columnId: string,\r\n event?: React.MouseEvent,\r\n ) => void;\r\n onCellDoubleClick?: (rowIndex: number, columnId: string) => void;\r\n onCellMouseDown?: (\r\n rowIndex: number,\r\n columnId: string,\r\n event: React.MouseEvent,\r\n ) => void;\r\n onCellMouseEnter?: (\r\n rowIndex: number,\r\n columnId: string,\r\n event: React.MouseEvent,\r\n ) => void;\r\n onCellMouseUp?: () => void;\r\n onCellContextMenu?: (\r\n rowIndex: number,\r\n columnId: string,\r\n event: React.MouseEvent,\r\n ) => void;\r\n onCellEditingStart?: (rowIndex: number, columnId: string) => void;\r\n onCellEditingStop?: (opts?: {\r\n direction?: NavigationDirection;\r\n moveToNextRow?: boolean;\r\n }) => void;\r\n contextMenu?: ContextMenuState;\r\n onContextMenuOpenChange?: (open: boolean) => void;\r\n rowHeight?: RowHeightValue;\r\n onRowHeightChange?: (value: RowHeightValue) => void;\r\n onRowSelect?: (\r\n rowIndex: number,\r\n checked: boolean,\r\n shiftKey: boolean,\r\n ) => void;\r\n }\r\n}\r\n\r\nexport interface CellPosition {\r\n rowIndex: number;\r\n columnId: string;\r\n}\r\n\r\nexport interface CellRange {\r\n start: CellPosition;\r\n end: CellPosition;\r\n}\r\n\r\nexport interface SelectionState {\r\n selectedCells: Set;\r\n selectionRange: CellRange | null;\r\n isSelecting: boolean;\r\n}\r\n\r\nexport interface ContextMenuState {\r\n open: boolean;\r\n x: number;\r\n y: number;\r\n}\r\n\r\nexport type NavigationDirection =\r\n | \"up\"\r\n | \"down\"\r\n | \"left\"\r\n | \"right\"\r\n | \"home\"\r\n | \"end\"\r\n | \"ctrl+home\"\r\n | \"ctrl+end\"\r\n | \"pageup\"\r\n | \"pagedown\";\r\n\r\nexport interface SearchState {\r\n searchMatches: CellPosition[];\r\n matchIndex: number;\r\n searchOpen: boolean;\r\n onSearchOpenChange: (open: boolean) => void;\r\n searchQuery: string;\r\n onSearchQueryChange: (query: string) => void;\r\n onSearch: (query: string) => void;\r\n onNavigateToNextMatch: () => void;\r\n onNavigateToPrevMatch: () => void;\r\n}\r\n", + "content": "import type { RowData } from \"@tanstack/react-table\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n label: string;\n value: string;\n}\n\nexport type Cell =\n | {\n variant: \"short-text\";\n }\n | {\n variant: \"long-text\";\n }\n | {\n variant: \"number\";\n min?: number;\n max?: number;\n step?: number;\n }\n | {\n variant: \"select\";\n options: CellSelectOption[];\n }\n | {\n variant: \"multi-select\";\n options: CellSelectOption[];\n }\n | {\n variant: \"checkbox\";\n }\n | {\n variant: \"date\";\n };\n\nexport interface UpdateCell {\n rowIndex: number;\n columnId: string;\n value: unknown;\n}\n\ndeclare module \"@tanstack/react-table\" {\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\n interface ColumnMeta {\n label?: string;\n cell?: Cell;\n }\n\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\n interface TableMeta {\n dataGridRef?: React.RefObject;\n focusedCell?: CellPosition | null;\n editingCell?: CellPosition | null;\n selectionState?: SelectionState;\n searchOpen?: boolean;\n isScrolling?: boolean;\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n onDataUpdate?: (props: UpdateCell | Array) => void;\n onColumnClick?: (columnId: string) => void;\n onCellClick?: (\n rowIndex: number,\n columnId: string,\n event?: React.MouseEvent,\n ) => void;\n onCellDoubleClick?: (rowIndex: number, columnId: string) => void;\n onCellMouseDown?: (\n rowIndex: number,\n columnId: string,\n event: React.MouseEvent,\n ) => void;\n onCellMouseEnter?: (\n rowIndex: number,\n columnId: string,\n event: React.MouseEvent,\n ) => void;\n onCellMouseUp?: () => void;\n onCellContextMenu?: (\n rowIndex: number,\n columnId: string,\n event: React.MouseEvent,\n ) => void;\n onCellEditingStart?: (rowIndex: number, columnId: string) => void;\n onCellEditingStop?: (opts?: {\n direction?: NavigationDirection;\n moveToNextRow?: boolean;\n }) => void;\n contextMenu?: ContextMenuState;\n onContextMenuOpenChange?: (open: boolean) => void;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (value: RowHeightValue) => void;\n onRowSelect?: (\n rowIndex: number,\n checked: boolean,\n shiftKey: boolean,\n ) => void;\n }\n}\n\nexport interface CellPosition {\n rowIndex: number;\n columnId: string;\n}\n\nexport interface CellRange {\n start: CellPosition;\n end: CellPosition;\n}\n\nexport interface SelectionState {\n selectedCells: Set;\n selectionRange: CellRange | null;\n isSelecting: boolean;\n}\n\nexport interface ContextMenuState {\n open: boolean;\n x: number;\n y: number;\n}\n\nexport type NavigationDirection =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"home\"\n | \"end\"\n | \"ctrl+home\"\n | \"ctrl+end\"\n | \"pageup\"\n | \"pagedown\";\n\nexport interface SearchState {\n searchMatches: CellPosition[];\n matchIndex: number;\n searchOpen: boolean;\n onSearchOpenChange: (open: boolean) => void;\n searchQuery: string;\n onSearchQueryChange: (query: string) => void;\n onSearch: (query: string) => void;\n onNavigateToNextMatch: () => void;\n onNavigateToPrevMatch: () => void;\n}\n", "type": "registry:file", "target": "src/types/data-grid.ts" } diff --git a/public/r/data-table-action-bar.json b/public/r/data-table-action-bar.json index 9798db7d4..868db8c2f 100644 --- a/public/r/data-table-action-bar.json +++ b/public/r/data-table-action-bar.json @@ -16,7 +16,7 @@ "files": [ { "path": "src/components/data-table/data-table-action-bar.tsx", - "content": "\"use client\";\r\n\r\nimport type { Table } from \"@tanstack/react-table\";\r\nimport { Loader, X } from \"lucide-react\";\r\nimport { AnimatePresence, motion } from \"motion/react\";\r\nimport * as React from \"react\";\r\nimport * as ReactDOM from \"react-dom\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipTrigger,\r\n} from \"@/components/ui/tooltip\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface DataTableActionBarProps\r\n extends React.ComponentProps {\r\n table: Table;\r\n visible?: boolean;\r\n portalContainer?: Element | DocumentFragment | null;\r\n}\r\n\r\nfunction DataTableActionBar({\r\n table,\r\n visible: visibleProp,\r\n portalContainer: portalContainerProp,\r\n children,\r\n className,\r\n ...props\r\n}: DataTableActionBarProps) {\r\n const [mounted, setMounted] = React.useState(false);\r\n\r\n React.useLayoutEffect(() => {\r\n setMounted(true);\r\n }, []);\r\n\r\n React.useEffect(() => {\r\n function onKeyDown(event: KeyboardEvent) {\r\n if (event.key === \"Escape\") {\r\n table.toggleAllRowsSelected(false);\r\n }\r\n }\r\n\r\n window.addEventListener(\"keydown\", onKeyDown);\r\n return () => window.removeEventListener(\"keydown\", onKeyDown);\r\n }, [table]);\r\n\r\n const portalContainer =\r\n portalContainerProp ?? (mounted ? globalThis.document?.body : null);\r\n\r\n if (!portalContainer) return null;\r\n\r\n const visible =\r\n visibleProp ?? table.getFilteredSelectedRowModel().rows.length > 0;\r\n\r\n return ReactDOM.createPortal(\r\n \r\n {visible && (\r\n \r\n {children}\r\n \r\n )}\r\n ,\r\n portalContainer,\r\n );\r\n}\r\n\r\ninterface DataTableActionBarActionProps\r\n extends React.ComponentProps {\r\n tooltip?: string;\r\n isPending?: boolean;\r\n}\r\n\r\nfunction DataTableActionBarAction({\r\n size = \"sm\",\r\n tooltip,\r\n isPending,\r\n disabled,\r\n className,\r\n children,\r\n ...props\r\n}: DataTableActionBarActionProps) {\r\n const trigger = (\r\n svg]:size-3.5\",\r\n size === \"icon\" ? \"size-7\" : \"h-7\",\r\n className,\r\n )}\r\n disabled={disabled || isPending}\r\n {...props}\r\n >\r\n {isPending ? : children}\r\n \r\n );\r\n\r\n if (!tooltip) return trigger;\r\n\r\n return (\r\n \r\n {trigger}\r\n span]:hidden\"\r\n >\r\n

{tooltip}

\r\n \r\n
\r\n );\r\n}\r\n\r\ninterface DataTableActionBarSelectionProps {\r\n table: Table;\r\n}\r\n\r\nfunction DataTableActionBarSelection({\r\n table,\r\n}: DataTableActionBarSelectionProps) {\r\n const onClearSelection = React.useCallback(() => {\r\n table.toggleAllRowsSelected(false);\r\n }, [table]);\r\n\r\n return (\r\n
\r\n \r\n {table.getFilteredSelectedRowModel().rows.length} selected\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n span]:hidden\"\r\n >\r\n

Clear selection

\r\n \r\n \r\n Esc\r\n \r\n \r\n \r\n
\r\n
\r\n );\r\n}\r\n\r\nexport {\r\n DataTableActionBar,\r\n DataTableActionBarAction,\r\n DataTableActionBarSelection,\r\n};\r\n", + "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { Loader, X } from \"lucide-react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { Button } from \"@/components/ui/button\";\nimport { Separator } from \"@/components/ui/separator\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableActionBarProps\n extends React.ComponentProps {\n table: Table;\n visible?: boolean;\n portalContainer?: Element | DocumentFragment | null;\n}\n\nfunction DataTableActionBar({\n table,\n visible: visibleProp,\n portalContainer: portalContainerProp,\n children,\n className,\n ...props\n}: DataTableActionBarProps) {\n const [mounted, setMounted] = React.useState(false);\n\n React.useLayoutEffect(() => {\n setMounted(true);\n }, []);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (event.key === \"Escape\") {\n table.toggleAllRowsSelected(false);\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [table]);\n\n const portalContainer =\n portalContainerProp ?? (mounted ? globalThis.document?.body : null);\n\n if (!portalContainer) return null;\n\n const visible =\n visibleProp ?? table.getFilteredSelectedRowModel().rows.length > 0;\n\n return ReactDOM.createPortal(\n \n {visible && (\n \n {children}\n \n )}\n ,\n portalContainer,\n );\n}\n\ninterface DataTableActionBarActionProps\n extends React.ComponentProps {\n tooltip?: string;\n isPending?: boolean;\n}\n\nfunction DataTableActionBarAction({\n size = \"sm\",\n tooltip,\n isPending,\n disabled,\n className,\n children,\n ...props\n}: DataTableActionBarActionProps) {\n const trigger = (\n svg]:size-3.5\",\n size === \"icon\" ? \"size-7\" : \"h-7\",\n className,\n )}\n disabled={disabled || isPending}\n {...props}\n >\n {isPending ? : children}\n \n );\n\n if (!tooltip) return trigger;\n\n return (\n \n {trigger}\n span]:hidden\"\n >\n

{tooltip}

\n \n
\n );\n}\n\ninterface DataTableActionBarSelectionProps {\n table: Table;\n}\n\nfunction DataTableActionBarSelection({\n table,\n}: DataTableActionBarSelectionProps) {\n const onClearSelection = React.useCallback(() => {\n table.toggleAllRowsSelected(false);\n }, [table]);\n\n return (\n
\n \n {table.getFilteredSelectedRowModel().rows.length} selected\n \n \n \n \n \n \n \n \n span]:hidden\"\n >\n

Clear selection

\n \n \n Esc\n \n \n \n
\n
\n );\n}\n\nexport {\n DataTableActionBar,\n DataTableActionBarAction,\n DataTableActionBarSelection,\n};\n", "type": "registry:component", "target": "src/components/data-table/data-table-action-bar.tsx" } diff --git a/public/r/data-table-filter-list.json b/public/r/data-table-filter-list.json index d5b019430..f408d3fa8 100644 --- a/public/r/data-table-filter-list.json +++ b/public/r/data-table-filter-list.json @@ -22,7 +22,7 @@ "files": [ { "path": "src/components/data-table/data-table-filter-list.tsx", - "content": "\"use client\";\r\n\r\nimport type { Column, ColumnMeta, Table } from \"@tanstack/react-table\";\r\nimport {\r\n CalendarIcon,\r\n Check,\r\n ChevronsUpDown,\r\n GripVertical,\r\n ListFilter,\r\n Trash2,\r\n} from \"lucide-react\";\r\nimport { parseAsStringEnum, useQueryState } from \"nuqs\";\r\nimport * as React from \"react\";\r\n\r\nimport { DataTableRangeFilter } from \"@/components/data-table/data-table-range-filter\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Calendar } from \"@/components/ui/calendar\";\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n} from \"@/components/ui/command\";\r\nimport {\r\n Faceted,\r\n FacetedBadgeList,\r\n FacetedContent,\r\n FacetedEmpty,\r\n FacetedGroup,\r\n FacetedInput,\r\n FacetedItem,\r\n FacetedList,\r\n FacetedTrigger,\r\n} from \"@/components/ui/faceted\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport {\r\n Sortable,\r\n SortableContent,\r\n SortableItem,\r\n SortableItemHandle,\r\n SortableOverlay,\r\n} from \"@/components/ui/sortable\";\r\nimport { dataTableConfig } from \"@/config/data-table\";\r\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\r\nimport { getDefaultFilterOperator, getFilterOperators } from \"@/lib/data-table\";\r\nimport { formatDate } from \"@/lib/format\";\r\nimport { generateId } from \"@/lib/id\";\r\nimport { getFiltersStateParser } from \"@/lib/parsers\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport type {\r\n ExtendedColumnFilter,\r\n FilterOperator,\r\n JoinOperator,\r\n} from \"@/types/data-table\";\r\n\r\nconst DEBOUNCE_MS = 300;\r\nconst THROTTLE_MS = 50;\r\nconst FILTER_SHORTCUT_KEY = \"f\";\r\nconst REMOVE_FILTER_SHORTCUTS = [\"backspace\", \"delete\"];\r\n\r\ninterface DataTableFilterListProps\r\n extends React.ComponentProps {\r\n table: Table;\r\n debounceMs?: number;\r\n throttleMs?: number;\r\n shallow?: boolean;\r\n}\r\n\r\nexport function DataTableFilterList({\r\n table,\r\n debounceMs = DEBOUNCE_MS,\r\n throttleMs = THROTTLE_MS,\r\n shallow = true,\r\n ...props\r\n}: DataTableFilterListProps) {\r\n const id = React.useId();\r\n const labelId = React.useId();\r\n const descriptionId = React.useId();\r\n const [open, setOpen] = React.useState(false);\r\n const addButtonRef = React.useRef(null);\r\n\r\n const columns = React.useMemo(() => {\r\n return table\r\n .getAllColumns()\r\n .filter((column) => column.columnDef.enableColumnFilter);\r\n }, [table]);\r\n\r\n const [filters, setFilters] = useQueryState(\r\n table.options.meta?.queryKeys?.filters ?? \"filters\",\r\n getFiltersStateParser(columns.map((field) => field.id))\r\n .withDefault([])\r\n .withOptions({\r\n clearOnDefault: true,\r\n shallow,\r\n throttleMs,\r\n }),\r\n );\r\n const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);\r\n\r\n const [joinOperator, setJoinOperator] = useQueryState(\r\n table.options.meta?.queryKeys?.joinOperator ?? \"\",\r\n parseAsStringEnum([\"and\", \"or\"]).withDefault(\"and\").withOptions({\r\n clearOnDefault: true,\r\n shallow,\r\n }),\r\n );\r\n\r\n const onFilterAdd = React.useCallback(() => {\r\n const column = columns[0];\r\n\r\n if (!column) return;\r\n\r\n debouncedSetFilters([\r\n ...filters,\r\n {\r\n id: column.id as Extract,\r\n value: \"\",\r\n variant: column.columnDef.meta?.variant ?? \"text\",\r\n operator: getDefaultFilterOperator(\r\n column.columnDef.meta?.variant ?? \"text\",\r\n ),\r\n filterId: generateId({ length: 8 }),\r\n },\r\n ]);\r\n }, [columns, filters, debouncedSetFilters]);\r\n\r\n const onFilterUpdate = React.useCallback(\r\n (\r\n filterId: string,\r\n updates: Partial, \"filterId\">>,\r\n ) => {\r\n debouncedSetFilters((prevFilters) => {\r\n const updatedFilters = prevFilters.map((filter) => {\r\n if (filter.filterId === filterId) {\r\n return { ...filter, ...updates } as ExtendedColumnFilter;\r\n }\r\n return filter;\r\n });\r\n return updatedFilters;\r\n });\r\n },\r\n [debouncedSetFilters],\r\n );\r\n\r\n const onFilterRemove = React.useCallback(\r\n (filterId: string) => {\r\n const updatedFilters = filters.filter(\r\n (filter) => filter.filterId !== filterId,\r\n );\r\n void setFilters(updatedFilters);\r\n requestAnimationFrame(() => {\r\n addButtonRef.current?.focus();\r\n });\r\n },\r\n [filters, setFilters],\r\n );\r\n\r\n const onFiltersReset = React.useCallback(() => {\r\n void setFilters(null);\r\n void setJoinOperator(\"and\");\r\n }, [setFilters, setJoinOperator]);\r\n\r\n React.useEffect(() => {\r\n function onKeyDown(event: KeyboardEvent) {\r\n if (\r\n event.target instanceof HTMLInputElement ||\r\n event.target instanceof HTMLTextAreaElement ||\r\n (event.target instanceof HTMLElement &&\r\n event.target.contentEditable === \"true\")\r\n ) {\r\n return;\r\n }\r\n\r\n if (\r\n event.key.toLowerCase() === FILTER_SHORTCUT_KEY &&\r\n (event.ctrlKey || event.metaKey) &&\r\n event.shiftKey\r\n ) {\r\n event.preventDefault();\r\n setOpen((prev) => !prev);\r\n }\r\n }\r\n\r\n window.addEventListener(\"keydown\", onKeyDown);\r\n return () => window.removeEventListener(\"keydown\", onKeyDown);\r\n }, []);\r\n\r\n const onTriggerKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (\r\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\r\n filters.length > 0\r\n ) {\r\n event.preventDefault();\r\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\r\n }\r\n },\r\n [filters, onFilterRemove],\r\n );\r\n\r\n return (\r\n item.filterId}\r\n >\r\n \r\n \r\n \r\n \r\n Filter\r\n {filters.length > 0 && (\r\n \r\n {filters.length}\r\n \r\n )}\r\n \r\n \r\n \r\n
\r\n

\r\n {filters.length > 0 ? \"Filters\" : \"No filters applied\"}\r\n

\r\n 0 && \"sr-only\",\r\n )}\r\n >\r\n {filters.length > 0\r\n ? \"Modify filters to refine your rows.\"\r\n : \"Add filters to refine your rows.\"}\r\n

\r\n
\r\n {filters.length > 0 ? (\r\n \r\n
    \r\n {filters.map((filter, index) => (\r\n \r\n key={filter.filterId}\r\n filter={filter}\r\n index={index}\r\n filterItemId={`${id}-filter-${filter.filterId}`}\r\n joinOperator={joinOperator}\r\n setJoinOperator={setJoinOperator}\r\n columns={columns}\r\n onFilterUpdate={onFilterUpdate}\r\n onFilterRemove={onFilterRemove}\r\n />\r\n ))}\r\n
\r\n
\r\n ) : null}\r\n
\r\n \r\n Add filter\r\n \r\n {filters.length > 0 ? (\r\n \r\n Reset filters\r\n \r\n ) : null}\r\n
\r\n \r\n
\r\n \r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n );\r\n}\r\n\r\ninterface DataTableFilterItemProps {\r\n filter: ExtendedColumnFilter;\r\n index: number;\r\n filterItemId: string;\r\n joinOperator: JoinOperator;\r\n setJoinOperator: (value: JoinOperator) => void;\r\n columns: Column[];\r\n onFilterUpdate: (\r\n filterId: string,\r\n updates: Partial, \"filterId\">>,\r\n ) => void;\r\n onFilterRemove: (filterId: string) => void;\r\n}\r\n\r\nfunction DataTableFilterItem({\r\n filter,\r\n index,\r\n filterItemId,\r\n joinOperator,\r\n setJoinOperator,\r\n columns,\r\n onFilterUpdate,\r\n onFilterRemove,\r\n}: DataTableFilterItemProps) {\r\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\r\n const [showOperatorSelector, setShowOperatorSelector] = React.useState(false);\r\n const [showValueSelector, setShowValueSelector] = React.useState(false);\r\n\r\n const column = columns.find((column) => column.id === filter.id);\r\n\r\n const joinOperatorListboxId = `${filterItemId}-join-operator-listbox`;\r\n const fieldListboxId = `${filterItemId}-field-listbox`;\r\n const operatorListboxId = `${filterItemId}-operator-listbox`;\r\n const inputId = `${filterItemId}-input`;\r\n\r\n const columnMeta = column?.columnDef.meta;\r\n const filterOperators = getFilterOperators(filter.variant);\r\n\r\n const onItemKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (\r\n event.target instanceof HTMLInputElement ||\r\n event.target instanceof HTMLTextAreaElement\r\n ) {\r\n return;\r\n }\r\n\r\n if (showFieldSelector || showOperatorSelector || showValueSelector) {\r\n return;\r\n }\r\n\r\n if (REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase())) {\r\n event.preventDefault();\r\n onFilterRemove(filter.filterId);\r\n }\r\n },\r\n [\r\n filter.filterId,\r\n showFieldSelector,\r\n showOperatorSelector,\r\n showValueSelector,\r\n onFilterRemove,\r\n ],\r\n );\r\n\r\n if (!column) return null;\r\n\r\n return (\r\n \r\n \r\n
\r\n {index === 0 ? (\r\n Where\r\n ) : index === 1 ? (\r\n setJoinOperator(value)}\r\n >\r\n \r\n \r\n \r\n \r\n {dataTableConfig.joinOperators.map((joinOperator) => (\r\n \r\n {joinOperator}\r\n \r\n ))}\r\n \r\n \r\n ) : (\r\n \r\n {joinOperator}\r\n \r\n )}\r\n
\r\n \r\n \r\n \r\n \r\n {columns.find((column) => column.id === filter.id)?.columnDef\r\n .meta?.label ?? \"Select field\"}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n No fields found.\r\n \r\n {columns.map((column) => (\r\n {\r\n onFilterUpdate(filter.filterId, {\r\n id: value as Extract,\r\n variant: column.columnDef.meta?.variant ?? \"text\",\r\n operator: getDefaultFilterOperator(\r\n column.columnDef.meta?.variant ?? \"text\",\r\n ),\r\n value: \"\",\r\n });\r\n\r\n setShowFieldSelector(false);\r\n }}\r\n >\r\n \r\n {column.columnDef.meta?.label}\r\n \r\n \r\n \r\n ))}\r\n \r\n \r\n \r\n \r\n \r\n \r\n onFilterUpdate(filter.filterId, {\r\n operator: value,\r\n value:\r\n value === \"isEmpty\" || value === \"isNotEmpty\"\r\n ? \"\"\r\n : filter.value,\r\n })\r\n }\r\n >\r\n \r\n
\r\n \r\n
\r\n \r\n \r\n {filterOperators.map((operator) => (\r\n \r\n {operator.label}\r\n \r\n ))}\r\n \r\n \r\n
\r\n {onFilterInputRender({\r\n filter,\r\n inputId,\r\n column,\r\n columnMeta,\r\n onFilterUpdate,\r\n showValueSelector,\r\n setShowValueSelector,\r\n })}\r\n
\r\n onFilterRemove(filter.filterId)}\r\n >\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n );\r\n}\r\n\r\nfunction onFilterInputRender({\r\n filter,\r\n inputId,\r\n column,\r\n columnMeta,\r\n onFilterUpdate,\r\n showValueSelector,\r\n setShowValueSelector,\r\n}: {\r\n filter: ExtendedColumnFilter;\r\n inputId: string;\r\n column: Column;\r\n columnMeta?: ColumnMeta;\r\n onFilterUpdate: (\r\n filterId: string,\r\n updates: Partial, \"filterId\">>,\r\n ) => void;\r\n showValueSelector: boolean;\r\n setShowValueSelector: (value: boolean) => void;\r\n}) {\r\n if (filter.operator === \"isEmpty\" || filter.operator === \"isNotEmpty\") {\r\n return (\r\n \r\n );\r\n }\r\n\r\n switch (filter.variant) {\r\n case \"text\":\r\n case \"number\":\r\n case \"range\": {\r\n if (\r\n (filter.variant === \"range\" && filter.operator === \"isBetween\") ||\r\n filter.operator === \"isBetween\"\r\n ) {\r\n return (\r\n \r\n );\r\n }\r\n\r\n const isNumber =\r\n filter.variant === \"number\" || filter.variant === \"range\";\r\n\r\n return (\r\n \r\n onFilterUpdate(filter.filterId, {\r\n value: event.target.value,\r\n })\r\n }\r\n />\r\n );\r\n }\r\n\r\n case \"boolean\": {\r\n if (Array.isArray(filter.value)) return null;\r\n\r\n const inputListboxId = `${inputId}-listbox`;\r\n\r\n return (\r\n \r\n onFilterUpdate(filter.filterId, {\r\n value,\r\n })\r\n }\r\n >\r\n \r\n \r\n \r\n \r\n True\r\n False\r\n \r\n \r\n );\r\n }\r\n\r\n case \"select\":\r\n case \"multiSelect\": {\r\n const inputListboxId = `${inputId}-listbox`;\r\n\r\n const multiple = filter.variant === \"multiSelect\";\r\n const selectedValues = multiple\r\n ? Array.isArray(filter.value)\r\n ? filter.value\r\n : []\r\n : typeof filter.value === \"string\"\r\n ? filter.value\r\n : undefined;\r\n\r\n return (\r\n {\r\n onFilterUpdate(filter.filterId, {\r\n value,\r\n });\r\n }}\r\n multiple={multiple}\r\n >\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n No options found.\r\n \r\n {columnMeta?.options?.map((option) => (\r\n \r\n {option.icon && }\r\n {option.label}\r\n {option.count && (\r\n \r\n {option.count}\r\n \r\n )}\r\n \r\n ))}\r\n \r\n \r\n \r\n \r\n );\r\n }\r\n\r\n case \"date\":\r\n case \"dateRange\": {\r\n const inputListboxId = `${inputId}-listbox`;\r\n\r\n const dateValue = Array.isArray(filter.value)\r\n ? filter.value.filter(Boolean)\r\n : [filter.value, filter.value].filter(Boolean);\r\n\r\n const displayValue =\r\n filter.operator === \"isBetween\" && dateValue.length === 2\r\n ? `${formatDate(new Date(Number(dateValue[0])))} - ${formatDate(\r\n new Date(Number(dateValue[1])),\r\n )}`\r\n : dateValue[0]\r\n ? formatDate(new Date(Number(dateValue[0])))\r\n : \"Pick a date\";\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n {displayValue}\r\n \r\n \r\n \r\n {filter.operator === \"isBetween\" ? (\r\n {\r\n onFilterUpdate(filter.filterId, {\r\n value: date\r\n ? [\r\n (date.from?.getTime() ?? \"\").toString(),\r\n (date.to?.getTime() ?? \"\").toString(),\r\n ]\r\n : [],\r\n });\r\n }}\r\n />\r\n ) : (\r\n {\r\n onFilterUpdate(filter.filterId, {\r\n value: (date?.getTime() ?? \"\").toString(),\r\n });\r\n }}\r\n />\r\n )}\r\n \r\n \r\n );\r\n }\r\n\r\n default:\r\n return null;\r\n }\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Column, ColumnMeta, Table } from \"@tanstack/react-table\";\nimport {\n CalendarIcon,\n Check,\n ChevronsUpDown,\n GripVertical,\n ListFilter,\n Trash2,\n} from \"lucide-react\";\nimport { parseAsStringEnum, useQueryState } from \"nuqs\";\nimport * as React from \"react\";\n\nimport { DataTableRangeFilter } from \"@/components/data-table/data-table-range-filter\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport {\n Faceted,\n FacetedBadgeList,\n FacetedContent,\n FacetedEmpty,\n FacetedGroup,\n FacetedInput,\n FacetedItem,\n FacetedList,\n FacetedTrigger,\n} from \"@/components/ui/faceted\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n} from \"@/components/ui/sortable\";\nimport { dataTableConfig } from \"@/config/data-table\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getDefaultFilterOperator, getFilterOperators } from \"@/lib/data-table\";\nimport { formatDate } from \"@/lib/format\";\nimport { generateId } from \"@/lib/id\";\nimport { getFiltersStateParser } from \"@/lib/parsers\";\nimport { cn } from \"@/lib/utils\";\nimport type {\n ExtendedColumnFilter,\n FilterOperator,\n JoinOperator,\n} from \"@/types/data-table\";\n\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\nconst FILTER_SHORTCUT_KEY = \"f\";\nconst REMOVE_FILTER_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableFilterListProps\n extends React.ComponentProps {\n table: Table;\n debounceMs?: number;\n throttleMs?: number;\n shallow?: boolean;\n}\n\nexport function DataTableFilterList({\n table,\n debounceMs = DEBOUNCE_MS,\n throttleMs = THROTTLE_MS,\n shallow = true,\n ...props\n}: DataTableFilterListProps) {\n const id = React.useId();\n const labelId = React.useId();\n const descriptionId = React.useId();\n const [open, setOpen] = React.useState(false);\n const addButtonRef = React.useRef(null);\n\n const columns = React.useMemo(() => {\n return table\n .getAllColumns()\n .filter((column) => column.columnDef.enableColumnFilter);\n }, [table]);\n\n const [filters, setFilters] = useQueryState(\n table.options.meta?.queryKeys?.filters ?? \"filters\",\n getFiltersStateParser(columns.map((field) => field.id))\n .withDefault([])\n .withOptions({\n clearOnDefault: true,\n shallow,\n throttleMs,\n }),\n );\n const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);\n\n const [joinOperator, setJoinOperator] = useQueryState(\n table.options.meta?.queryKeys?.joinOperator ?? \"\",\n parseAsStringEnum([\"and\", \"or\"]).withDefault(\"and\").withOptions({\n clearOnDefault: true,\n shallow,\n }),\n );\n\n const onFilterAdd = React.useCallback(() => {\n const column = columns[0];\n\n if (!column) return;\n\n debouncedSetFilters([\n ...filters,\n {\n id: column.id as Extract,\n value: \"\",\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n filterId: generateId({ length: 8 }),\n },\n ]);\n }, [columns, filters, debouncedSetFilters]);\n\n const onFilterUpdate = React.useCallback(\n (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => {\n debouncedSetFilters((prevFilters) => {\n const updatedFilters = prevFilters.map((filter) => {\n if (filter.filterId === filterId) {\n return { ...filter, ...updates } as ExtendedColumnFilter;\n }\n return filter;\n });\n return updatedFilters;\n });\n },\n [debouncedSetFilters],\n );\n\n const onFilterRemove = React.useCallback(\n (filterId: string) => {\n const updatedFilters = filters.filter(\n (filter) => filter.filterId !== filterId,\n );\n void setFilters(updatedFilters);\n requestAnimationFrame(() => {\n addButtonRef.current?.focus();\n });\n },\n [filters, setFilters],\n );\n\n const onFiltersReset = React.useCallback(() => {\n void setFilters(null);\n void setJoinOperator(\"and\");\n }, [setFilters, setJoinOperator]);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement ||\n (event.target instanceof HTMLElement &&\n event.target.contentEditable === \"true\")\n ) {\n return;\n }\n\n if (\n event.key.toLowerCase() === FILTER_SHORTCUT_KEY &&\n (event.ctrlKey || event.metaKey) &&\n event.shiftKey\n ) {\n event.preventDefault();\n setOpen((prev) => !prev);\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, []);\n\n const onTriggerKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\n filters.length > 0\n ) {\n event.preventDefault();\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\n }\n },\n [filters, onFilterRemove],\n );\n\n return (\n item.filterId}\n >\n \n \n \n \n Filter\n {filters.length > 0 && (\n \n {filters.length}\n \n )}\n \n \n \n
\n

\n {filters.length > 0 ? \"Filters\" : \"No filters applied\"}\n

\n 0 && \"sr-only\",\n )}\n >\n {filters.length > 0\n ? \"Modify filters to refine your rows.\"\n : \"Add filters to refine your rows.\"}\n

\n
\n {filters.length > 0 ? (\n \n
    \n {filters.map((filter, index) => (\n \n key={filter.filterId}\n filter={filter}\n index={index}\n filterItemId={`${id}-filter-${filter.filterId}`}\n joinOperator={joinOperator}\n setJoinOperator={setJoinOperator}\n columns={columns}\n onFilterUpdate={onFilterUpdate}\n onFilterRemove={onFilterRemove}\n />\n ))}\n
\n
\n ) : null}\n
\n \n Add filter\n \n {filters.length > 0 ? (\n \n Reset filters\n \n ) : null}\n
\n \n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n \n );\n}\n\ninterface DataTableFilterItemProps {\n filter: ExtendedColumnFilter;\n index: number;\n filterItemId: string;\n joinOperator: JoinOperator;\n setJoinOperator: (value: JoinOperator) => void;\n columns: Column[];\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n onFilterRemove: (filterId: string) => void;\n}\n\nfunction DataTableFilterItem({\n filter,\n index,\n filterItemId,\n joinOperator,\n setJoinOperator,\n columns,\n onFilterUpdate,\n onFilterRemove,\n}: DataTableFilterItemProps) {\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n const [showOperatorSelector, setShowOperatorSelector] = React.useState(false);\n const [showValueSelector, setShowValueSelector] = React.useState(false);\n\n const column = columns.find((column) => column.id === filter.id);\n\n const joinOperatorListboxId = `${filterItemId}-join-operator-listbox`;\n const fieldListboxId = `${filterItemId}-field-listbox`;\n const operatorListboxId = `${filterItemId}-operator-listbox`;\n const inputId = `${filterItemId}-input`;\n\n const columnMeta = column?.columnDef.meta;\n const filterOperators = getFilterOperators(filter.variant);\n\n const onItemKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (showFieldSelector || showOperatorSelector || showValueSelector) {\n return;\n }\n\n if (REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase())) {\n event.preventDefault();\n onFilterRemove(filter.filterId);\n }\n },\n [\n filter.filterId,\n showFieldSelector,\n showOperatorSelector,\n showValueSelector,\n onFilterRemove,\n ],\n );\n\n if (!column) return null;\n\n return (\n \n \n
\n {index === 0 ? (\n Where\n ) : index === 1 ? (\n setJoinOperator(value)}\n >\n \n \n \n \n {dataTableConfig.joinOperators.map((joinOperator) => (\n \n {joinOperator}\n \n ))}\n \n \n ) : (\n \n {joinOperator}\n \n )}\n
\n \n \n \n \n {columns.find((column) => column.id === filter.id)?.columnDef\n .meta?.label ?? \"Select field\"}\n \n \n \n \n \n \n \n \n No fields found.\n \n {columns.map((column) => (\n {\n onFilterUpdate(filter.filterId, {\n id: value as Extract,\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n value: \"\",\n });\n\n setShowFieldSelector(false);\n }}\n >\n \n {column.columnDef.meta?.label}\n \n \n \n ))}\n \n \n \n \n \n \n onFilterUpdate(filter.filterId, {\n operator: value,\n value:\n value === \"isEmpty\" || value === \"isNotEmpty\"\n ? \"\"\n : filter.value,\n })\n }\n >\n \n
\n \n
\n \n \n {filterOperators.map((operator) => (\n \n {operator.label}\n \n ))}\n \n \n
\n {onFilterInputRender({\n filter,\n inputId,\n column,\n columnMeta,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n })}\n
\n onFilterRemove(filter.filterId)}\n >\n \n \n \n \n \n \n
\n );\n}\n\nfunction onFilterInputRender({\n filter,\n inputId,\n column,\n columnMeta,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n}: {\n filter: ExtendedColumnFilter;\n inputId: string;\n column: Column;\n columnMeta?: ColumnMeta;\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n showValueSelector: boolean;\n setShowValueSelector: (value: boolean) => void;\n}) {\n if (filter.operator === \"isEmpty\" || filter.operator === \"isNotEmpty\") {\n return (\n \n );\n }\n\n switch (filter.variant) {\n case \"text\":\n case \"number\":\n case \"range\": {\n if (\n (filter.variant === \"range\" && filter.operator === \"isBetween\") ||\n filter.operator === \"isBetween\"\n ) {\n return (\n \n );\n }\n\n const isNumber =\n filter.variant === \"number\" || filter.variant === \"range\";\n\n return (\n \n onFilterUpdate(filter.filterId, {\n value: event.target.value,\n })\n }\n />\n );\n }\n\n case \"boolean\": {\n if (Array.isArray(filter.value)) return null;\n\n const inputListboxId = `${inputId}-listbox`;\n\n return (\n \n onFilterUpdate(filter.filterId, {\n value,\n })\n }\n >\n \n \n \n \n True\n False\n \n \n );\n }\n\n case \"select\":\n case \"multiSelect\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const multiple = filter.variant === \"multiSelect\";\n const selectedValues = multiple\n ? Array.isArray(filter.value)\n ? filter.value\n : []\n : typeof filter.value === \"string\"\n ? filter.value\n : undefined;\n\n return (\n {\n onFilterUpdate(filter.filterId, {\n value,\n });\n }}\n multiple={multiple}\n >\n \n \n \n \n \n \n \n \n No options found.\n \n {columnMeta?.options?.map((option) => (\n \n {option.icon && }\n {option.label}\n {option.count && (\n \n {option.count}\n \n )}\n \n ))}\n \n \n \n \n );\n }\n\n case \"date\":\n case \"dateRange\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const dateValue = Array.isArray(filter.value)\n ? filter.value.filter(Boolean)\n : [filter.value, filter.value].filter(Boolean);\n\n const displayValue =\n filter.operator === \"isBetween\" && dateValue.length === 2\n ? `${formatDate(new Date(Number(dateValue[0])))} - ${formatDate(\n new Date(Number(dateValue[1])),\n )}`\n : dateValue[0]\n ? formatDate(new Date(Number(dateValue[0])))\n : \"Pick a date\";\n\n return (\n \n \n \n \n {displayValue}\n \n \n \n {filter.operator === \"isBetween\" ? (\n {\n onFilterUpdate(filter.filterId, {\n value: date\n ? [\n (date.from?.getTime() ?? \"\").toString(),\n (date.to?.getTime() ?? \"\").toString(),\n ]\n : [],\n });\n }}\n />\n ) : (\n {\n onFilterUpdate(filter.filterId, {\n value: (date?.getTime() ?? \"\").toString(),\n });\n }}\n />\n )}\n \n \n );\n }\n\n default:\n return null;\n }\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-filter-list.tsx" }, @@ -34,19 +34,19 @@ }, { "path": "src/components/data-table/data-table-advanced-toolbar.tsx", - "content": "\"use client\";\r\n\r\nimport type { Table } from \"@tanstack/react-table\";\r\nimport type * as React from \"react\";\r\n\r\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface DataTableAdvancedToolbarProps\r\n extends React.ComponentProps<\"div\"> {\r\n table: Table;\r\n}\r\n\r\nexport function DataTableAdvancedToolbar({\r\n table,\r\n children,\r\n className,\r\n ...props\r\n}: DataTableAdvancedToolbarProps) {\r\n return (\r\n \r\n
{children}
\r\n
\r\n \r\n
\r\n
\r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableAdvancedToolbarProps\n extends React.ComponentProps<\"div\"> {\n table: Table;\n}\n\nexport function DataTableAdvancedToolbar({\n table,\n children,\n className,\n ...props\n}: DataTableAdvancedToolbarProps) {\n return (\n \n
{children}
\n
\n \n
\n
\n );\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-advanced-toolbar.tsx" }, { "path": "src/components/ui/sortable.tsx", - "content": "\"use client\";\r\n\r\nimport {\r\n type Announcements,\r\n closestCenter,\r\n closestCorners,\r\n DndContext,\r\n type DndContextProps,\r\n type DragEndEvent,\r\n type DraggableAttributes,\r\n type DraggableSyntheticListeners,\r\n DragOverlay,\r\n type DragStartEvent,\r\n type DropAnimation,\r\n defaultDropAnimationSideEffects,\r\n KeyboardSensor,\r\n MouseSensor,\r\n type ScreenReaderInstructions,\r\n TouchSensor,\r\n type UniqueIdentifier,\r\n useSensor,\r\n useSensors,\r\n} from \"@dnd-kit/core\";\r\nimport {\r\n restrictToHorizontalAxis,\r\n restrictToParentElement,\r\n restrictToVerticalAxis,\r\n} from \"@dnd-kit/modifiers\";\r\nimport {\r\n arrayMove,\r\n horizontalListSortingStrategy,\r\n SortableContext,\r\n type SortableContextProps,\r\n sortableKeyboardCoordinates,\r\n useSortable,\r\n verticalListSortingStrategy,\r\n} from \"@dnd-kit/sortable\";\r\nimport { CSS } from \"@dnd-kit/utilities\";\r\nimport { Slot } from \"@radix-ui/react-slot\";\r\nimport * as React from \"react\";\r\nimport * as ReactDOM from \"react-dom\";\r\nimport { useComposedRefs } from \"@/lib/compose-refs\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nconst orientationConfig = {\r\n vertical: {\r\n modifiers: [restrictToVerticalAxis, restrictToParentElement],\r\n strategy: verticalListSortingStrategy,\r\n collisionDetection: closestCenter,\r\n },\r\n horizontal: {\r\n modifiers: [restrictToHorizontalAxis, restrictToParentElement],\r\n strategy: horizontalListSortingStrategy,\r\n collisionDetection: closestCenter,\r\n },\r\n mixed: {\r\n modifiers: [restrictToParentElement],\r\n strategy: undefined,\r\n collisionDetection: closestCorners,\r\n },\r\n};\r\n\r\nconst ROOT_NAME = \"Sortable\";\r\nconst CONTENT_NAME = \"SortableContent\";\r\nconst ITEM_NAME = \"SortableItem\";\r\nconst ITEM_HANDLE_NAME = \"SortableItemHandle\";\r\nconst OVERLAY_NAME = \"SortableOverlay\";\r\n\r\ninterface SortableRootContextValue {\r\n id: string;\r\n items: UniqueIdentifier[];\r\n modifiers: DndContextProps[\"modifiers\"];\r\n strategy: SortableContextProps[\"strategy\"];\r\n activeId: UniqueIdentifier | null;\r\n setActiveId: (id: UniqueIdentifier | null) => void;\r\n getItemValue: (item: T) => UniqueIdentifier;\r\n flatCursor: boolean;\r\n}\r\n\r\nconst SortableRootContext =\r\n React.createContext | null>(null);\r\n\r\nfunction useSortableContext(consumerName: string) {\r\n const context = React.useContext(SortableRootContext);\r\n if (!context) {\r\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\r\n }\r\n return context;\r\n}\r\n\r\ninterface GetItemValue {\r\n /**\r\n * Callback that returns a unique identifier for each sortable item. Required for array of objects.\r\n * @example getItemValue={(item) => item.id}\r\n */\r\n getItemValue: (item: T) => UniqueIdentifier;\r\n}\r\n\r\ntype SortableRootProps = DndContextProps &\r\n (T extends object ? GetItemValue : Partial>) & {\r\n value: T[];\r\n onValueChange?: (items: T[]) => void;\r\n onMove?: (\r\n event: DragEndEvent & { activeIndex: number; overIndex: number },\r\n ) => void;\r\n strategy?: SortableContextProps[\"strategy\"];\r\n orientation?: \"vertical\" | \"horizontal\" | \"mixed\";\r\n flatCursor?: boolean;\r\n };\r\n\r\nfunction SortableRoot(props: SortableRootProps) {\r\n const {\r\n value,\r\n onValueChange,\r\n collisionDetection,\r\n modifiers,\r\n strategy,\r\n onMove,\r\n orientation = \"vertical\",\r\n flatCursor = false,\r\n getItemValue: getItemValueProp,\r\n accessibility,\r\n onDragStart: onDragStartProp,\r\n onDragEnd: onDragEndProp,\r\n onDragCancel: onDragCancelProp,\r\n ...sortableProps\r\n } = props;\r\n\r\n const id = React.useId();\r\n const [activeId, setActiveId] = React.useState(null);\r\n\r\n const sensors = useSensors(\r\n useSensor(MouseSensor),\r\n useSensor(TouchSensor),\r\n useSensor(KeyboardSensor, {\r\n coordinateGetter: sortableKeyboardCoordinates,\r\n }),\r\n );\r\n const config = React.useMemo(\r\n () => orientationConfig[orientation],\r\n [orientation],\r\n );\r\n\r\n const getItemValue = React.useCallback(\r\n (item: T): UniqueIdentifier => {\r\n if (typeof item === \"object\" && !getItemValueProp) {\r\n throw new Error(\"getItemValue is required when using array of objects\");\r\n }\r\n return getItemValueProp\r\n ? getItemValueProp(item)\r\n : (item as UniqueIdentifier);\r\n },\r\n [getItemValueProp],\r\n );\r\n\r\n const items = React.useMemo(() => {\r\n return value.map((item) => getItemValue(item));\r\n }, [value, getItemValue]);\r\n\r\n const onDragStart = React.useCallback(\r\n (event: DragStartEvent) => {\r\n onDragStartProp?.(event);\r\n\r\n if (event.activatorEvent.defaultPrevented) return;\r\n\r\n setActiveId(event.active.id);\r\n },\r\n [onDragStartProp],\r\n );\r\n\r\n const onDragEnd = React.useCallback(\r\n (event: DragEndEvent) => {\r\n onDragEndProp?.(event);\r\n\r\n if (event.activatorEvent.defaultPrevented) return;\r\n\r\n const { active, over } = event;\r\n if (over && active.id !== over?.id) {\r\n const activeIndex = value.findIndex(\r\n (item) => getItemValue(item) === active.id,\r\n );\r\n const overIndex = value.findIndex(\r\n (item) => getItemValue(item) === over.id,\r\n );\r\n\r\n if (onMove) {\r\n onMove({ ...event, activeIndex, overIndex });\r\n } else {\r\n onValueChange?.(arrayMove(value, activeIndex, overIndex));\r\n }\r\n }\r\n setActiveId(null);\r\n },\r\n [value, onValueChange, onMove, getItemValue, onDragEndProp],\r\n );\r\n\r\n const onDragCancel = React.useCallback(\r\n (event: DragEndEvent) => {\r\n onDragCancelProp?.(event);\r\n\r\n if (event.activatorEvent.defaultPrevented) return;\r\n\r\n setActiveId(null);\r\n },\r\n [onDragCancelProp],\r\n );\r\n\r\n const announcements: Announcements = React.useMemo(\r\n () => ({\r\n onDragStart({ active }) {\r\n const activeValue = active.id.toString();\r\n return `Grabbed sortable item \"${activeValue}\". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;\r\n },\r\n onDragOver({ active, over }) {\r\n if (over) {\r\n const overIndex = over.data.current?.sortable.index ?? 0;\r\n const activeIndex = active.data.current?.sortable.index ?? 0;\r\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\r\n const activeValue = active.id.toString();\r\n return `Sortable item \"${activeValue}\" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\r\n }\r\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\r\n },\r\n onDragEnd({ active, over }) {\r\n const activeValue = active.id.toString();\r\n if (over) {\r\n const overIndex = over.data.current?.sortable.index ?? 0;\r\n return `Sortable item \"${activeValue}\" dropped at position ${overIndex + 1} of ${value.length}.`;\r\n }\r\n return `Sortable item \"${activeValue}\" dropped. No changes were made.`;\r\n },\r\n onDragCancel({ active }) {\r\n const activeIndex = active.data.current?.sortable.index ?? 0;\r\n const activeValue = active.id.toString();\r\n return `Sorting cancelled. Sortable item \"${activeValue}\" returned to position ${activeIndex + 1} of ${value.length}.`;\r\n },\r\n onDragMove({ active, over }) {\r\n if (over) {\r\n const overIndex = over.data.current?.sortable.index ?? 0;\r\n const activeIndex = active.data.current?.sortable.index ?? 0;\r\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\r\n const activeValue = active.id.toString();\r\n return `Sortable item \"${activeValue}\" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\r\n }\r\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\r\n },\r\n }),\r\n [value],\r\n );\r\n\r\n const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(\r\n () => ({\r\n draggable: `\r\n To pick up a sortable item, press space or enter.\r\n While dragging, use the ${orientation === \"vertical\" ? \"up and down\" : orientation === \"horizontal\" ? \"left and right\" : \"arrow\"} keys to move the item.\r\n Press space or enter again to drop the item in its new position, or press escape to cancel.\r\n `,\r\n }),\r\n [orientation],\r\n );\r\n\r\n const contextValue = React.useMemo(\r\n () => ({\r\n id,\r\n items,\r\n modifiers: modifiers ?? config.modifiers,\r\n strategy: strategy ?? config.strategy,\r\n activeId,\r\n setActiveId,\r\n getItemValue,\r\n flatCursor,\r\n }),\r\n [\r\n id,\r\n items,\r\n modifiers,\r\n strategy,\r\n config.modifiers,\r\n config.strategy,\r\n activeId,\r\n getItemValue,\r\n flatCursor,\r\n ],\r\n );\r\n\r\n return (\r\n }\r\n >\r\n \r\n \r\n );\r\n}\r\n\r\nconst SortableContentContext = React.createContext(false);\r\n\r\ninterface SortableContentProps extends React.ComponentProps<\"div\"> {\r\n strategy?: SortableContextProps[\"strategy\"];\r\n children: React.ReactNode;\r\n asChild?: boolean;\r\n withoutSlot?: boolean;\r\n}\r\n\r\nfunction SortableContent(props: SortableContentProps) {\r\n const {\r\n strategy: strategyProp,\r\n asChild,\r\n withoutSlot,\r\n children,\r\n ref,\r\n ...contentProps\r\n } = props;\r\n\r\n const context = useSortableContext(CONTENT_NAME);\r\n\r\n const ContentPrimitive = asChild ? Slot : \"div\";\r\n\r\n return (\r\n \r\n \r\n {withoutSlot ? (\r\n children\r\n ) : (\r\n \r\n {children}\r\n \r\n )}\r\n \r\n \r\n );\r\n}\r\n\r\ninterface SortableItemContextValue {\r\n id: string;\r\n attributes: DraggableAttributes;\r\n listeners: DraggableSyntheticListeners | undefined;\r\n setActivatorNodeRef: (node: HTMLElement | null) => void;\r\n isDragging?: boolean;\r\n disabled?: boolean;\r\n}\r\n\r\nconst SortableItemContext =\r\n React.createContext(null);\r\n\r\nfunction useSortableItemContext(consumerName: string) {\r\n const context = React.useContext(SortableItemContext);\r\n if (!context) {\r\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\r\n }\r\n return context;\r\n}\r\n\r\ninterface SortableItemProps extends React.ComponentProps<\"div\"> {\r\n value: UniqueIdentifier;\r\n asHandle?: boolean;\r\n asChild?: boolean;\r\n disabled?: boolean;\r\n}\r\n\r\nfunction SortableItem(props: SortableItemProps) {\r\n const {\r\n value,\r\n style,\r\n asHandle,\r\n asChild,\r\n disabled,\r\n className,\r\n ref,\r\n ...itemProps\r\n } = props;\r\n\r\n const inSortableContent = React.useContext(SortableContentContext);\r\n const inSortableOverlay = React.useContext(SortableOverlayContext);\r\n\r\n if (!inSortableContent && !inSortableOverlay) {\r\n throw new Error(\r\n `\\`${ITEM_NAME}\\` must be used within \\`${CONTENT_NAME}\\` or \\`${OVERLAY_NAME}\\``,\r\n );\r\n }\r\n\r\n if (value === \"\") {\r\n throw new Error(`\\`${ITEM_NAME}\\` value cannot be an empty string`);\r\n }\r\n\r\n const context = useSortableContext(ITEM_NAME);\r\n const id = React.useId();\r\n const {\r\n attributes,\r\n listeners,\r\n setNodeRef,\r\n setActivatorNodeRef,\r\n transform,\r\n transition,\r\n isDragging,\r\n } = useSortable({ id: value, disabled });\r\n\r\n const composedRef = useComposedRefs(ref, (node) => {\r\n if (disabled) return;\r\n setNodeRef(node);\r\n if (asHandle) setActivatorNodeRef(node);\r\n });\r\n\r\n const composedStyle = React.useMemo(() => {\r\n return {\r\n transform: CSS.Translate.toString(transform),\r\n transition,\r\n ...style,\r\n };\r\n }, [transform, transition, style]);\r\n\r\n const itemContext = React.useMemo(\r\n () => ({\r\n id,\r\n attributes,\r\n listeners,\r\n setActivatorNodeRef,\r\n isDragging,\r\n disabled,\r\n }),\r\n [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],\r\n );\r\n\r\n const ItemPrimitive = asChild ? Slot : \"div\";\r\n\r\n return (\r\n \r\n \r\n \r\n );\r\n}\r\n\r\ninterface SortableItemHandleProps extends React.ComponentProps<\"button\"> {\r\n asChild?: boolean;\r\n}\r\n\r\nfunction SortableItemHandle(props: SortableItemHandleProps) {\r\n const { asChild, disabled, className, ref, ...itemHandleProps } = props;\r\n\r\n const context = useSortableContext(ITEM_HANDLE_NAME);\r\n const itemContext = useSortableItemContext(ITEM_HANDLE_NAME);\r\n\r\n const isDisabled = disabled ?? itemContext.disabled;\r\n\r\n const composedRef = useComposedRefs(ref, (node) => {\r\n if (!isDisabled) return;\r\n itemContext.setActivatorNodeRef(node);\r\n });\r\n\r\n const HandlePrimitive = asChild ? Slot : \"button\";\r\n\r\n return (\r\n \r\n );\r\n}\r\n\r\nconst SortableOverlayContext = React.createContext(false);\r\n\r\nconst dropAnimation: DropAnimation = {\r\n sideEffects: defaultDropAnimationSideEffects({\r\n styles: {\r\n active: {\r\n opacity: \"0.4\",\r\n },\r\n },\r\n }),\r\n};\r\n\r\ninterface SortableOverlayProps\r\n extends Omit, \"children\"> {\r\n container?: Element | DocumentFragment | null;\r\n children?:\r\n | ((params: { value: UniqueIdentifier }) => React.ReactNode)\r\n | React.ReactNode;\r\n}\r\n\r\nfunction SortableOverlay(props: SortableOverlayProps) {\r\n const { container: containerProp, children, ...overlayProps } = props;\r\n\r\n const context = useSortableContext(OVERLAY_NAME);\r\n\r\n const [mounted, setMounted] = React.useState(false);\r\n React.useLayoutEffect(() => setMounted(true), []);\r\n\r\n const container =\r\n containerProp ?? (mounted ? globalThis.document?.body : null);\r\n\r\n if (!container) return null;\r\n\r\n return ReactDOM.createPortal(\r\n \r\n \r\n {context.activeId\r\n ? typeof children === \"function\"\r\n ? children({ value: context.activeId })\r\n : children\r\n : null}\r\n \r\n ,\r\n container,\r\n );\r\n}\r\n\r\nexport {\r\n SortableRoot as Sortable,\r\n SortableContent,\r\n SortableItem,\r\n SortableItemHandle,\r\n SortableOverlay,\r\n //\r\n SortableRoot as Root,\r\n SortableContent as Content,\r\n SortableItem as Item,\r\n SortableItemHandle as ItemHandle,\r\n SortableOverlay as Overlay,\r\n};\r\n", + "content": "\"use client\";\n\nimport {\n type Announcements,\n closestCenter,\n closestCorners,\n DndContext,\n type DndContextProps,\n type DragEndEvent,\n type DraggableAttributes,\n type DraggableSyntheticListeners,\n DragOverlay,\n type DragStartEvent,\n type DropAnimation,\n defaultDropAnimationSideEffects,\n KeyboardSensor,\n MouseSensor,\n type ScreenReaderInstructions,\n TouchSensor,\n type UniqueIdentifier,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport {\n restrictToHorizontalAxis,\n restrictToParentElement,\n restrictToVerticalAxis,\n} from \"@dnd-kit/modifiers\";\nimport {\n arrayMove,\n horizontalListSortingStrategy,\n SortableContext,\n type SortableContextProps,\n sortableKeyboardCoordinates,\n useSortable,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\n\nconst orientationConfig = {\n vertical: {\n modifiers: [restrictToVerticalAxis, restrictToParentElement],\n strategy: verticalListSortingStrategy,\n collisionDetection: closestCenter,\n },\n horizontal: {\n modifiers: [restrictToHorizontalAxis, restrictToParentElement],\n strategy: horizontalListSortingStrategy,\n collisionDetection: closestCenter,\n },\n mixed: {\n modifiers: [restrictToParentElement],\n strategy: undefined,\n collisionDetection: closestCorners,\n },\n};\n\nconst ROOT_NAME = \"Sortable\";\nconst CONTENT_NAME = \"SortableContent\";\nconst ITEM_NAME = \"SortableItem\";\nconst ITEM_HANDLE_NAME = \"SortableItemHandle\";\nconst OVERLAY_NAME = \"SortableOverlay\";\n\ninterface SortableRootContextValue {\n id: string;\n items: UniqueIdentifier[];\n modifiers: DndContextProps[\"modifiers\"];\n strategy: SortableContextProps[\"strategy\"];\n activeId: UniqueIdentifier | null;\n setActiveId: (id: UniqueIdentifier | null) => void;\n getItemValue: (item: T) => UniqueIdentifier;\n flatCursor: boolean;\n}\n\nconst SortableRootContext =\n React.createContext | null>(null);\n\nfunction useSortableContext(consumerName: string) {\n const context = React.useContext(SortableRootContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface GetItemValue {\n /**\n * Callback that returns a unique identifier for each sortable item. Required for array of objects.\n * @example getItemValue={(item) => item.id}\n */\n getItemValue: (item: T) => UniqueIdentifier;\n}\n\ntype SortableRootProps = DndContextProps &\n (T extends object ? GetItemValue : Partial>) & {\n value: T[];\n onValueChange?: (items: T[]) => void;\n onMove?: (\n event: DragEndEvent & { activeIndex: number; overIndex: number },\n ) => void;\n strategy?: SortableContextProps[\"strategy\"];\n orientation?: \"vertical\" | \"horizontal\" | \"mixed\";\n flatCursor?: boolean;\n };\n\nfunction SortableRoot(props: SortableRootProps) {\n const {\n value,\n onValueChange,\n collisionDetection,\n modifiers,\n strategy,\n onMove,\n orientation = \"vertical\",\n flatCursor = false,\n getItemValue: getItemValueProp,\n accessibility,\n onDragStart: onDragStartProp,\n onDragEnd: onDragEndProp,\n onDragCancel: onDragCancelProp,\n ...sortableProps\n } = props;\n\n const id = React.useId();\n const [activeId, setActiveId] = React.useState(null);\n\n const sensors = useSensors(\n useSensor(MouseSensor),\n useSensor(TouchSensor),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n }),\n );\n const config = React.useMemo(\n () => orientationConfig[orientation],\n [orientation],\n );\n\n const getItemValue = React.useCallback(\n (item: T): UniqueIdentifier => {\n if (typeof item === \"object\" && !getItemValueProp) {\n throw new Error(\"getItemValue is required when using array of objects\");\n }\n return getItemValueProp\n ? getItemValueProp(item)\n : (item as UniqueIdentifier);\n },\n [getItemValueProp],\n );\n\n const items = React.useMemo(() => {\n return value.map((item) => getItemValue(item));\n }, [value, getItemValue]);\n\n const onDragStart = React.useCallback(\n (event: DragStartEvent) => {\n onDragStartProp?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(event.active.id);\n },\n [onDragStartProp],\n );\n\n const onDragEnd = React.useCallback(\n (event: DragEndEvent) => {\n onDragEndProp?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n const { active, over } = event;\n if (over && active.id !== over?.id) {\n const activeIndex = value.findIndex(\n (item) => getItemValue(item) === active.id,\n );\n const overIndex = value.findIndex(\n (item) => getItemValue(item) === over.id,\n );\n\n if (onMove) {\n onMove({ ...event, activeIndex, overIndex });\n } else {\n onValueChange?.(arrayMove(value, activeIndex, overIndex));\n }\n }\n setActiveId(null);\n },\n [value, onValueChange, onMove, getItemValue, onDragEndProp],\n );\n\n const onDragCancel = React.useCallback(\n (event: DragEndEvent) => {\n onDragCancelProp?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(null);\n },\n [onDragCancelProp],\n );\n\n const announcements: Announcements = React.useMemo(\n () => ({\n onDragStart({ active }) {\n const activeValue = active.id.toString();\n return `Grabbed sortable item \"${activeValue}\". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;\n },\n onDragOver({ active, over }) {\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n const activeValue = active.id.toString();\n return `Sortable item \"${activeValue}\" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n }\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n },\n onDragEnd({ active, over }) {\n const activeValue = active.id.toString();\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n return `Sortable item \"${activeValue}\" dropped at position ${overIndex + 1} of ${value.length}.`;\n }\n return `Sortable item \"${activeValue}\" dropped. No changes were made.`;\n },\n onDragCancel({ active }) {\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const activeValue = active.id.toString();\n return `Sorting cancelled. Sortable item \"${activeValue}\" returned to position ${activeIndex + 1} of ${value.length}.`;\n },\n onDragMove({ active, over }) {\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n const activeValue = active.id.toString();\n return `Sortable item \"${activeValue}\" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n }\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n },\n }),\n [value],\n );\n\n const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(\n () => ({\n draggable: `\n To pick up a sortable item, press space or enter.\n While dragging, use the ${orientation === \"vertical\" ? \"up and down\" : orientation === \"horizontal\" ? \"left and right\" : \"arrow\"} keys to move the item.\n Press space or enter again to drop the item in its new position, or press escape to cancel.\n `,\n }),\n [orientation],\n );\n\n const contextValue = React.useMemo(\n () => ({\n id,\n items,\n modifiers: modifiers ?? config.modifiers,\n strategy: strategy ?? config.strategy,\n activeId,\n setActiveId,\n getItemValue,\n flatCursor,\n }),\n [\n id,\n items,\n modifiers,\n strategy,\n config.modifiers,\n config.strategy,\n activeId,\n getItemValue,\n flatCursor,\n ],\n );\n\n return (\n }\n >\n \n \n );\n}\n\nconst SortableContentContext = React.createContext(false);\n\ninterface SortableContentProps extends React.ComponentProps<\"div\"> {\n strategy?: SortableContextProps[\"strategy\"];\n children: React.ReactNode;\n asChild?: boolean;\n withoutSlot?: boolean;\n}\n\nfunction SortableContent(props: SortableContentProps) {\n const {\n strategy: strategyProp,\n asChild,\n withoutSlot,\n children,\n ref,\n ...contentProps\n } = props;\n\n const context = useSortableContext(CONTENT_NAME);\n\n const ContentPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n {withoutSlot ? (\n children\n ) : (\n \n {children}\n \n )}\n \n \n );\n}\n\ninterface SortableItemContextValue {\n id: string;\n attributes: DraggableAttributes;\n listeners: DraggableSyntheticListeners | undefined;\n setActivatorNodeRef: (node: HTMLElement | null) => void;\n isDragging?: boolean;\n disabled?: boolean;\n}\n\nconst SortableItemContext =\n React.createContext(null);\n\nfunction useSortableItemContext(consumerName: string) {\n const context = React.useContext(SortableItemContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\n }\n return context;\n}\n\ninterface SortableItemProps extends React.ComponentProps<\"div\"> {\n value: UniqueIdentifier;\n asHandle?: boolean;\n asChild?: boolean;\n disabled?: boolean;\n}\n\nfunction SortableItem(props: SortableItemProps) {\n const {\n value,\n style,\n asHandle,\n asChild,\n disabled,\n className,\n ref,\n ...itemProps\n } = props;\n\n const inSortableContent = React.useContext(SortableContentContext);\n const inSortableOverlay = React.useContext(SortableOverlayContext);\n\n if (!inSortableContent && !inSortableOverlay) {\n throw new Error(\n `\\`${ITEM_NAME}\\` must be used within \\`${CONTENT_NAME}\\` or \\`${OVERLAY_NAME}\\``,\n );\n }\n\n if (value === \"\") {\n throw new Error(`\\`${ITEM_NAME}\\` value cannot be an empty string`);\n }\n\n const context = useSortableContext(ITEM_NAME);\n const id = React.useId();\n const {\n attributes,\n listeners,\n setNodeRef,\n setActivatorNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: value, disabled });\n\n const composedRef = useComposedRefs(ref, (node) => {\n if (disabled) return;\n setNodeRef(node);\n if (asHandle) setActivatorNodeRef(node);\n });\n\n const composedStyle = React.useMemo(() => {\n return {\n transform: CSS.Translate.toString(transform),\n transition,\n ...style,\n };\n }, [transform, transition, style]);\n\n const itemContext = React.useMemo(\n () => ({\n id,\n attributes,\n listeners,\n setActivatorNodeRef,\n isDragging,\n disabled,\n }),\n [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],\n );\n\n const ItemPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n \n );\n}\n\ninterface SortableItemHandleProps extends React.ComponentProps<\"button\"> {\n asChild?: boolean;\n}\n\nfunction SortableItemHandle(props: SortableItemHandleProps) {\n const { asChild, disabled, className, ref, ...itemHandleProps } = props;\n\n const context = useSortableContext(ITEM_HANDLE_NAME);\n const itemContext = useSortableItemContext(ITEM_HANDLE_NAME);\n\n const isDisabled = disabled ?? itemContext.disabled;\n\n const composedRef = useComposedRefs(ref, (node) => {\n if (!isDisabled) return;\n itemContext.setActivatorNodeRef(node);\n });\n\n const HandlePrimitive = asChild ? Slot : \"button\";\n\n return (\n \n );\n}\n\nconst SortableOverlayContext = React.createContext(false);\n\nconst dropAnimation: DropAnimation = {\n sideEffects: defaultDropAnimationSideEffects({\n styles: {\n active: {\n opacity: \"0.4\",\n },\n },\n }),\n};\n\ninterface SortableOverlayProps\n extends Omit, \"children\"> {\n container?: Element | DocumentFragment | null;\n children?:\n | ((params: { value: UniqueIdentifier }) => React.ReactNode)\n | React.ReactNode;\n}\n\nfunction SortableOverlay(props: SortableOverlayProps) {\n const { container: containerProp, children, ...overlayProps } = props;\n\n const context = useSortableContext(OVERLAY_NAME);\n\n const [mounted, setMounted] = React.useState(false);\n React.useLayoutEffect(() => setMounted(true), []);\n\n const container =\n containerProp ?? (mounted ? globalThis.document?.body : null);\n\n if (!container) return null;\n\n return ReactDOM.createPortal(\n \n \n {context.activeId\n ? typeof children === \"function\"\n ? children({ value: context.activeId })\n : children\n : null}\n \n ,\n container,\n );\n}\n\nexport {\n SortableRoot as Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n //\n SortableRoot as Root,\n SortableContent as Content,\n SortableItem as Item,\n SortableItemHandle as ItemHandle,\n SortableOverlay as Overlay,\n};\n", "type": "registry:ui", "target": "src/components/ui/sortable.tsx" }, { "path": "src/components/ui/faceted.tsx", - "content": "\"use client\";\r\n\r\nimport { Check, ChevronsUpDown } from \"lucide-react\";\r\nimport * as React from \"react\";\r\n\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n CommandSeparator,\r\n} from \"@/components/ui/command\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ntype FacetedValue = Multiple extends true\r\n ? string[]\r\n : string;\r\n\r\ninterface FacetedContextValue {\r\n value?: FacetedValue;\r\n onItemSelect?: (value: string) => void;\r\n multiple?: Multiple;\r\n}\r\n\r\nconst FacetedContext = React.createContext | null>(\r\n null,\r\n);\r\n\r\nfunction useFacetedContext(name: string) {\r\n const context = React.useContext(FacetedContext);\r\n if (!context) {\r\n throw new Error(`\\`${name}\\` must be within Faceted`);\r\n }\r\n return context;\r\n}\r\n\r\ninterface FacetedProps\r\n extends React.ComponentProps {\r\n value?: FacetedValue;\r\n onValueChange?: (value: FacetedValue | undefined) => void;\r\n children?: React.ReactNode;\r\n multiple?: Multiple;\r\n}\r\n\r\nfunction Faceted(\r\n props: FacetedProps,\r\n) {\r\n const {\r\n open: openProp,\r\n onOpenChange: onOpenChangeProp,\r\n value,\r\n onValueChange,\r\n children,\r\n multiple = false,\r\n ...facetedProps\r\n } = props;\r\n\r\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);\r\n const isControlled = openProp !== undefined;\r\n const open = isControlled ? openProp : uncontrolledOpen;\r\n\r\n const onOpenChange = React.useCallback(\r\n (newOpen: boolean) => {\r\n if (!isControlled) {\r\n setUncontrolledOpen(newOpen);\r\n }\r\n onOpenChangeProp?.(newOpen);\r\n },\r\n [isControlled, onOpenChangeProp],\r\n );\r\n\r\n const onItemSelect = React.useCallback(\r\n (selectedValue: string) => {\r\n if (!onValueChange) return;\r\n\r\n if (multiple) {\r\n const currentValue = (Array.isArray(value) ? value : []) as string[];\r\n const newValue = currentValue.includes(selectedValue)\r\n ? currentValue.filter((v) => v !== selectedValue)\r\n : [...currentValue, selectedValue];\r\n onValueChange(newValue as FacetedValue);\r\n } else {\r\n if (value === selectedValue) {\r\n onValueChange(undefined);\r\n } else {\r\n onValueChange(selectedValue as FacetedValue);\r\n }\r\n\r\n requestAnimationFrame(() => onOpenChange(false));\r\n }\r\n },\r\n [multiple, value, onValueChange, onOpenChange],\r\n );\r\n\r\n const contextValue = React.useMemo>(\r\n () => ({ value, onItemSelect, multiple }),\r\n [value, onItemSelect, multiple],\r\n );\r\n\r\n return (\r\n \r\n \r\n {children}\r\n \r\n \r\n );\r\n}\r\n\r\nfunction FacetedTrigger(props: React.ComponentProps) {\r\n const { className, children, ...triggerProps } = props;\r\n\r\n return (\r\n \r\n {children}\r\n \r\n );\r\n}\r\n\r\ninterface FacetedBadgeListProps extends React.ComponentProps<\"div\"> {\r\n options?: { label: string; value: string }[];\r\n max?: number;\r\n badgeClassName?: string;\r\n placeholder?: string;\r\n}\r\n\r\nfunction FacetedBadgeList(props: FacetedBadgeListProps) {\r\n const {\r\n options = [],\r\n max = 2,\r\n placeholder = \"Select options...\",\r\n className,\r\n badgeClassName,\r\n ...badgeListProps\r\n } = props;\r\n\r\n const context = useFacetedContext(\"FacetedBadgeList\");\r\n const values = Array.isArray(context.value)\r\n ? context.value\r\n : ([context.value].filter(Boolean) as string[]);\r\n\r\n const getLabel = React.useCallback(\r\n (value: string) => {\r\n const option = options.find((opt) => opt.value === value);\r\n return option?.label ?? value;\r\n },\r\n [options],\r\n );\r\n\r\n if (!values || values.length === 0) {\r\n return (\r\n \r\n {placeholder}\r\n \r\n
\r\n );\r\n }\r\n\r\n return (\r\n \r\n {values.length > max ? (\r\n \r\n {values.length} selected\r\n \r\n ) : (\r\n values.map((value) => (\r\n \r\n {getLabel(value)}\r\n \r\n ))\r\n )}\r\n
\r\n );\r\n}\r\n\r\nfunction FacetedContent(props: React.ComponentProps) {\r\n const { className, children, ...contentProps } = props;\r\n\r\n return (\r\n \r\n {children}\r\n \r\n );\r\n}\r\n\r\nconst FacetedInput = CommandInput;\r\n\r\nconst FacetedList = CommandList;\r\n\r\nconst FacetedEmpty = CommandEmpty;\r\n\r\nconst FacetedGroup = CommandGroup;\r\n\r\ninterface FacetedItemProps extends React.ComponentProps {\r\n value: string;\r\n}\r\n\r\nfunction FacetedItem(props: FacetedItemProps) {\r\n const { value, onSelect, className, children, ...itemProps } = props;\r\n const context = useFacetedContext(\"FacetedItem\");\r\n\r\n const isSelected = context.multiple\r\n ? Array.isArray(context.value) && context.value.includes(value)\r\n : context.value === value;\r\n\r\n const onItemSelect = React.useCallback(\r\n (currentValue: string) => {\r\n if (onSelect) {\r\n onSelect(currentValue);\r\n } else if (context.onItemSelect) {\r\n context.onItemSelect(currentValue);\r\n }\r\n },\r\n [onSelect, context],\r\n );\r\n\r\n return (\r\n onItemSelect(value)}\r\n {...itemProps}\r\n >\r\n \r\n \r\n \r\n {children}\r\n \r\n );\r\n}\r\n\r\nconst FacetedSeparator = CommandSeparator;\r\n\r\nexport {\r\n Faceted,\r\n FacetedBadgeList,\r\n FacetedContent,\r\n FacetedEmpty,\r\n FacetedGroup,\r\n FacetedInput,\r\n FacetedItem,\r\n FacetedList,\r\n FacetedSeparator,\r\n FacetedTrigger,\r\n};\r\n", + "content": "\"use client\";\n\nimport { Check, ChevronsUpDown } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\n\ntype FacetedValue = Multiple extends true\n ? string[]\n : string;\n\ninterface FacetedContextValue {\n value?: FacetedValue;\n onItemSelect?: (value: string) => void;\n multiple?: Multiple;\n}\n\nconst FacetedContext = React.createContext | null>(\n null,\n);\n\nfunction useFacetedContext(name: string) {\n const context = React.useContext(FacetedContext);\n if (!context) {\n throw new Error(`\\`${name}\\` must be within Faceted`);\n }\n return context;\n}\n\ninterface FacetedProps\n extends React.ComponentProps {\n value?: FacetedValue;\n onValueChange?: (value: FacetedValue | undefined) => void;\n children?: React.ReactNode;\n multiple?: Multiple;\n}\n\nfunction Faceted(\n props: FacetedProps,\n) {\n const {\n open: openProp,\n onOpenChange: onOpenChangeProp,\n value,\n onValueChange,\n children,\n multiple = false,\n ...facetedProps\n } = props;\n\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);\n const isControlled = openProp !== undefined;\n const open = isControlled ? openProp : uncontrolledOpen;\n\n const onOpenChange = React.useCallback(\n (newOpen: boolean) => {\n if (!isControlled) {\n setUncontrolledOpen(newOpen);\n }\n onOpenChangeProp?.(newOpen);\n },\n [isControlled, onOpenChangeProp],\n );\n\n const onItemSelect = React.useCallback(\n (selectedValue: string) => {\n if (!onValueChange) return;\n\n if (multiple) {\n const currentValue = (Array.isArray(value) ? value : []) as string[];\n const newValue = currentValue.includes(selectedValue)\n ? currentValue.filter((v) => v !== selectedValue)\n : [...currentValue, selectedValue];\n onValueChange(newValue as FacetedValue);\n } else {\n if (value === selectedValue) {\n onValueChange(undefined);\n } else {\n onValueChange(selectedValue as FacetedValue);\n }\n\n requestAnimationFrame(() => onOpenChange(false));\n }\n },\n [multiple, value, onValueChange, onOpenChange],\n );\n\n const contextValue = React.useMemo>(\n () => ({ value, onItemSelect, multiple }),\n [value, onItemSelect, multiple],\n );\n\n return (\n \n \n {children}\n \n \n );\n}\n\nfunction FacetedTrigger(props: React.ComponentProps) {\n const { className, children, ...triggerProps } = props;\n\n return (\n \n {children}\n \n );\n}\n\ninterface FacetedBadgeListProps extends React.ComponentProps<\"div\"> {\n options?: { label: string; value: string }[];\n max?: number;\n badgeClassName?: string;\n placeholder?: string;\n}\n\nfunction FacetedBadgeList(props: FacetedBadgeListProps) {\n const {\n options = [],\n max = 2,\n placeholder = \"Select options...\",\n className,\n badgeClassName,\n ...badgeListProps\n } = props;\n\n const context = useFacetedContext(\"FacetedBadgeList\");\n const values = Array.isArray(context.value)\n ? context.value\n : ([context.value].filter(Boolean) as string[]);\n\n const getLabel = React.useCallback(\n (value: string) => {\n const option = options.find((opt) => opt.value === value);\n return option?.label ?? value;\n },\n [options],\n );\n\n if (!values || values.length === 0) {\n return (\n \n {placeholder}\n \n
\n );\n }\n\n return (\n \n {values.length > max ? (\n \n {values.length} selected\n \n ) : (\n values.map((value) => (\n \n {getLabel(value)}\n \n ))\n )}\n
\n );\n}\n\nfunction FacetedContent(props: React.ComponentProps) {\n const { className, children, ...contentProps } = props;\n\n return (\n \n {children}\n \n );\n}\n\nconst FacetedInput = CommandInput;\n\nconst FacetedList = CommandList;\n\nconst FacetedEmpty = CommandEmpty;\n\nconst FacetedGroup = CommandGroup;\n\ninterface FacetedItemProps extends React.ComponentProps {\n value: string;\n}\n\nfunction FacetedItem(props: FacetedItemProps) {\n const { value, onSelect, className, children, ...itemProps } = props;\n const context = useFacetedContext(\"FacetedItem\");\n\n const isSelected = context.multiple\n ? Array.isArray(context.value) && context.value.includes(value)\n : context.value === value;\n\n const onItemSelect = React.useCallback(\n (currentValue: string) => {\n if (onSelect) {\n onSelect(currentValue);\n } else if (context.onItemSelect) {\n context.onItemSelect(currentValue);\n }\n },\n [onSelect, context],\n );\n\n return (\n onItemSelect(value)}\n {...itemProps}\n >\n \n \n \n {children}\n \n );\n}\n\nconst FacetedSeparator = CommandSeparator;\n\nexport {\n Faceted,\n FacetedBadgeList,\n FacetedContent,\n FacetedEmpty,\n FacetedGroup,\n FacetedInput,\n FacetedItem,\n FacetedList,\n FacetedSeparator,\n FacetedTrigger,\n};\n", "type": "registry:ui", "target": "src/components/ui/faceted.tsx" }, @@ -67,7 +67,7 @@ }, { "path": "src/lib/data-table.ts", - "content": "import type { Column } from \"@tanstack/react-table\";\r\nimport { dataTableConfig } from \"@/config/data-table\";\r\nimport type {\r\n ExtendedColumnFilter,\r\n FilterOperator,\r\n FilterVariant,\r\n} from \"@/types/data-table\";\r\n\r\nexport function getCommonPinningStyles({\r\n column,\r\n withBorder = false,\r\n}: {\r\n column: Column;\r\n withBorder?: boolean;\r\n}): React.CSSProperties {\r\n const isPinned = column.getIsPinned();\r\n const isLastLeftPinnedColumn =\r\n isPinned === \"left\" && column.getIsLastColumn(\"left\");\r\n const isFirstRightPinnedColumn =\r\n isPinned === \"right\" && column.getIsFirstColumn(\"right\");\r\n\r\n return {\r\n boxShadow: withBorder\r\n ? isLastLeftPinnedColumn\r\n ? \"-4px 0 4px -4px var(--border) inset\"\r\n : isFirstRightPinnedColumn\r\n ? \"4px 0 4px -4px var(--border) inset\"\r\n : undefined\r\n : undefined,\r\n left: isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\r\n right: isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined,\r\n opacity: isPinned ? 0.97 : 1,\r\n position: isPinned ? \"sticky\" : \"relative\",\r\n background: isPinned ? \"var(--background)\" : \"var(--background)\",\r\n width: column.getSize(),\r\n zIndex: isPinned ? 1 : undefined,\r\n };\r\n}\r\n\r\nexport function getFilterOperators(filterVariant: FilterVariant) {\r\n const operatorMap: Record<\r\n FilterVariant,\r\n { label: string; value: FilterOperator }[]\r\n > = {\r\n text: dataTableConfig.textOperators,\r\n number: dataTableConfig.numericOperators,\r\n range: dataTableConfig.numericOperators,\r\n date: dataTableConfig.dateOperators,\r\n dateRange: dataTableConfig.dateOperators,\r\n boolean: dataTableConfig.booleanOperators,\r\n select: dataTableConfig.selectOperators,\r\n multiSelect: dataTableConfig.multiSelectOperators,\r\n };\r\n\r\n return operatorMap[filterVariant] ?? dataTableConfig.textOperators;\r\n}\r\n\r\nexport function getDefaultFilterOperator(filterVariant: FilterVariant) {\r\n const operators = getFilterOperators(filterVariant);\r\n\r\n return operators[0]?.value ?? (filterVariant === \"text\" ? \"iLike\" : \"eq\");\r\n}\r\n\r\nexport function getValidFilters(\r\n filters: ExtendedColumnFilter[],\r\n): ExtendedColumnFilter[] {\r\n return filters.filter(\r\n (filter) =>\r\n filter.operator === \"isEmpty\" ||\r\n filter.operator === \"isNotEmpty\" ||\r\n (Array.isArray(filter.value)\r\n ? filter.value.length > 0\r\n : filter.value !== \"\" &&\r\n filter.value !== null &&\r\n filter.value !== undefined),\r\n );\r\n}\r\n", + "content": "import type { Column } from \"@tanstack/react-table\";\nimport { dataTableConfig } from \"@/config/data-table\";\nimport type {\n ExtendedColumnFilter,\n FilterOperator,\n FilterVariant,\n} from \"@/types/data-table\";\n\nexport function getCommonPinningStyles({\n column,\n withBorder = false,\n}: {\n column: Column;\n withBorder?: boolean;\n}): React.CSSProperties {\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn =\n isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn =\n isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\n right: isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined,\n opacity: isPinned ? 0.97 : 1,\n position: isPinned ? \"sticky\" : \"relative\",\n background: isPinned ? \"var(--background)\" : \"var(--background)\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : undefined,\n };\n}\n\nexport function getFilterOperators(filterVariant: FilterVariant) {\n const operatorMap: Record<\n FilterVariant,\n { label: string; value: FilterOperator }[]\n > = {\n text: dataTableConfig.textOperators,\n number: dataTableConfig.numericOperators,\n range: dataTableConfig.numericOperators,\n date: dataTableConfig.dateOperators,\n dateRange: dataTableConfig.dateOperators,\n boolean: dataTableConfig.booleanOperators,\n select: dataTableConfig.selectOperators,\n multiSelect: dataTableConfig.multiSelectOperators,\n };\n\n return operatorMap[filterVariant] ?? dataTableConfig.textOperators;\n}\n\nexport function getDefaultFilterOperator(filterVariant: FilterVariant) {\n const operators = getFilterOperators(filterVariant);\n\n return operators[0]?.value ?? (filterVariant === \"text\" ? \"iLike\" : \"eq\");\n}\n\nexport function getValidFilters(\n filters: ExtendedColumnFilter[],\n): ExtendedColumnFilter[] {\n return filters.filter(\n (filter) =>\n filter.operator === \"isEmpty\" ||\n filter.operator === \"isNotEmpty\" ||\n (Array.isArray(filter.value)\n ? filter.value.length > 0\n : filter.value !== \"\" &&\n filter.value !== null &&\n filter.value !== undefined),\n );\n}\n", "type": "registry:lib" }, { @@ -93,7 +93,7 @@ }, { "path": "src/types/data-table.ts", - "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\r\nimport type { DataTableConfig } from \"@/config/data-table\";\r\nimport type { FilterItemSchema } from \"@/lib/parsers\";\r\n\r\ndeclare module \"@tanstack/react-table\" {\r\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\r\n interface TableMeta {\r\n queryKeys?: QueryKeys;\r\n }\r\n\r\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\r\n interface ColumnMeta {\r\n label?: string;\r\n placeholder?: string;\r\n variant?: FilterVariant;\r\n options?: Option[];\r\n range?: [number, number];\r\n unit?: string;\r\n icon?: React.FC>;\r\n }\r\n}\r\n\r\nexport interface QueryKeys {\r\n page: string;\r\n perPage: string;\r\n sort: string;\r\n filters: string;\r\n joinOperator: string;\r\n}\r\n\r\nexport interface Option {\r\n label: string;\r\n value: string;\r\n count?: number;\r\n icon?: React.FC>;\r\n}\r\n\r\nexport type FilterOperator = DataTableConfig[\"operators\"][number];\r\nexport type FilterVariant = DataTableConfig[\"filterVariants\"][number];\r\nexport type JoinOperator = DataTableConfig[\"joinOperators\"][number];\r\n\r\nexport interface ExtendedColumnSort extends Omit {\r\n id: Extract;\r\n}\r\n\r\nexport interface ExtendedColumnFilter extends FilterItemSchema {\r\n id: Extract;\r\n}\r\n\r\nexport interface DataTableRowAction {\r\n row: Row;\r\n variant: \"update\" | \"delete\";\r\n}\r\n", + "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\nimport type { DataTableConfig } from \"@/config/data-table\";\nimport type { FilterItemSchema } from \"@/lib/parsers\";\n\ndeclare module \"@tanstack/react-table\" {\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\n interface TableMeta {\n queryKeys?: QueryKeys;\n }\n\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\n interface ColumnMeta {\n label?: string;\n placeholder?: string;\n variant?: FilterVariant;\n options?: Option[];\n range?: [number, number];\n unit?: string;\n icon?: React.FC>;\n }\n}\n\nexport interface QueryKeys {\n page: string;\n perPage: string;\n sort: string;\n filters: string;\n joinOperator: string;\n}\n\nexport interface Option {\n label: string;\n value: string;\n count?: number;\n icon?: React.FC>;\n}\n\nexport type FilterOperator = DataTableConfig[\"operators\"][number];\nexport type FilterVariant = DataTableConfig[\"filterVariants\"][number];\nexport type JoinOperator = DataTableConfig[\"joinOperators\"][number];\n\nexport interface ExtendedColumnSort extends Omit {\n id: Extract;\n}\n\nexport interface ExtendedColumnFilter extends FilterItemSchema {\n id: Extract;\n}\n\nexport interface DataTableRowAction {\n row: Row;\n variant: \"update\" | \"delete\";\n}\n", "type": "registry:file", "target": "src/types/data-table.ts" } diff --git a/public/r/data-table-filter-menu.json b/public/r/data-table-filter-menu.json index 274b10075..75af3c7c2 100644 --- a/public/r/data-table-filter-menu.json +++ b/public/r/data-table-filter-menu.json @@ -23,7 +23,7 @@ "files": [ { "path": "src/components/data-table/data-table-filter-menu.tsx", - "content": "\"use client\";\r\n\r\nimport type { Column, Table } from \"@tanstack/react-table\";\r\nimport {\r\n BadgeCheck,\r\n CalendarIcon,\r\n Check,\r\n ListFilter,\r\n Text,\r\n X,\r\n} from \"lucide-react\";\r\nimport { useQueryState } from \"nuqs\";\r\nimport * as React from \"react\";\r\n\r\nimport { DataTableRangeFilter } from \"@/components/data-table/data-table-range-filter\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Calendar } from \"@/components/ui/calendar\";\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n} from \"@/components/ui/command\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\r\nimport { getDefaultFilterOperator, getFilterOperators } from \"@/lib/data-table\";\r\nimport { formatDate } from \"@/lib/format\";\r\nimport { generateId } from \"@/lib/id\";\r\nimport { getFiltersStateParser } from \"@/lib/parsers\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport type { ExtendedColumnFilter, FilterOperator } from \"@/types/data-table\";\r\n\r\nconst DEBOUNCE_MS = 300;\r\nconst THROTTLE_MS = 50;\r\nconst FILTER_SHORTCUT_KEY = \"f\";\r\nconst REMOVE_FILTER_SHORTCUTS = [\"backspace\", \"delete\"];\r\n\r\ninterface DataTableFilterMenuProps\r\n extends React.ComponentProps {\r\n table: Table;\r\n debounceMs?: number;\r\n throttleMs?: number;\r\n shallow?: boolean;\r\n}\r\n\r\nexport function DataTableFilterMenu({\r\n table,\r\n debounceMs = DEBOUNCE_MS,\r\n throttleMs = THROTTLE_MS,\r\n shallow = true,\r\n align = \"start\",\r\n ...props\r\n}: DataTableFilterMenuProps) {\r\n const id = React.useId();\r\n\r\n const columns = React.useMemo(() => {\r\n return table\r\n .getAllColumns()\r\n .filter((column) => column.columnDef.enableColumnFilter);\r\n }, [table]);\r\n\r\n const [open, setOpen] = React.useState(false);\r\n const [selectedColumn, setSelectedColumn] =\r\n React.useState | null>(null);\r\n const [inputValue, setInputValue] = React.useState(\"\");\r\n const triggerRef = React.useRef(null);\r\n const inputRef = React.useRef(null);\r\n\r\n const onOpenChange = React.useCallback((open: boolean) => {\r\n setOpen(open);\r\n\r\n if (!open) {\r\n setTimeout(() => {\r\n setSelectedColumn(null);\r\n setInputValue(\"\");\r\n }, 100);\r\n }\r\n }, []);\r\n\r\n const onInputKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (\r\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\r\n !inputValue &&\r\n selectedColumn\r\n ) {\r\n event.preventDefault();\r\n setSelectedColumn(null);\r\n }\r\n },\r\n [inputValue, selectedColumn],\r\n );\r\n\r\n const [filters, setFilters] = useQueryState(\r\n table.options.meta?.queryKeys?.filters ?? \"filters\",\r\n getFiltersStateParser(columns.map((field) => field.id))\r\n .withDefault([])\r\n .withOptions({\r\n clearOnDefault: true,\r\n shallow,\r\n throttleMs,\r\n }),\r\n );\r\n const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);\r\n\r\n const onFilterAdd = React.useCallback(\r\n (column: Column, value: string) => {\r\n if (!value.trim() && column.columnDef.meta?.variant !== \"boolean\") {\r\n return;\r\n }\r\n\r\n const filterValue =\r\n column.columnDef.meta?.variant === \"multiSelect\" ? [value] : value;\r\n\r\n const newFilter: ExtendedColumnFilter = {\r\n id: column.id as Extract,\r\n value: filterValue,\r\n variant: column.columnDef.meta?.variant ?? \"text\",\r\n operator: getDefaultFilterOperator(\r\n column.columnDef.meta?.variant ?? \"text\",\r\n ),\r\n filterId: generateId({ length: 8 }),\r\n };\r\n\r\n debouncedSetFilters([...filters, newFilter]);\r\n setOpen(false);\r\n\r\n setTimeout(() => {\r\n setSelectedColumn(null);\r\n setInputValue(\"\");\r\n }, 100);\r\n },\r\n [filters, debouncedSetFilters],\r\n );\r\n\r\n const onFilterRemove = React.useCallback(\r\n (filterId: string) => {\r\n const updatedFilters = filters.filter(\r\n (filter) => filter.filterId !== filterId,\r\n );\r\n debouncedSetFilters(updatedFilters);\r\n requestAnimationFrame(() => {\r\n triggerRef.current?.focus();\r\n });\r\n },\r\n [filters, debouncedSetFilters],\r\n );\r\n\r\n const onFilterUpdate = React.useCallback(\r\n (\r\n filterId: string,\r\n updates: Partial, \"filterId\">>,\r\n ) => {\r\n debouncedSetFilters((prevFilters) => {\r\n const updatedFilters = prevFilters.map((filter) => {\r\n if (filter.filterId === filterId) {\r\n return { ...filter, ...updates } as ExtendedColumnFilter;\r\n }\r\n return filter;\r\n });\r\n return updatedFilters;\r\n });\r\n },\r\n [debouncedSetFilters],\r\n );\r\n\r\n const onFiltersReset = React.useCallback(() => {\r\n debouncedSetFilters([]);\r\n }, [debouncedSetFilters]);\r\n\r\n React.useEffect(() => {\r\n function onKeyDown(event: KeyboardEvent) {\r\n if (\r\n event.target instanceof HTMLInputElement ||\r\n event.target instanceof HTMLTextAreaElement ||\r\n (event.target instanceof HTMLElement &&\r\n event.target.contentEditable === \"true\")\r\n ) {\r\n return;\r\n }\r\n\r\n if (\r\n event.key.toLowerCase() === FILTER_SHORTCUT_KEY &&\r\n (event.ctrlKey || event.metaKey) &&\r\n event.shiftKey\r\n ) {\r\n event.preventDefault();\r\n setOpen((prev) => !prev);\r\n }\r\n }\r\n\r\n window.addEventListener(\"keydown\", onKeyDown);\r\n return () => window.removeEventListener(\"keydown\", onKeyDown);\r\n }, []);\r\n\r\n const onTriggerKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (\r\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\r\n filters.length > 0\r\n ) {\r\n event.preventDefault();\r\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\r\n }\r\n },\r\n [filters, onFilterRemove],\r\n );\r\n\r\n return (\r\n
\r\n {filters.map((filter) => (\r\n \r\n ))}\r\n {filters.length > 0 && (\r\n \r\n \r\n \r\n )}\r\n \r\n \r\n 0 ? \"icon\" : \"sm\"}\r\n className={cn(filters.length > 0 && \"size-8\", \"h-8 font-normal\")}\r\n ref={triggerRef}\r\n onKeyDown={onTriggerKeyDown}\r\n >\r\n \r\n {filters.length > 0 ? null : \"Filter\"}\r\n \r\n \r\n \r\n \r\n \r\n \r\n {selectedColumn ? (\r\n <>\r\n {selectedColumn.columnDef.meta?.options && (\r\n No options found.\r\n )}\r\n onFilterAdd(selectedColumn, value)}\r\n />\r\n \r\n ) : (\r\n <>\r\n No fields found.\r\n \r\n {columns.map((column) => (\r\n {\r\n setSelectedColumn(column);\r\n setInputValue(\"\");\r\n requestAnimationFrame(() => {\r\n inputRef.current?.focus();\r\n });\r\n }}\r\n >\r\n {column.columnDef.meta?.icon && (\r\n \r\n )}\r\n \r\n {column.columnDef.meta?.label ?? column.id}\r\n \r\n \r\n ))}\r\n \r\n \r\n )}\r\n \r\n \r\n \r\n \r\n
\r\n );\r\n}\r\n\r\ninterface DataTableFilterItemProps {\r\n filter: ExtendedColumnFilter;\r\n filterItemId: string;\r\n columns: Column[];\r\n onFilterUpdate: (\r\n filterId: string,\r\n updates: Partial, \"filterId\">>,\r\n ) => void;\r\n onFilterRemove: (filterId: string) => void;\r\n}\r\n\r\nfunction DataTableFilterItem({\r\n filter,\r\n filterItemId,\r\n columns,\r\n onFilterUpdate,\r\n onFilterRemove,\r\n}: DataTableFilterItemProps) {\r\n {\r\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\r\n const [showOperatorSelector, setShowOperatorSelector] =\r\n React.useState(false);\r\n const [showValueSelector, setShowValueSelector] = React.useState(false);\r\n\r\n const column = columns.find((column) => column.id === filter.id);\r\n\r\n const operatorListboxId = `${filterItemId}-operator-listbox`;\r\n const inputId = `${filterItemId}-input`;\r\n\r\n const columnMeta = column?.columnDef.meta;\r\n const filterOperators = getFilterOperators(filter.variant);\r\n\r\n const onItemKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (\r\n event.target instanceof HTMLInputElement ||\r\n event.target instanceof HTMLTextAreaElement\r\n ) {\r\n return;\r\n }\r\n\r\n if (showFieldSelector || showOperatorSelector || showValueSelector) {\r\n return;\r\n }\r\n\r\n if (REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase())) {\r\n event.preventDefault();\r\n onFilterRemove(filter.filterId);\r\n }\r\n },\r\n [\r\n filter.filterId,\r\n showFieldSelector,\r\n showOperatorSelector,\r\n showValueSelector,\r\n onFilterRemove,\r\n ],\r\n );\r\n\r\n if (!column) return null;\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n {columnMeta?.icon && (\r\n \r\n )}\r\n {columnMeta?.label ?? column.id}\r\n \r\n \r\n \r\n \r\n \r\n \r\n No fields found.\r\n \r\n {columns.map((column) => (\r\n {\r\n onFilterUpdate(filter.filterId, {\r\n id: column.id as Extract,\r\n variant: column.columnDef.meta?.variant ?? \"text\",\r\n operator: getDefaultFilterOperator(\r\n column.columnDef.meta?.variant ?? \"text\",\r\n ),\r\n value: \"\",\r\n });\r\n\r\n setShowFieldSelector(false);\r\n }}\r\n >\r\n {column.columnDef.meta?.icon && (\r\n \r\n )}\r\n \r\n {column.columnDef.meta?.label ?? column.id}\r\n \r\n \r\n \r\n ))}\r\n \r\n \r\n \r\n \r\n \r\n \r\n onFilterUpdate(filter.filterId, {\r\n operator: value,\r\n value:\r\n value === \"isEmpty\" || value === \"isNotEmpty\"\r\n ? \"\"\r\n : filter.value,\r\n })\r\n }\r\n >\r\n \r\n \r\n \r\n \r\n {filterOperators.map((operator) => (\r\n \r\n {operator.label}\r\n \r\n ))}\r\n \r\n \r\n {onFilterInputRender({\r\n filter,\r\n column,\r\n inputId,\r\n onFilterUpdate,\r\n showValueSelector,\r\n setShowValueSelector,\r\n })}\r\n onFilterRemove(filter.filterId)}\r\n >\r\n \r\n \r\n
\r\n );\r\n }\r\n}\r\n\r\ninterface FilterValueSelectorProps {\r\n column: Column;\r\n value: string;\r\n onSelect: (value: string) => void;\r\n}\r\n\r\nfunction FilterValueSelector({\r\n column,\r\n value,\r\n onSelect,\r\n}: FilterValueSelectorProps) {\r\n const variant = column.columnDef.meta?.variant ?? \"text\";\r\n\r\n switch (variant) {\r\n case \"boolean\":\r\n return (\r\n \r\n onSelect(\"true\")}>\r\n True\r\n \r\n onSelect(\"false\")}>\r\n False\r\n \r\n \r\n );\r\n\r\n case \"select\":\r\n case \"multiSelect\":\r\n return (\r\n \r\n {column.columnDef.meta?.options?.map((option) => (\r\n onSelect(option.value)}\r\n >\r\n {option.icon && }\r\n {option.label}\r\n {option.count && (\r\n \r\n {option.count}\r\n \r\n )}\r\n \r\n ))}\r\n \r\n );\r\n\r\n case \"date\":\r\n case \"dateRange\":\r\n return (\r\n onSelect(date?.getTime().toString() ?? \"\")}\r\n />\r\n );\r\n\r\n default: {\r\n const isEmpty = !value.trim();\r\n\r\n return (\r\n \r\n onSelect(value)}\r\n disabled={isEmpty}\r\n >\r\n {isEmpty ? (\r\n <>\r\n \r\n Type to add filter...\r\n \r\n ) : (\r\n <>\r\n \r\n Filter by "{value}"\r\n \r\n )}\r\n \r\n \r\n );\r\n }\r\n }\r\n}\r\n\r\nfunction onFilterInputRender({\r\n filter,\r\n column,\r\n inputId,\r\n onFilterUpdate,\r\n showValueSelector,\r\n setShowValueSelector,\r\n}: {\r\n filter: ExtendedColumnFilter;\r\n column: Column;\r\n inputId: string;\r\n onFilterUpdate: (\r\n filterId: string,\r\n updates: Partial, \"filterId\">>,\r\n ) => void;\r\n showValueSelector: boolean;\r\n setShowValueSelector: (value: boolean) => void;\r\n}) {\r\n if (filter.operator === \"isEmpty\" || filter.operator === \"isNotEmpty\") {\r\n return (\r\n \r\n );\r\n }\r\n\r\n switch (filter.variant) {\r\n case \"text\":\r\n case \"number\":\r\n case \"range\": {\r\n if (\r\n (filter.variant === \"range\" && filter.operator === \"isBetween\") ||\r\n filter.operator === \"isBetween\"\r\n ) {\r\n return (\r\n \r\n );\r\n }\r\n\r\n const isNumber =\r\n filter.variant === \"number\" || filter.variant === \"range\";\r\n\r\n return (\r\n \r\n onFilterUpdate(filter.filterId, { value: event.target.value })\r\n }\r\n />\r\n );\r\n }\r\n\r\n case \"boolean\": {\r\n const inputListboxId = `${inputId}-listbox`;\r\n\r\n return (\r\n \r\n onFilterUpdate(filter.filterId, { value })\r\n }\r\n >\r\n \r\n \r\n \r\n \r\n True\r\n False\r\n \r\n \r\n );\r\n }\r\n\r\n case \"select\":\r\n case \"multiSelect\": {\r\n const inputListboxId = `${inputId}-listbox`;\r\n\r\n const options = column.columnDef.meta?.options ?? [];\r\n const selectedValues = Array.isArray(filter.value)\r\n ? filter.value\r\n : [filter.value];\r\n\r\n const selectedOptions = options.filter((option) =>\r\n selectedValues.includes(option.value),\r\n );\r\n\r\n return (\r\n \r\n \r\n \r\n {selectedOptions.length === 0 ? (\r\n filter.variant === \"multiSelect\" ? (\r\n \"Select options...\"\r\n ) : (\r\n \"Select option...\"\r\n )\r\n ) : (\r\n <>\r\n
\r\n {selectedOptions.map((selectedOption) =>\r\n selectedOption.icon ? (\r\n \r\n \r\n
\r\n ) : null,\r\n )}\r\n
\r\n \r\n {selectedOptions.length > 1\r\n ? `${selectedOptions.length} selected`\r\n : selectedOptions[0]?.label}\r\n \r\n \r\n )}\r\n \r\n \r\n \r\n \r\n \r\n \r\n No options found.\r\n \r\n {options.map((option) => (\r\n {\r\n const value =\r\n filter.variant === \"multiSelect\"\r\n ? selectedValues.includes(option.value)\r\n ? selectedValues.filter((v) => v !== option.value)\r\n : [...selectedValues, option.value]\r\n : option.value;\r\n onFilterUpdate(filter.filterId, { value });\r\n }}\r\n >\r\n {option.icon && }\r\n {option.label}\r\n {filter.variant === \"multiSelect\" && (\r\n \r\n )}\r\n \r\n ))}\r\n \r\n \r\n \r\n \r\n \r\n );\r\n }\r\n\r\n case \"date\":\r\n case \"dateRange\": {\r\n const inputListboxId = `${inputId}-listbox`;\r\n\r\n const dateValue = Array.isArray(filter.value)\r\n ? filter.value.filter(Boolean)\r\n : [filter.value, filter.value].filter(Boolean);\r\n\r\n const displayValue =\r\n filter.operator === \"isBetween\" && dateValue.length === 2\r\n ? `${formatDate(new Date(Number(dateValue[0])))} - ${formatDate(\r\n new Date(Number(dateValue[1])),\r\n )}`\r\n : dateValue[0]\r\n ? formatDate(new Date(Number(dateValue[0])))\r\n : \"Pick date...\";\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n {displayValue}\r\n \r\n \r\n \r\n {filter.operator === \"isBetween\" ? (\r\n {\r\n onFilterUpdate(filter.filterId, {\r\n value: date\r\n ? [\r\n (date.from?.getTime() ?? \"\").toString(),\r\n (date.to?.getTime() ?? \"\").toString(),\r\n ]\r\n : [],\r\n });\r\n }}\r\n />\r\n ) : (\r\n {\r\n onFilterUpdate(filter.filterId, {\r\n value: (date?.getTime() ?? \"\").toString(),\r\n });\r\n }}\r\n />\r\n )}\r\n \r\n \r\n );\r\n }\r\n\r\n default:\r\n return null;\r\n }\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Column, Table } from \"@tanstack/react-table\";\nimport {\n BadgeCheck,\n CalendarIcon,\n Check,\n ListFilter,\n Text,\n X,\n} from \"lucide-react\";\nimport { useQueryState } from \"nuqs\";\nimport * as React from \"react\";\n\nimport { DataTableRangeFilter } from \"@/components/data-table/data-table-range-filter\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getDefaultFilterOperator, getFilterOperators } from \"@/lib/data-table\";\nimport { formatDate } from \"@/lib/format\";\nimport { generateId } from \"@/lib/id\";\nimport { getFiltersStateParser } from \"@/lib/parsers\";\nimport { cn } from \"@/lib/utils\";\nimport type { ExtendedColumnFilter, FilterOperator } from \"@/types/data-table\";\n\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\nconst FILTER_SHORTCUT_KEY = \"f\";\nconst REMOVE_FILTER_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableFilterMenuProps\n extends React.ComponentProps {\n table: Table;\n debounceMs?: number;\n throttleMs?: number;\n shallow?: boolean;\n}\n\nexport function DataTableFilterMenu({\n table,\n debounceMs = DEBOUNCE_MS,\n throttleMs = THROTTLE_MS,\n shallow = true,\n align = \"start\",\n ...props\n}: DataTableFilterMenuProps) {\n const id = React.useId();\n\n const columns = React.useMemo(() => {\n return table\n .getAllColumns()\n .filter((column) => column.columnDef.enableColumnFilter);\n }, [table]);\n\n const [open, setOpen] = React.useState(false);\n const [selectedColumn, setSelectedColumn] =\n React.useState | null>(null);\n const [inputValue, setInputValue] = React.useState(\"\");\n const triggerRef = React.useRef(null);\n const inputRef = React.useRef(null);\n\n const onOpenChange = React.useCallback((open: boolean) => {\n setOpen(open);\n\n if (!open) {\n setTimeout(() => {\n setSelectedColumn(null);\n setInputValue(\"\");\n }, 100);\n }\n }, []);\n\n const onInputKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\n !inputValue &&\n selectedColumn\n ) {\n event.preventDefault();\n setSelectedColumn(null);\n }\n },\n [inputValue, selectedColumn],\n );\n\n const [filters, setFilters] = useQueryState(\n table.options.meta?.queryKeys?.filters ?? \"filters\",\n getFiltersStateParser(columns.map((field) => field.id))\n .withDefault([])\n .withOptions({\n clearOnDefault: true,\n shallow,\n throttleMs,\n }),\n );\n const debouncedSetFilters = useDebouncedCallback(setFilters, debounceMs);\n\n const onFilterAdd = React.useCallback(\n (column: Column, value: string) => {\n if (!value.trim() && column.columnDef.meta?.variant !== \"boolean\") {\n return;\n }\n\n const filterValue =\n column.columnDef.meta?.variant === \"multiSelect\" ? [value] : value;\n\n const newFilter: ExtendedColumnFilter = {\n id: column.id as Extract,\n value: filterValue,\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n filterId: generateId({ length: 8 }),\n };\n\n debouncedSetFilters([...filters, newFilter]);\n setOpen(false);\n\n setTimeout(() => {\n setSelectedColumn(null);\n setInputValue(\"\");\n }, 100);\n },\n [filters, debouncedSetFilters],\n );\n\n const onFilterRemove = React.useCallback(\n (filterId: string) => {\n const updatedFilters = filters.filter(\n (filter) => filter.filterId !== filterId,\n );\n debouncedSetFilters(updatedFilters);\n requestAnimationFrame(() => {\n triggerRef.current?.focus();\n });\n },\n [filters, debouncedSetFilters],\n );\n\n const onFilterUpdate = React.useCallback(\n (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => {\n debouncedSetFilters((prevFilters) => {\n const updatedFilters = prevFilters.map((filter) => {\n if (filter.filterId === filterId) {\n return { ...filter, ...updates } as ExtendedColumnFilter;\n }\n return filter;\n });\n return updatedFilters;\n });\n },\n [debouncedSetFilters],\n );\n\n const onFiltersReset = React.useCallback(() => {\n debouncedSetFilters([]);\n }, [debouncedSetFilters]);\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement ||\n (event.target instanceof HTMLElement &&\n event.target.contentEditable === \"true\")\n ) {\n return;\n }\n\n if (\n event.key.toLowerCase() === FILTER_SHORTCUT_KEY &&\n (event.ctrlKey || event.metaKey) &&\n event.shiftKey\n ) {\n event.preventDefault();\n setOpen((prev) => !prev);\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, []);\n\n const onTriggerKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase()) &&\n filters.length > 0\n ) {\n event.preventDefault();\n onFilterRemove(filters[filters.length - 1]?.filterId ?? \"\");\n }\n },\n [filters, onFilterRemove],\n );\n\n return (\n
\n {filters.map((filter) => (\n \n ))}\n {filters.length > 0 && (\n \n \n \n )}\n \n \n 0 ? \"icon\" : \"sm\"}\n className={cn(filters.length > 0 && \"size-8\", \"h-8 font-normal\")}\n ref={triggerRef}\n onKeyDown={onTriggerKeyDown}\n >\n \n {filters.length > 0 ? null : \"Filter\"}\n \n \n \n \n \n \n {selectedColumn ? (\n <>\n {selectedColumn.columnDef.meta?.options && (\n No options found.\n )}\n onFilterAdd(selectedColumn, value)}\n />\n \n ) : (\n <>\n No fields found.\n \n {columns.map((column) => (\n {\n setSelectedColumn(column);\n setInputValue(\"\");\n requestAnimationFrame(() => {\n inputRef.current?.focus();\n });\n }}\n >\n {column.columnDef.meta?.icon && (\n \n )}\n \n {column.columnDef.meta?.label ?? column.id}\n \n \n ))}\n \n \n )}\n \n \n \n \n
\n );\n}\n\ninterface DataTableFilterItemProps {\n filter: ExtendedColumnFilter;\n filterItemId: string;\n columns: Column[];\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n onFilterRemove: (filterId: string) => void;\n}\n\nfunction DataTableFilterItem({\n filter,\n filterItemId,\n columns,\n onFilterUpdate,\n onFilterRemove,\n}: DataTableFilterItemProps) {\n {\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n const [showOperatorSelector, setShowOperatorSelector] =\n React.useState(false);\n const [showValueSelector, setShowValueSelector] = React.useState(false);\n\n const column = columns.find((column) => column.id === filter.id);\n\n const operatorListboxId = `${filterItemId}-operator-listbox`;\n const inputId = `${filterItemId}-input`;\n\n const columnMeta = column?.columnDef.meta;\n const filterOperators = getFilterOperators(filter.variant);\n\n const onItemKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (showFieldSelector || showOperatorSelector || showValueSelector) {\n return;\n }\n\n if (REMOVE_FILTER_SHORTCUTS.includes(event.key.toLowerCase())) {\n event.preventDefault();\n onFilterRemove(filter.filterId);\n }\n },\n [\n filter.filterId,\n showFieldSelector,\n showOperatorSelector,\n showValueSelector,\n onFilterRemove,\n ],\n );\n\n if (!column) return null;\n\n return (\n \n \n \n \n {columnMeta?.icon && (\n \n )}\n {columnMeta?.label ?? column.id}\n \n \n \n \n \n \n No fields found.\n \n {columns.map((column) => (\n {\n onFilterUpdate(filter.filterId, {\n id: column.id as Extract,\n variant: column.columnDef.meta?.variant ?? \"text\",\n operator: getDefaultFilterOperator(\n column.columnDef.meta?.variant ?? \"text\",\n ),\n value: \"\",\n });\n\n setShowFieldSelector(false);\n }}\n >\n {column.columnDef.meta?.icon && (\n \n )}\n \n {column.columnDef.meta?.label ?? column.id}\n \n \n \n ))}\n \n \n \n \n \n \n onFilterUpdate(filter.filterId, {\n operator: value,\n value:\n value === \"isEmpty\" || value === \"isNotEmpty\"\n ? \"\"\n : filter.value,\n })\n }\n >\n \n \n \n \n {filterOperators.map((operator) => (\n \n {operator.label}\n \n ))}\n \n \n {onFilterInputRender({\n filter,\n column,\n inputId,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n })}\n onFilterRemove(filter.filterId)}\n >\n \n \n
\n );\n }\n}\n\ninterface FilterValueSelectorProps {\n column: Column;\n value: string;\n onSelect: (value: string) => void;\n}\n\nfunction FilterValueSelector({\n column,\n value,\n onSelect,\n}: FilterValueSelectorProps) {\n const variant = column.columnDef.meta?.variant ?? \"text\";\n\n switch (variant) {\n case \"boolean\":\n return (\n \n onSelect(\"true\")}>\n True\n \n onSelect(\"false\")}>\n False\n \n \n );\n\n case \"select\":\n case \"multiSelect\":\n return (\n \n {column.columnDef.meta?.options?.map((option) => (\n onSelect(option.value)}\n >\n {option.icon && }\n {option.label}\n {option.count && (\n \n {option.count}\n \n )}\n \n ))}\n \n );\n\n case \"date\":\n case \"dateRange\":\n return (\n onSelect(date?.getTime().toString() ?? \"\")}\n />\n );\n\n default: {\n const isEmpty = !value.trim();\n\n return (\n \n onSelect(value)}\n disabled={isEmpty}\n >\n {isEmpty ? (\n <>\n \n Type to add filter...\n \n ) : (\n <>\n \n Filter by "{value}"\n \n )}\n \n \n );\n }\n }\n}\n\nfunction onFilterInputRender({\n filter,\n column,\n inputId,\n onFilterUpdate,\n showValueSelector,\n setShowValueSelector,\n}: {\n filter: ExtendedColumnFilter;\n column: Column;\n inputId: string;\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n showValueSelector: boolean;\n setShowValueSelector: (value: boolean) => void;\n}) {\n if (filter.operator === \"isEmpty\" || filter.operator === \"isNotEmpty\") {\n return (\n \n );\n }\n\n switch (filter.variant) {\n case \"text\":\n case \"number\":\n case \"range\": {\n if (\n (filter.variant === \"range\" && filter.operator === \"isBetween\") ||\n filter.operator === \"isBetween\"\n ) {\n return (\n \n );\n }\n\n const isNumber =\n filter.variant === \"number\" || filter.variant === \"range\";\n\n return (\n \n onFilterUpdate(filter.filterId, { value: event.target.value })\n }\n />\n );\n }\n\n case \"boolean\": {\n const inputListboxId = `${inputId}-listbox`;\n\n return (\n \n onFilterUpdate(filter.filterId, { value })\n }\n >\n \n \n \n \n True\n False\n \n \n );\n }\n\n case \"select\":\n case \"multiSelect\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const options = column.columnDef.meta?.options ?? [];\n const selectedValues = Array.isArray(filter.value)\n ? filter.value\n : [filter.value];\n\n const selectedOptions = options.filter((option) =>\n selectedValues.includes(option.value),\n );\n\n return (\n \n \n \n {selectedOptions.length === 0 ? (\n filter.variant === \"multiSelect\" ? (\n \"Select options...\"\n ) : (\n \"Select option...\"\n )\n ) : (\n <>\n
\n {selectedOptions.map((selectedOption) =>\n selectedOption.icon ? (\n \n \n
\n ) : null,\n )}\n
\n \n {selectedOptions.length > 1\n ? `${selectedOptions.length} selected`\n : selectedOptions[0]?.label}\n \n \n )}\n \n \n \n \n \n \n No options found.\n \n {options.map((option) => (\n {\n const value =\n filter.variant === \"multiSelect\"\n ? selectedValues.includes(option.value)\n ? selectedValues.filter((v) => v !== option.value)\n : [...selectedValues, option.value]\n : option.value;\n onFilterUpdate(filter.filterId, { value });\n }}\n >\n {option.icon && }\n {option.label}\n {filter.variant === \"multiSelect\" && (\n \n )}\n \n ))}\n \n \n \n \n \n );\n }\n\n case \"date\":\n case \"dateRange\": {\n const inputListboxId = `${inputId}-listbox`;\n\n const dateValue = Array.isArray(filter.value)\n ? filter.value.filter(Boolean)\n : [filter.value, filter.value].filter(Boolean);\n\n const displayValue =\n filter.operator === \"isBetween\" && dateValue.length === 2\n ? `${formatDate(new Date(Number(dateValue[0])))} - ${formatDate(\n new Date(Number(dateValue[1])),\n )}`\n : dateValue[0]\n ? formatDate(new Date(Number(dateValue[0])))\n : \"Pick date...\";\n\n return (\n \n \n \n \n {displayValue}\n \n \n \n {filter.operator === \"isBetween\" ? (\n {\n onFilterUpdate(filter.filterId, {\n value: date\n ? [\n (date.from?.getTime() ?? \"\").toString(),\n (date.to?.getTime() ?? \"\").toString(),\n ]\n : [],\n });\n }}\n />\n ) : (\n {\n onFilterUpdate(filter.filterId, {\n value: (date?.getTime() ?? \"\").toString(),\n });\n }}\n />\n )}\n \n \n );\n }\n\n default:\n return null;\n }\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-filter-menu.tsx" }, @@ -35,7 +35,7 @@ }, { "path": "src/components/data-table/data-table-advanced-toolbar.tsx", - "content": "\"use client\";\r\n\r\nimport type { Table } from \"@tanstack/react-table\";\r\nimport type * as React from \"react\";\r\n\r\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface DataTableAdvancedToolbarProps\r\n extends React.ComponentProps<\"div\"> {\r\n table: Table;\r\n}\r\n\r\nexport function DataTableAdvancedToolbar({\r\n table,\r\n children,\r\n className,\r\n ...props\r\n}: DataTableAdvancedToolbarProps) {\r\n return (\r\n \r\n
{children}
\r\n
\r\n \r\n
\r\n
\r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableAdvancedToolbarProps\n extends React.ComponentProps<\"div\"> {\n table: Table;\n}\n\nexport function DataTableAdvancedToolbar({\n table,\n children,\n className,\n ...props\n}: DataTableAdvancedToolbarProps) {\n return (\n \n
{children}
\n
\n \n
\n
\n );\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-advanced-toolbar.tsx" }, @@ -51,7 +51,7 @@ }, { "path": "src/lib/data-table.ts", - "content": "import type { Column } from \"@tanstack/react-table\";\r\nimport { dataTableConfig } from \"@/config/data-table\";\r\nimport type {\r\n ExtendedColumnFilter,\r\n FilterOperator,\r\n FilterVariant,\r\n} from \"@/types/data-table\";\r\n\r\nexport function getCommonPinningStyles({\r\n column,\r\n withBorder = false,\r\n}: {\r\n column: Column;\r\n withBorder?: boolean;\r\n}): React.CSSProperties {\r\n const isPinned = column.getIsPinned();\r\n const isLastLeftPinnedColumn =\r\n isPinned === \"left\" && column.getIsLastColumn(\"left\");\r\n const isFirstRightPinnedColumn =\r\n isPinned === \"right\" && column.getIsFirstColumn(\"right\");\r\n\r\n return {\r\n boxShadow: withBorder\r\n ? isLastLeftPinnedColumn\r\n ? \"-4px 0 4px -4px var(--border) inset\"\r\n : isFirstRightPinnedColumn\r\n ? \"4px 0 4px -4px var(--border) inset\"\r\n : undefined\r\n : undefined,\r\n left: isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\r\n right: isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined,\r\n opacity: isPinned ? 0.97 : 1,\r\n position: isPinned ? \"sticky\" : \"relative\",\r\n background: isPinned ? \"var(--background)\" : \"var(--background)\",\r\n width: column.getSize(),\r\n zIndex: isPinned ? 1 : undefined,\r\n };\r\n}\r\n\r\nexport function getFilterOperators(filterVariant: FilterVariant) {\r\n const operatorMap: Record<\r\n FilterVariant,\r\n { label: string; value: FilterOperator }[]\r\n > = {\r\n text: dataTableConfig.textOperators,\r\n number: dataTableConfig.numericOperators,\r\n range: dataTableConfig.numericOperators,\r\n date: dataTableConfig.dateOperators,\r\n dateRange: dataTableConfig.dateOperators,\r\n boolean: dataTableConfig.booleanOperators,\r\n select: dataTableConfig.selectOperators,\r\n multiSelect: dataTableConfig.multiSelectOperators,\r\n };\r\n\r\n return operatorMap[filterVariant] ?? dataTableConfig.textOperators;\r\n}\r\n\r\nexport function getDefaultFilterOperator(filterVariant: FilterVariant) {\r\n const operators = getFilterOperators(filterVariant);\r\n\r\n return operators[0]?.value ?? (filterVariant === \"text\" ? \"iLike\" : \"eq\");\r\n}\r\n\r\nexport function getValidFilters(\r\n filters: ExtendedColumnFilter[],\r\n): ExtendedColumnFilter[] {\r\n return filters.filter(\r\n (filter) =>\r\n filter.operator === \"isEmpty\" ||\r\n filter.operator === \"isNotEmpty\" ||\r\n (Array.isArray(filter.value)\r\n ? filter.value.length > 0\r\n : filter.value !== \"\" &&\r\n filter.value !== null &&\r\n filter.value !== undefined),\r\n );\r\n}\r\n", + "content": "import type { Column } from \"@tanstack/react-table\";\nimport { dataTableConfig } from \"@/config/data-table\";\nimport type {\n ExtendedColumnFilter,\n FilterOperator,\n FilterVariant,\n} from \"@/types/data-table\";\n\nexport function getCommonPinningStyles({\n column,\n withBorder = false,\n}: {\n column: Column;\n withBorder?: boolean;\n}): React.CSSProperties {\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn =\n isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn =\n isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\n right: isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined,\n opacity: isPinned ? 0.97 : 1,\n position: isPinned ? \"sticky\" : \"relative\",\n background: isPinned ? \"var(--background)\" : \"var(--background)\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : undefined,\n };\n}\n\nexport function getFilterOperators(filterVariant: FilterVariant) {\n const operatorMap: Record<\n FilterVariant,\n { label: string; value: FilterOperator }[]\n > = {\n text: dataTableConfig.textOperators,\n number: dataTableConfig.numericOperators,\n range: dataTableConfig.numericOperators,\n date: dataTableConfig.dateOperators,\n dateRange: dataTableConfig.dateOperators,\n boolean: dataTableConfig.booleanOperators,\n select: dataTableConfig.selectOperators,\n multiSelect: dataTableConfig.multiSelectOperators,\n };\n\n return operatorMap[filterVariant] ?? dataTableConfig.textOperators;\n}\n\nexport function getDefaultFilterOperator(filterVariant: FilterVariant) {\n const operators = getFilterOperators(filterVariant);\n\n return operators[0]?.value ?? (filterVariant === \"text\" ? \"iLike\" : \"eq\");\n}\n\nexport function getValidFilters(\n filters: ExtendedColumnFilter[],\n): ExtendedColumnFilter[] {\n return filters.filter(\n (filter) =>\n filter.operator === \"isEmpty\" ||\n filter.operator === \"isNotEmpty\" ||\n (Array.isArray(filter.value)\n ? filter.value.length > 0\n : filter.value !== \"\" &&\n filter.value !== null &&\n filter.value !== undefined),\n );\n}\n", "type": "registry:lib" }, { @@ -77,7 +77,7 @@ }, { "path": "src/types/data-table.ts", - "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\r\nimport type { DataTableConfig } from \"@/config/data-table\";\r\nimport type { FilterItemSchema } from \"@/lib/parsers\";\r\n\r\ndeclare module \"@tanstack/react-table\" {\r\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\r\n interface TableMeta {\r\n queryKeys?: QueryKeys;\r\n }\r\n\r\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\r\n interface ColumnMeta {\r\n label?: string;\r\n placeholder?: string;\r\n variant?: FilterVariant;\r\n options?: Option[];\r\n range?: [number, number];\r\n unit?: string;\r\n icon?: React.FC>;\r\n }\r\n}\r\n\r\nexport interface QueryKeys {\r\n page: string;\r\n perPage: string;\r\n sort: string;\r\n filters: string;\r\n joinOperator: string;\r\n}\r\n\r\nexport interface Option {\r\n label: string;\r\n value: string;\r\n count?: number;\r\n icon?: React.FC>;\r\n}\r\n\r\nexport type FilterOperator = DataTableConfig[\"operators\"][number];\r\nexport type FilterVariant = DataTableConfig[\"filterVariants\"][number];\r\nexport type JoinOperator = DataTableConfig[\"joinOperators\"][number];\r\n\r\nexport interface ExtendedColumnSort extends Omit {\r\n id: Extract;\r\n}\r\n\r\nexport interface ExtendedColumnFilter extends FilterItemSchema {\r\n id: Extract;\r\n}\r\n\r\nexport interface DataTableRowAction {\r\n row: Row;\r\n variant: \"update\" | \"delete\";\r\n}\r\n", + "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\nimport type { DataTableConfig } from \"@/config/data-table\";\nimport type { FilterItemSchema } from \"@/lib/parsers\";\n\ndeclare module \"@tanstack/react-table\" {\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\n interface TableMeta {\n queryKeys?: QueryKeys;\n }\n\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\n interface ColumnMeta {\n label?: string;\n placeholder?: string;\n variant?: FilterVariant;\n options?: Option[];\n range?: [number, number];\n unit?: string;\n icon?: React.FC>;\n }\n}\n\nexport interface QueryKeys {\n page: string;\n perPage: string;\n sort: string;\n filters: string;\n joinOperator: string;\n}\n\nexport interface Option {\n label: string;\n value: string;\n count?: number;\n icon?: React.FC>;\n}\n\nexport type FilterOperator = DataTableConfig[\"operators\"][number];\nexport type FilterVariant = DataTableConfig[\"filterVariants\"][number];\nexport type JoinOperator = DataTableConfig[\"joinOperators\"][number];\n\nexport interface ExtendedColumnSort extends Omit {\n id: Extract;\n}\n\nexport interface ExtendedColumnFilter extends FilterItemSchema {\n id: Extract;\n}\n\nexport interface DataTableRowAction {\n row: Row;\n variant: \"update\" | \"delete\";\n}\n", "type": "registry:file", "target": "src/types/data-table.ts" } diff --git a/public/r/data-table-sort-list.json b/public/r/data-table-sort-list.json index 0b4cbc4b2..338ff6f3e 100644 --- a/public/r/data-table-sort-list.json +++ b/public/r/data-table-sort-list.json @@ -18,13 +18,13 @@ "files": [ { "path": "src/components/data-table/data-table-sort-list.tsx", - "content": "\"use client\";\r\n\r\nimport type { ColumnSort, SortDirection, Table } from \"@tanstack/react-table\";\r\nimport {\r\n ArrowDownUp,\r\n ChevronsUpDown,\r\n GripVertical,\r\n Trash2,\r\n} from \"lucide-react\";\r\nimport * as React from \"react\";\r\n\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n} from \"@/components/ui/command\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport {\r\n Sortable,\r\n SortableContent,\r\n SortableItem,\r\n SortableItemHandle,\r\n SortableOverlay,\r\n} from \"@/components/ui/sortable\";\r\nimport { dataTableConfig } from \"@/config/data-table\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nconst SORT_SHORTCUT_KEY = \"s\";\r\nconst REMOVE_SORT_SHORTCUTS = [\"backspace\", \"delete\"];\r\n\r\ninterface DataTableSortListProps\r\n extends React.ComponentProps {\r\n table: Table;\r\n}\r\n\r\nexport function DataTableSortList({\r\n table,\r\n ...props\r\n}: DataTableSortListProps) {\r\n const id = React.useId();\r\n const labelId = React.useId();\r\n const descriptionId = React.useId();\r\n const [open, setOpen] = React.useState(false);\r\n const addButtonRef = React.useRef(null);\r\n\r\n const sorting = table.getState().sorting;\r\n const onSortingChange = table.setSorting;\r\n\r\n const { columnLabels, columns } = React.useMemo(() => {\r\n const labels = new Map();\r\n const sortingIds = new Set(sorting.map((s) => s.id));\r\n const availableColumns: { id: string; label: string }[] = [];\r\n\r\n for (const column of table.getAllColumns()) {\r\n if (!column.getCanSort()) continue;\r\n\r\n const label = column.columnDef.meta?.label ?? column.id;\r\n labels.set(column.id, label);\r\n\r\n if (!sortingIds.has(column.id)) {\r\n availableColumns.push({ id: column.id, label });\r\n }\r\n }\r\n\r\n return {\r\n columnLabels: labels,\r\n columns: availableColumns,\r\n };\r\n }, [sorting, table]);\r\n\r\n const onSortAdd = React.useCallback(() => {\r\n const firstColumn = columns[0];\r\n if (!firstColumn) return;\r\n\r\n onSortingChange((prevSorting) => [\r\n ...prevSorting,\r\n { id: firstColumn.id, desc: false },\r\n ]);\r\n }, [columns, onSortingChange]);\r\n\r\n const onSortUpdate = React.useCallback(\r\n (sortId: string, updates: Partial) => {\r\n onSortingChange((prevSorting) => {\r\n if (!prevSorting) return prevSorting;\r\n return prevSorting.map((sort) =>\r\n sort.id === sortId ? { ...sort, ...updates } : sort,\r\n );\r\n });\r\n },\r\n [onSortingChange],\r\n );\r\n\r\n const onSortRemove = React.useCallback(\r\n (sortId: string) => {\r\n onSortingChange((prevSorting) =>\r\n prevSorting.filter((item) => item.id !== sortId),\r\n );\r\n },\r\n [onSortingChange],\r\n );\r\n\r\n const onSortingReset = React.useCallback(\r\n () => onSortingChange(table.initialState.sorting),\r\n [onSortingChange, table.initialState.sorting],\r\n );\r\n\r\n React.useEffect(() => {\r\n function onKeyDown(event: KeyboardEvent) {\r\n if (\r\n event.target instanceof HTMLInputElement ||\r\n event.target instanceof HTMLTextAreaElement ||\r\n (event.target instanceof HTMLElement &&\r\n event.target.contentEditable === \"true\")\r\n ) {\r\n return;\r\n }\r\n\r\n if (\r\n event.key.toLowerCase() === SORT_SHORTCUT_KEY &&\r\n (event.ctrlKey || event.metaKey) &&\r\n event.shiftKey\r\n ) {\r\n event.preventDefault();\r\n setOpen((prev) => !prev);\r\n }\r\n }\r\n\r\n window.addEventListener(\"keydown\", onKeyDown);\r\n return () => window.removeEventListener(\"keydown\", onKeyDown);\r\n }, []);\r\n\r\n const onTriggerKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (\r\n REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase()) &&\r\n sorting.length > 0\r\n ) {\r\n event.preventDefault();\r\n onSortingReset();\r\n }\r\n },\r\n [sorting.length, onSortingReset],\r\n );\r\n\r\n return (\r\n item.id}\r\n >\r\n \r\n \r\n \r\n \r\n Sort\r\n {sorting.length > 0 && (\r\n \r\n {sorting.length}\r\n \r\n )}\r\n \r\n \r\n \r\n
\r\n

\r\n {sorting.length > 0 ? \"Sort by\" : \"No sorting applied\"}\r\n

\r\n 0 && \"sr-only\",\r\n )}\r\n >\r\n {sorting.length > 0\r\n ? \"Modify sorting to organize your rows.\"\r\n : \"Add sorting to organize your rows.\"}\r\n

\r\n
\r\n {sorting.length > 0 && (\r\n \r\n
    \r\n {sorting.map((sort) => (\r\n \r\n ))}\r\n
\r\n
\r\n )}\r\n
\r\n \r\n Add sort\r\n \r\n {sorting.length > 0 && (\r\n \r\n Reset sorting\r\n \r\n )}\r\n
\r\n \r\n
\r\n \r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n \r\n \r\n );\r\n}\r\n\r\ninterface DataTableSortItemProps {\r\n sort: ColumnSort;\r\n sortItemId: string;\r\n columns: { id: string; label: string }[];\r\n columnLabels: Map;\r\n onSortUpdate: (sortId: string, updates: Partial) => void;\r\n onSortRemove: (sortId: string) => void;\r\n}\r\n\r\nfunction DataTableSortItem({\r\n sort,\r\n sortItemId,\r\n columns,\r\n columnLabels,\r\n onSortUpdate,\r\n onSortRemove,\r\n}: DataTableSortItemProps) {\r\n const fieldListboxId = `${sortItemId}-field-listbox`;\r\n const fieldTriggerId = `${sortItemId}-field-trigger`;\r\n const directionListboxId = `${sortItemId}-direction-listbox`;\r\n\r\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\r\n const [showDirectionSelector, setShowDirectionSelector] =\r\n React.useState(false);\r\n\r\n const onItemKeyDown = React.useCallback(\r\n (event: React.KeyboardEvent) => {\r\n if (\r\n event.target instanceof HTMLInputElement ||\r\n event.target instanceof HTMLTextAreaElement\r\n ) {\r\n return;\r\n }\r\n\r\n if (showFieldSelector || showDirectionSelector) {\r\n return;\r\n }\r\n\r\n if (REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase())) {\r\n event.preventDefault();\r\n onSortRemove(sort.id);\r\n }\r\n },\r\n [sort.id, showFieldSelector, showDirectionSelector, onSortRemove],\r\n );\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n \r\n {columnLabels.get(sort.id)}\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n No fields found.\r\n \r\n {columns.map((column) => (\r\n onSortUpdate(sort.id, { id: value })}\r\n >\r\n {column.label}\r\n \r\n ))}\r\n \r\n \r\n \r\n \r\n \r\n \r\n onSortUpdate(sort.id, { desc: value === \"desc\" })\r\n }\r\n >\r\n \r\n \r\n \r\n \r\n {dataTableConfig.sortOrders.map((order) => (\r\n \r\n {order.label}\r\n \r\n ))}\r\n \r\n \r\n onSortRemove(sort.id)}\r\n >\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { ColumnSort, SortDirection, Table } from \"@tanstack/react-table\";\nimport {\n ArrowDownUp,\n ChevronsUpDown,\n GripVertical,\n Trash2,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n} from \"@/components/ui/sortable\";\nimport { dataTableConfig } from \"@/config/data-table\";\nimport { cn } from \"@/lib/utils\";\n\nconst SORT_SHORTCUT_KEY = \"s\";\nconst REMOVE_SORT_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableSortListProps\n extends React.ComponentProps {\n table: Table;\n}\n\nexport function DataTableSortList({\n table,\n ...props\n}: DataTableSortListProps) {\n const id = React.useId();\n const labelId = React.useId();\n const descriptionId = React.useId();\n const [open, setOpen] = React.useState(false);\n const addButtonRef = React.useRef(null);\n\n const sorting = table.getState().sorting;\n const onSortingChange = table.setSorting;\n\n const { columnLabels, columns } = React.useMemo(() => {\n const labels = new Map();\n const sortingIds = new Set(sorting.map((s) => s.id));\n const availableColumns: { id: string; label: string }[] = [];\n\n for (const column of table.getAllColumns()) {\n if (!column.getCanSort()) continue;\n\n const label = column.columnDef.meta?.label ?? column.id;\n labels.set(column.id, label);\n\n if (!sortingIds.has(column.id)) {\n availableColumns.push({ id: column.id, label });\n }\n }\n\n return {\n columnLabels: labels,\n columns: availableColumns,\n };\n }, [sorting, table]);\n\n const onSortAdd = React.useCallback(() => {\n const firstColumn = columns[0];\n if (!firstColumn) return;\n\n onSortingChange((prevSorting) => [\n ...prevSorting,\n { id: firstColumn.id, desc: false },\n ]);\n }, [columns, onSortingChange]);\n\n const onSortUpdate = React.useCallback(\n (sortId: string, updates: Partial) => {\n onSortingChange((prevSorting) => {\n if (!prevSorting) return prevSorting;\n return prevSorting.map((sort) =>\n sort.id === sortId ? { ...sort, ...updates } : sort,\n );\n });\n },\n [onSortingChange],\n );\n\n const onSortRemove = React.useCallback(\n (sortId: string) => {\n onSortingChange((prevSorting) =>\n prevSorting.filter((item) => item.id !== sortId),\n );\n },\n [onSortingChange],\n );\n\n const onSortingReset = React.useCallback(\n () => onSortingChange(table.initialState.sorting),\n [onSortingChange, table.initialState.sorting],\n );\n\n React.useEffect(() => {\n function onKeyDown(event: KeyboardEvent) {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement ||\n (event.target instanceof HTMLElement &&\n event.target.contentEditable === \"true\")\n ) {\n return;\n }\n\n if (\n event.key.toLowerCase() === SORT_SHORTCUT_KEY &&\n (event.ctrlKey || event.metaKey) &&\n event.shiftKey\n ) {\n event.preventDefault();\n setOpen((prev) => !prev);\n }\n }\n\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, []);\n\n const onTriggerKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase()) &&\n sorting.length > 0\n ) {\n event.preventDefault();\n onSortingReset();\n }\n },\n [sorting.length, onSortingReset],\n );\n\n return (\n item.id}\n >\n \n \n \n \n Sort\n {sorting.length > 0 && (\n \n {sorting.length}\n \n )}\n \n \n \n
\n

\n {sorting.length > 0 ? \"Sort by\" : \"No sorting applied\"}\n

\n 0 && \"sr-only\",\n )}\n >\n {sorting.length > 0\n ? \"Modify sorting to organize your rows.\"\n : \"Add sorting to organize your rows.\"}\n

\n
\n {sorting.length > 0 && (\n \n
    \n {sorting.map((sort) => (\n \n ))}\n
\n
\n )}\n
\n \n Add sort\n \n {sorting.length > 0 && (\n \n Reset sorting\n \n )}\n
\n \n
\n \n
\n
\n
\n
\n
\n
\n \n \n );\n}\n\ninterface DataTableSortItemProps {\n sort: ColumnSort;\n sortItemId: string;\n columns: { id: string; label: string }[];\n columnLabels: Map;\n onSortUpdate: (sortId: string, updates: Partial) => void;\n onSortRemove: (sortId: string) => void;\n}\n\nfunction DataTableSortItem({\n sort,\n sortItemId,\n columns,\n columnLabels,\n onSortUpdate,\n onSortRemove,\n}: DataTableSortItemProps) {\n const fieldListboxId = `${sortItemId}-field-listbox`;\n const fieldTriggerId = `${sortItemId}-field-trigger`;\n const directionListboxId = `${sortItemId}-direction-listbox`;\n\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n const [showDirectionSelector, setShowDirectionSelector] =\n React.useState(false);\n\n const onItemKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n event.target instanceof HTMLInputElement ||\n event.target instanceof HTMLTextAreaElement\n ) {\n return;\n }\n\n if (showFieldSelector || showDirectionSelector) {\n return;\n }\n\n if (REMOVE_SORT_SHORTCUTS.includes(event.key.toLowerCase())) {\n event.preventDefault();\n onSortRemove(sort.id);\n }\n },\n [sort.id, showFieldSelector, showDirectionSelector, onSortRemove],\n );\n\n return (\n \n \n \n \n \n {columnLabels.get(sort.id)}\n \n \n \n \n \n \n \n No fields found.\n \n {columns.map((column) => (\n onSortUpdate(sort.id, { id: value })}\n >\n {column.label}\n \n ))}\n \n \n \n \n \n \n onSortUpdate(sort.id, { desc: value === \"desc\" })\n }\n >\n \n \n \n \n {dataTableConfig.sortOrders.map((order) => (\n \n {order.label}\n \n ))}\n \n \n onSortRemove(sort.id)}\n >\n \n \n \n \n \n \n \n \n \n );\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-sort-list.tsx" }, { "path": "src/components/ui/sortable.tsx", - "content": "\"use client\";\r\n\r\nimport {\r\n type Announcements,\r\n closestCenter,\r\n closestCorners,\r\n DndContext,\r\n type DndContextProps,\r\n type DragEndEvent,\r\n type DraggableAttributes,\r\n type DraggableSyntheticListeners,\r\n DragOverlay,\r\n type DragStartEvent,\r\n type DropAnimation,\r\n defaultDropAnimationSideEffects,\r\n KeyboardSensor,\r\n MouseSensor,\r\n type ScreenReaderInstructions,\r\n TouchSensor,\r\n type UniqueIdentifier,\r\n useSensor,\r\n useSensors,\r\n} from \"@dnd-kit/core\";\r\nimport {\r\n restrictToHorizontalAxis,\r\n restrictToParentElement,\r\n restrictToVerticalAxis,\r\n} from \"@dnd-kit/modifiers\";\r\nimport {\r\n arrayMove,\r\n horizontalListSortingStrategy,\r\n SortableContext,\r\n type SortableContextProps,\r\n sortableKeyboardCoordinates,\r\n useSortable,\r\n verticalListSortingStrategy,\r\n} from \"@dnd-kit/sortable\";\r\nimport { CSS } from \"@dnd-kit/utilities\";\r\nimport { Slot } from \"@radix-ui/react-slot\";\r\nimport * as React from \"react\";\r\nimport * as ReactDOM from \"react-dom\";\r\nimport { useComposedRefs } from \"@/lib/compose-refs\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\nconst orientationConfig = {\r\n vertical: {\r\n modifiers: [restrictToVerticalAxis, restrictToParentElement],\r\n strategy: verticalListSortingStrategy,\r\n collisionDetection: closestCenter,\r\n },\r\n horizontal: {\r\n modifiers: [restrictToHorizontalAxis, restrictToParentElement],\r\n strategy: horizontalListSortingStrategy,\r\n collisionDetection: closestCenter,\r\n },\r\n mixed: {\r\n modifiers: [restrictToParentElement],\r\n strategy: undefined,\r\n collisionDetection: closestCorners,\r\n },\r\n};\r\n\r\nconst ROOT_NAME = \"Sortable\";\r\nconst CONTENT_NAME = \"SortableContent\";\r\nconst ITEM_NAME = \"SortableItem\";\r\nconst ITEM_HANDLE_NAME = \"SortableItemHandle\";\r\nconst OVERLAY_NAME = \"SortableOverlay\";\r\n\r\ninterface SortableRootContextValue {\r\n id: string;\r\n items: UniqueIdentifier[];\r\n modifiers: DndContextProps[\"modifiers\"];\r\n strategy: SortableContextProps[\"strategy\"];\r\n activeId: UniqueIdentifier | null;\r\n setActiveId: (id: UniqueIdentifier | null) => void;\r\n getItemValue: (item: T) => UniqueIdentifier;\r\n flatCursor: boolean;\r\n}\r\n\r\nconst SortableRootContext =\r\n React.createContext | null>(null);\r\n\r\nfunction useSortableContext(consumerName: string) {\r\n const context = React.useContext(SortableRootContext);\r\n if (!context) {\r\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\r\n }\r\n return context;\r\n}\r\n\r\ninterface GetItemValue {\r\n /**\r\n * Callback that returns a unique identifier for each sortable item. Required for array of objects.\r\n * @example getItemValue={(item) => item.id}\r\n */\r\n getItemValue: (item: T) => UniqueIdentifier;\r\n}\r\n\r\ntype SortableRootProps = DndContextProps &\r\n (T extends object ? GetItemValue : Partial>) & {\r\n value: T[];\r\n onValueChange?: (items: T[]) => void;\r\n onMove?: (\r\n event: DragEndEvent & { activeIndex: number; overIndex: number },\r\n ) => void;\r\n strategy?: SortableContextProps[\"strategy\"];\r\n orientation?: \"vertical\" | \"horizontal\" | \"mixed\";\r\n flatCursor?: boolean;\r\n };\r\n\r\nfunction SortableRoot(props: SortableRootProps) {\r\n const {\r\n value,\r\n onValueChange,\r\n collisionDetection,\r\n modifiers,\r\n strategy,\r\n onMove,\r\n orientation = \"vertical\",\r\n flatCursor = false,\r\n getItemValue: getItemValueProp,\r\n accessibility,\r\n onDragStart: onDragStartProp,\r\n onDragEnd: onDragEndProp,\r\n onDragCancel: onDragCancelProp,\r\n ...sortableProps\r\n } = props;\r\n\r\n const id = React.useId();\r\n const [activeId, setActiveId] = React.useState(null);\r\n\r\n const sensors = useSensors(\r\n useSensor(MouseSensor),\r\n useSensor(TouchSensor),\r\n useSensor(KeyboardSensor, {\r\n coordinateGetter: sortableKeyboardCoordinates,\r\n }),\r\n );\r\n const config = React.useMemo(\r\n () => orientationConfig[orientation],\r\n [orientation],\r\n );\r\n\r\n const getItemValue = React.useCallback(\r\n (item: T): UniqueIdentifier => {\r\n if (typeof item === \"object\" && !getItemValueProp) {\r\n throw new Error(\"getItemValue is required when using array of objects\");\r\n }\r\n return getItemValueProp\r\n ? getItemValueProp(item)\r\n : (item as UniqueIdentifier);\r\n },\r\n [getItemValueProp],\r\n );\r\n\r\n const items = React.useMemo(() => {\r\n return value.map((item) => getItemValue(item));\r\n }, [value, getItemValue]);\r\n\r\n const onDragStart = React.useCallback(\r\n (event: DragStartEvent) => {\r\n onDragStartProp?.(event);\r\n\r\n if (event.activatorEvent.defaultPrevented) return;\r\n\r\n setActiveId(event.active.id);\r\n },\r\n [onDragStartProp],\r\n );\r\n\r\n const onDragEnd = React.useCallback(\r\n (event: DragEndEvent) => {\r\n onDragEndProp?.(event);\r\n\r\n if (event.activatorEvent.defaultPrevented) return;\r\n\r\n const { active, over } = event;\r\n if (over && active.id !== over?.id) {\r\n const activeIndex = value.findIndex(\r\n (item) => getItemValue(item) === active.id,\r\n );\r\n const overIndex = value.findIndex(\r\n (item) => getItemValue(item) === over.id,\r\n );\r\n\r\n if (onMove) {\r\n onMove({ ...event, activeIndex, overIndex });\r\n } else {\r\n onValueChange?.(arrayMove(value, activeIndex, overIndex));\r\n }\r\n }\r\n setActiveId(null);\r\n },\r\n [value, onValueChange, onMove, getItemValue, onDragEndProp],\r\n );\r\n\r\n const onDragCancel = React.useCallback(\r\n (event: DragEndEvent) => {\r\n onDragCancelProp?.(event);\r\n\r\n if (event.activatorEvent.defaultPrevented) return;\r\n\r\n setActiveId(null);\r\n },\r\n [onDragCancelProp],\r\n );\r\n\r\n const announcements: Announcements = React.useMemo(\r\n () => ({\r\n onDragStart({ active }) {\r\n const activeValue = active.id.toString();\r\n return `Grabbed sortable item \"${activeValue}\". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;\r\n },\r\n onDragOver({ active, over }) {\r\n if (over) {\r\n const overIndex = over.data.current?.sortable.index ?? 0;\r\n const activeIndex = active.data.current?.sortable.index ?? 0;\r\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\r\n const activeValue = active.id.toString();\r\n return `Sortable item \"${activeValue}\" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\r\n }\r\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\r\n },\r\n onDragEnd({ active, over }) {\r\n const activeValue = active.id.toString();\r\n if (over) {\r\n const overIndex = over.data.current?.sortable.index ?? 0;\r\n return `Sortable item \"${activeValue}\" dropped at position ${overIndex + 1} of ${value.length}.`;\r\n }\r\n return `Sortable item \"${activeValue}\" dropped. No changes were made.`;\r\n },\r\n onDragCancel({ active }) {\r\n const activeIndex = active.data.current?.sortable.index ?? 0;\r\n const activeValue = active.id.toString();\r\n return `Sorting cancelled. Sortable item \"${activeValue}\" returned to position ${activeIndex + 1} of ${value.length}.`;\r\n },\r\n onDragMove({ active, over }) {\r\n if (over) {\r\n const overIndex = over.data.current?.sortable.index ?? 0;\r\n const activeIndex = active.data.current?.sortable.index ?? 0;\r\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\r\n const activeValue = active.id.toString();\r\n return `Sortable item \"${activeValue}\" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\r\n }\r\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\r\n },\r\n }),\r\n [value],\r\n );\r\n\r\n const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(\r\n () => ({\r\n draggable: `\r\n To pick up a sortable item, press space or enter.\r\n While dragging, use the ${orientation === \"vertical\" ? \"up and down\" : orientation === \"horizontal\" ? \"left and right\" : \"arrow\"} keys to move the item.\r\n Press space or enter again to drop the item in its new position, or press escape to cancel.\r\n `,\r\n }),\r\n [orientation],\r\n );\r\n\r\n const contextValue = React.useMemo(\r\n () => ({\r\n id,\r\n items,\r\n modifiers: modifiers ?? config.modifiers,\r\n strategy: strategy ?? config.strategy,\r\n activeId,\r\n setActiveId,\r\n getItemValue,\r\n flatCursor,\r\n }),\r\n [\r\n id,\r\n items,\r\n modifiers,\r\n strategy,\r\n config.modifiers,\r\n config.strategy,\r\n activeId,\r\n getItemValue,\r\n flatCursor,\r\n ],\r\n );\r\n\r\n return (\r\n }\r\n >\r\n \r\n \r\n );\r\n}\r\n\r\nconst SortableContentContext = React.createContext(false);\r\n\r\ninterface SortableContentProps extends React.ComponentProps<\"div\"> {\r\n strategy?: SortableContextProps[\"strategy\"];\r\n children: React.ReactNode;\r\n asChild?: boolean;\r\n withoutSlot?: boolean;\r\n}\r\n\r\nfunction SortableContent(props: SortableContentProps) {\r\n const {\r\n strategy: strategyProp,\r\n asChild,\r\n withoutSlot,\r\n children,\r\n ref,\r\n ...contentProps\r\n } = props;\r\n\r\n const context = useSortableContext(CONTENT_NAME);\r\n\r\n const ContentPrimitive = asChild ? Slot : \"div\";\r\n\r\n return (\r\n \r\n \r\n {withoutSlot ? (\r\n children\r\n ) : (\r\n \r\n {children}\r\n \r\n )}\r\n \r\n \r\n );\r\n}\r\n\r\ninterface SortableItemContextValue {\r\n id: string;\r\n attributes: DraggableAttributes;\r\n listeners: DraggableSyntheticListeners | undefined;\r\n setActivatorNodeRef: (node: HTMLElement | null) => void;\r\n isDragging?: boolean;\r\n disabled?: boolean;\r\n}\r\n\r\nconst SortableItemContext =\r\n React.createContext(null);\r\n\r\nfunction useSortableItemContext(consumerName: string) {\r\n const context = React.useContext(SortableItemContext);\r\n if (!context) {\r\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\r\n }\r\n return context;\r\n}\r\n\r\ninterface SortableItemProps extends React.ComponentProps<\"div\"> {\r\n value: UniqueIdentifier;\r\n asHandle?: boolean;\r\n asChild?: boolean;\r\n disabled?: boolean;\r\n}\r\n\r\nfunction SortableItem(props: SortableItemProps) {\r\n const {\r\n value,\r\n style,\r\n asHandle,\r\n asChild,\r\n disabled,\r\n className,\r\n ref,\r\n ...itemProps\r\n } = props;\r\n\r\n const inSortableContent = React.useContext(SortableContentContext);\r\n const inSortableOverlay = React.useContext(SortableOverlayContext);\r\n\r\n if (!inSortableContent && !inSortableOverlay) {\r\n throw new Error(\r\n `\\`${ITEM_NAME}\\` must be used within \\`${CONTENT_NAME}\\` or \\`${OVERLAY_NAME}\\``,\r\n );\r\n }\r\n\r\n if (value === \"\") {\r\n throw new Error(`\\`${ITEM_NAME}\\` value cannot be an empty string`);\r\n }\r\n\r\n const context = useSortableContext(ITEM_NAME);\r\n const id = React.useId();\r\n const {\r\n attributes,\r\n listeners,\r\n setNodeRef,\r\n setActivatorNodeRef,\r\n transform,\r\n transition,\r\n isDragging,\r\n } = useSortable({ id: value, disabled });\r\n\r\n const composedRef = useComposedRefs(ref, (node) => {\r\n if (disabled) return;\r\n setNodeRef(node);\r\n if (asHandle) setActivatorNodeRef(node);\r\n });\r\n\r\n const composedStyle = React.useMemo(() => {\r\n return {\r\n transform: CSS.Translate.toString(transform),\r\n transition,\r\n ...style,\r\n };\r\n }, [transform, transition, style]);\r\n\r\n const itemContext = React.useMemo(\r\n () => ({\r\n id,\r\n attributes,\r\n listeners,\r\n setActivatorNodeRef,\r\n isDragging,\r\n disabled,\r\n }),\r\n [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],\r\n );\r\n\r\n const ItemPrimitive = asChild ? Slot : \"div\";\r\n\r\n return (\r\n \r\n \r\n \r\n );\r\n}\r\n\r\ninterface SortableItemHandleProps extends React.ComponentProps<\"button\"> {\r\n asChild?: boolean;\r\n}\r\n\r\nfunction SortableItemHandle(props: SortableItemHandleProps) {\r\n const { asChild, disabled, className, ref, ...itemHandleProps } = props;\r\n\r\n const context = useSortableContext(ITEM_HANDLE_NAME);\r\n const itemContext = useSortableItemContext(ITEM_HANDLE_NAME);\r\n\r\n const isDisabled = disabled ?? itemContext.disabled;\r\n\r\n const composedRef = useComposedRefs(ref, (node) => {\r\n if (!isDisabled) return;\r\n itemContext.setActivatorNodeRef(node);\r\n });\r\n\r\n const HandlePrimitive = asChild ? Slot : \"button\";\r\n\r\n return (\r\n \r\n );\r\n}\r\n\r\nconst SortableOverlayContext = React.createContext(false);\r\n\r\nconst dropAnimation: DropAnimation = {\r\n sideEffects: defaultDropAnimationSideEffects({\r\n styles: {\r\n active: {\r\n opacity: \"0.4\",\r\n },\r\n },\r\n }),\r\n};\r\n\r\ninterface SortableOverlayProps\r\n extends Omit, \"children\"> {\r\n container?: Element | DocumentFragment | null;\r\n children?:\r\n | ((params: { value: UniqueIdentifier }) => React.ReactNode)\r\n | React.ReactNode;\r\n}\r\n\r\nfunction SortableOverlay(props: SortableOverlayProps) {\r\n const { container: containerProp, children, ...overlayProps } = props;\r\n\r\n const context = useSortableContext(OVERLAY_NAME);\r\n\r\n const [mounted, setMounted] = React.useState(false);\r\n React.useLayoutEffect(() => setMounted(true), []);\r\n\r\n const container =\r\n containerProp ?? (mounted ? globalThis.document?.body : null);\r\n\r\n if (!container) return null;\r\n\r\n return ReactDOM.createPortal(\r\n \r\n \r\n {context.activeId\r\n ? typeof children === \"function\"\r\n ? children({ value: context.activeId })\r\n : children\r\n : null}\r\n \r\n ,\r\n container,\r\n );\r\n}\r\n\r\nexport {\r\n SortableRoot as Sortable,\r\n SortableContent,\r\n SortableItem,\r\n SortableItemHandle,\r\n SortableOverlay,\r\n //\r\n SortableRoot as Root,\r\n SortableContent as Content,\r\n SortableItem as Item,\r\n SortableItemHandle as ItemHandle,\r\n SortableOverlay as Overlay,\r\n};\r\n", + "content": "\"use client\";\n\nimport {\n type Announcements,\n closestCenter,\n closestCorners,\n DndContext,\n type DndContextProps,\n type DragEndEvent,\n type DraggableAttributes,\n type DraggableSyntheticListeners,\n DragOverlay,\n type DragStartEvent,\n type DropAnimation,\n defaultDropAnimationSideEffects,\n KeyboardSensor,\n MouseSensor,\n type ScreenReaderInstructions,\n TouchSensor,\n type UniqueIdentifier,\n useSensor,\n useSensors,\n} from \"@dnd-kit/core\";\nimport {\n restrictToHorizontalAxis,\n restrictToParentElement,\n restrictToVerticalAxis,\n} from \"@dnd-kit/modifiers\";\nimport {\n arrayMove,\n horizontalListSortingStrategy,\n SortableContext,\n type SortableContextProps,\n sortableKeyboardCoordinates,\n useSortable,\n verticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\n\nconst orientationConfig = {\n vertical: {\n modifiers: [restrictToVerticalAxis, restrictToParentElement],\n strategy: verticalListSortingStrategy,\n collisionDetection: closestCenter,\n },\n horizontal: {\n modifiers: [restrictToHorizontalAxis, restrictToParentElement],\n strategy: horizontalListSortingStrategy,\n collisionDetection: closestCenter,\n },\n mixed: {\n modifiers: [restrictToParentElement],\n strategy: undefined,\n collisionDetection: closestCorners,\n },\n};\n\nconst ROOT_NAME = \"Sortable\";\nconst CONTENT_NAME = \"SortableContent\";\nconst ITEM_NAME = \"SortableItem\";\nconst ITEM_HANDLE_NAME = \"SortableItemHandle\";\nconst OVERLAY_NAME = \"SortableOverlay\";\n\ninterface SortableRootContextValue {\n id: string;\n items: UniqueIdentifier[];\n modifiers: DndContextProps[\"modifiers\"];\n strategy: SortableContextProps[\"strategy\"];\n activeId: UniqueIdentifier | null;\n setActiveId: (id: UniqueIdentifier | null) => void;\n getItemValue: (item: T) => UniqueIdentifier;\n flatCursor: boolean;\n}\n\nconst SortableRootContext =\n React.createContext | null>(null);\n\nfunction useSortableContext(consumerName: string) {\n const context = React.useContext(SortableRootContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ROOT_NAME}\\``);\n }\n return context;\n}\n\ninterface GetItemValue {\n /**\n * Callback that returns a unique identifier for each sortable item. Required for array of objects.\n * @example getItemValue={(item) => item.id}\n */\n getItemValue: (item: T) => UniqueIdentifier;\n}\n\ntype SortableRootProps = DndContextProps &\n (T extends object ? GetItemValue : Partial>) & {\n value: T[];\n onValueChange?: (items: T[]) => void;\n onMove?: (\n event: DragEndEvent & { activeIndex: number; overIndex: number },\n ) => void;\n strategy?: SortableContextProps[\"strategy\"];\n orientation?: \"vertical\" | \"horizontal\" | \"mixed\";\n flatCursor?: boolean;\n };\n\nfunction SortableRoot(props: SortableRootProps) {\n const {\n value,\n onValueChange,\n collisionDetection,\n modifiers,\n strategy,\n onMove,\n orientation = \"vertical\",\n flatCursor = false,\n getItemValue: getItemValueProp,\n accessibility,\n onDragStart: onDragStartProp,\n onDragEnd: onDragEndProp,\n onDragCancel: onDragCancelProp,\n ...sortableProps\n } = props;\n\n const id = React.useId();\n const [activeId, setActiveId] = React.useState(null);\n\n const sensors = useSensors(\n useSensor(MouseSensor),\n useSensor(TouchSensor),\n useSensor(KeyboardSensor, {\n coordinateGetter: sortableKeyboardCoordinates,\n }),\n );\n const config = React.useMemo(\n () => orientationConfig[orientation],\n [orientation],\n );\n\n const getItemValue = React.useCallback(\n (item: T): UniqueIdentifier => {\n if (typeof item === \"object\" && !getItemValueProp) {\n throw new Error(\"getItemValue is required when using array of objects\");\n }\n return getItemValueProp\n ? getItemValueProp(item)\n : (item as UniqueIdentifier);\n },\n [getItemValueProp],\n );\n\n const items = React.useMemo(() => {\n return value.map((item) => getItemValue(item));\n }, [value, getItemValue]);\n\n const onDragStart = React.useCallback(\n (event: DragStartEvent) => {\n onDragStartProp?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(event.active.id);\n },\n [onDragStartProp],\n );\n\n const onDragEnd = React.useCallback(\n (event: DragEndEvent) => {\n onDragEndProp?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n const { active, over } = event;\n if (over && active.id !== over?.id) {\n const activeIndex = value.findIndex(\n (item) => getItemValue(item) === active.id,\n );\n const overIndex = value.findIndex(\n (item) => getItemValue(item) === over.id,\n );\n\n if (onMove) {\n onMove({ ...event, activeIndex, overIndex });\n } else {\n onValueChange?.(arrayMove(value, activeIndex, overIndex));\n }\n }\n setActiveId(null);\n },\n [value, onValueChange, onMove, getItemValue, onDragEndProp],\n );\n\n const onDragCancel = React.useCallback(\n (event: DragEndEvent) => {\n onDragCancelProp?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(null);\n },\n [onDragCancelProp],\n );\n\n const announcements: Announcements = React.useMemo(\n () => ({\n onDragStart({ active }) {\n const activeValue = active.id.toString();\n return `Grabbed sortable item \"${activeValue}\". Current position is ${active.data.current?.sortable.index + 1} of ${value.length}. Use arrow keys to move, space to drop.`;\n },\n onDragOver({ active, over }) {\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n const activeValue = active.id.toString();\n return `Sortable item \"${activeValue}\" moved ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n }\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n },\n onDragEnd({ active, over }) {\n const activeValue = active.id.toString();\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n return `Sortable item \"${activeValue}\" dropped at position ${overIndex + 1} of ${value.length}.`;\n }\n return `Sortable item \"${activeValue}\" dropped. No changes were made.`;\n },\n onDragCancel({ active }) {\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const activeValue = active.id.toString();\n return `Sorting cancelled. Sortable item \"${activeValue}\" returned to position ${activeIndex + 1} of ${value.length}.`;\n },\n onDragMove({ active, over }) {\n if (over) {\n const overIndex = over.data.current?.sortable.index ?? 0;\n const activeIndex = active.data.current?.sortable.index ?? 0;\n const moveDirection = overIndex > activeIndex ? \"down\" : \"up\";\n const activeValue = active.id.toString();\n return `Sortable item \"${activeValue}\" is moving ${moveDirection} to position ${overIndex + 1} of ${value.length}.`;\n }\n return \"Sortable item is no longer over a droppable area. Press escape to cancel.\";\n },\n }),\n [value],\n );\n\n const screenReaderInstructions: ScreenReaderInstructions = React.useMemo(\n () => ({\n draggable: `\n To pick up a sortable item, press space or enter.\n While dragging, use the ${orientation === \"vertical\" ? \"up and down\" : orientation === \"horizontal\" ? \"left and right\" : \"arrow\"} keys to move the item.\n Press space or enter again to drop the item in its new position, or press escape to cancel.\n `,\n }),\n [orientation],\n );\n\n const contextValue = React.useMemo(\n () => ({\n id,\n items,\n modifiers: modifiers ?? config.modifiers,\n strategy: strategy ?? config.strategy,\n activeId,\n setActiveId,\n getItemValue,\n flatCursor,\n }),\n [\n id,\n items,\n modifiers,\n strategy,\n config.modifiers,\n config.strategy,\n activeId,\n getItemValue,\n flatCursor,\n ],\n );\n\n return (\n }\n >\n \n \n );\n}\n\nconst SortableContentContext = React.createContext(false);\n\ninterface SortableContentProps extends React.ComponentProps<\"div\"> {\n strategy?: SortableContextProps[\"strategy\"];\n children: React.ReactNode;\n asChild?: boolean;\n withoutSlot?: boolean;\n}\n\nfunction SortableContent(props: SortableContentProps) {\n const {\n strategy: strategyProp,\n asChild,\n withoutSlot,\n children,\n ref,\n ...contentProps\n } = props;\n\n const context = useSortableContext(CONTENT_NAME);\n\n const ContentPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n {withoutSlot ? (\n children\n ) : (\n \n {children}\n \n )}\n \n \n );\n}\n\ninterface SortableItemContextValue {\n id: string;\n attributes: DraggableAttributes;\n listeners: DraggableSyntheticListeners | undefined;\n setActivatorNodeRef: (node: HTMLElement | null) => void;\n isDragging?: boolean;\n disabled?: boolean;\n}\n\nconst SortableItemContext =\n React.createContext(null);\n\nfunction useSortableItemContext(consumerName: string) {\n const context = React.useContext(SortableItemContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`${ITEM_NAME}\\``);\n }\n return context;\n}\n\ninterface SortableItemProps extends React.ComponentProps<\"div\"> {\n value: UniqueIdentifier;\n asHandle?: boolean;\n asChild?: boolean;\n disabled?: boolean;\n}\n\nfunction SortableItem(props: SortableItemProps) {\n const {\n value,\n style,\n asHandle,\n asChild,\n disabled,\n className,\n ref,\n ...itemProps\n } = props;\n\n const inSortableContent = React.useContext(SortableContentContext);\n const inSortableOverlay = React.useContext(SortableOverlayContext);\n\n if (!inSortableContent && !inSortableOverlay) {\n throw new Error(\n `\\`${ITEM_NAME}\\` must be used within \\`${CONTENT_NAME}\\` or \\`${OVERLAY_NAME}\\``,\n );\n }\n\n if (value === \"\") {\n throw new Error(`\\`${ITEM_NAME}\\` value cannot be an empty string`);\n }\n\n const context = useSortableContext(ITEM_NAME);\n const id = React.useId();\n const {\n attributes,\n listeners,\n setNodeRef,\n setActivatorNodeRef,\n transform,\n transition,\n isDragging,\n } = useSortable({ id: value, disabled });\n\n const composedRef = useComposedRefs(ref, (node) => {\n if (disabled) return;\n setNodeRef(node);\n if (asHandle) setActivatorNodeRef(node);\n });\n\n const composedStyle = React.useMemo(() => {\n return {\n transform: CSS.Translate.toString(transform),\n transition,\n ...style,\n };\n }, [transform, transition, style]);\n\n const itemContext = React.useMemo(\n () => ({\n id,\n attributes,\n listeners,\n setActivatorNodeRef,\n isDragging,\n disabled,\n }),\n [id, attributes, listeners, setActivatorNodeRef, isDragging, disabled],\n );\n\n const ItemPrimitive = asChild ? Slot : \"div\";\n\n return (\n \n \n \n );\n}\n\ninterface SortableItemHandleProps extends React.ComponentProps<\"button\"> {\n asChild?: boolean;\n}\n\nfunction SortableItemHandle(props: SortableItemHandleProps) {\n const { asChild, disabled, className, ref, ...itemHandleProps } = props;\n\n const context = useSortableContext(ITEM_HANDLE_NAME);\n const itemContext = useSortableItemContext(ITEM_HANDLE_NAME);\n\n const isDisabled = disabled ?? itemContext.disabled;\n\n const composedRef = useComposedRefs(ref, (node) => {\n if (!isDisabled) return;\n itemContext.setActivatorNodeRef(node);\n });\n\n const HandlePrimitive = asChild ? Slot : \"button\";\n\n return (\n \n );\n}\n\nconst SortableOverlayContext = React.createContext(false);\n\nconst dropAnimation: DropAnimation = {\n sideEffects: defaultDropAnimationSideEffects({\n styles: {\n active: {\n opacity: \"0.4\",\n },\n },\n }),\n};\n\ninterface SortableOverlayProps\n extends Omit, \"children\"> {\n container?: Element | DocumentFragment | null;\n children?:\n | ((params: { value: UniqueIdentifier }) => React.ReactNode)\n | React.ReactNode;\n}\n\nfunction SortableOverlay(props: SortableOverlayProps) {\n const { container: containerProp, children, ...overlayProps } = props;\n\n const context = useSortableContext(OVERLAY_NAME);\n\n const [mounted, setMounted] = React.useState(false);\n React.useLayoutEffect(() => setMounted(true), []);\n\n const container =\n containerProp ?? (mounted ? globalThis.document?.body : null);\n\n if (!container) return null;\n\n return ReactDOM.createPortal(\n \n \n {context.activeId\n ? typeof children === \"function\"\n ? children({ value: context.activeId })\n : children\n : null}\n \n ,\n container,\n );\n}\n\nexport {\n SortableRoot as Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n //\n SortableRoot as Root,\n SortableContent as Content,\n SortableItem as Item,\n SortableItemHandle as ItemHandle,\n SortableOverlay as Overlay,\n};\n", "type": "registry:ui", "target": "src/components/ui/sortable.tsx" }, @@ -41,7 +41,7 @@ }, { "path": "src/types/data-table.ts", - "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\r\nimport type { DataTableConfig } from \"@/config/data-table\";\r\nimport type { FilterItemSchema } from \"@/lib/parsers\";\r\n\r\ndeclare module \"@tanstack/react-table\" {\r\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\r\n interface TableMeta {\r\n queryKeys?: QueryKeys;\r\n }\r\n\r\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\r\n interface ColumnMeta {\r\n label?: string;\r\n placeholder?: string;\r\n variant?: FilterVariant;\r\n options?: Option[];\r\n range?: [number, number];\r\n unit?: string;\r\n icon?: React.FC>;\r\n }\r\n}\r\n\r\nexport interface QueryKeys {\r\n page: string;\r\n perPage: string;\r\n sort: string;\r\n filters: string;\r\n joinOperator: string;\r\n}\r\n\r\nexport interface Option {\r\n label: string;\r\n value: string;\r\n count?: number;\r\n icon?: React.FC>;\r\n}\r\n\r\nexport type FilterOperator = DataTableConfig[\"operators\"][number];\r\nexport type FilterVariant = DataTableConfig[\"filterVariants\"][number];\r\nexport type JoinOperator = DataTableConfig[\"joinOperators\"][number];\r\n\r\nexport interface ExtendedColumnSort extends Omit {\r\n id: Extract;\r\n}\r\n\r\nexport interface ExtendedColumnFilter extends FilterItemSchema {\r\n id: Extract;\r\n}\r\n\r\nexport interface DataTableRowAction {\r\n row: Row;\r\n variant: \"update\" | \"delete\";\r\n}\r\n", + "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\nimport type { DataTableConfig } from \"@/config/data-table\";\nimport type { FilterItemSchema } from \"@/lib/parsers\";\n\ndeclare module \"@tanstack/react-table\" {\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\n interface TableMeta {\n queryKeys?: QueryKeys;\n }\n\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\n interface ColumnMeta {\n label?: string;\n placeholder?: string;\n variant?: FilterVariant;\n options?: Option[];\n range?: [number, number];\n unit?: string;\n icon?: React.FC>;\n }\n}\n\nexport interface QueryKeys {\n page: string;\n perPage: string;\n sort: string;\n filters: string;\n joinOperator: string;\n}\n\nexport interface Option {\n label: string;\n value: string;\n count?: number;\n icon?: React.FC>;\n}\n\nexport type FilterOperator = DataTableConfig[\"operators\"][number];\nexport type FilterVariant = DataTableConfig[\"filterVariants\"][number];\nexport type JoinOperator = DataTableConfig[\"joinOperators\"][number];\n\nexport interface ExtendedColumnSort extends Omit {\n id: Extract;\n}\n\nexport interface ExtendedColumnFilter extends FilterItemSchema {\n id: Extract;\n}\n\nexport interface DataTableRowAction {\n row: Row;\n variant: \"update\" | \"delete\";\n}\n", "type": "registry:file", "target": "src/types/data-table.ts" } diff --git a/public/r/data-table.json b/public/r/data-table.json index ee19f2433..60fda87d9 100644 --- a/public/r/data-table.json +++ b/public/r/data-table.json @@ -31,49 +31,49 @@ }, { "path": "src/components/data-table/data-table-column-header.tsx", - "content": "\"use client\";\r\n\r\nimport type { Column } from \"@tanstack/react-table\";\r\nimport {\r\n ChevronDown,\r\n ChevronsUpDown,\r\n ChevronUp,\r\n EyeOff,\r\n X,\r\n} from \"lucide-react\";\r\n\r\nimport {\r\n DropdownMenu,\r\n DropdownMenuCheckboxItem,\r\n DropdownMenuContent,\r\n DropdownMenuItem,\r\n DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface DataTableColumnHeaderProps\r\n extends React.ComponentProps {\r\n column: Column;\r\n label: string;\r\n}\r\n\r\nexport function DataTableColumnHeader({\r\n column,\r\n label,\r\n className,\r\n ...props\r\n}: DataTableColumnHeaderProps) {\r\n if (!column.getCanSort() && !column.getCanHide()) {\r\n return
{label}
;\r\n }\r\n\r\n return (\r\n \r\n \r\n {label}\r\n {column.getCanSort() &&\r\n (column.getIsSorted() === \"desc\" ? (\r\n \r\n ) : column.getIsSorted() === \"asc\" ? (\r\n \r\n ) : (\r\n \r\n ))}\r\n \r\n \r\n {column.getCanSort() && (\r\n <>\r\n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\r\n checked={column.getIsSorted() === \"asc\"}\r\n onClick={() => column.toggleSorting(false)}\r\n >\r\n \r\n Asc\r\n \r\n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\r\n checked={column.getIsSorted() === \"desc\"}\r\n onClick={() => column.toggleSorting(true)}\r\n >\r\n \r\n Desc\r\n \r\n {column.getIsSorted() && (\r\n column.clearSorting()}\r\n >\r\n \r\n Reset\r\n \r\n )}\r\n \r\n )}\r\n {column.getCanHide() && (\r\n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\r\n checked={!column.getIsVisible()}\r\n onClick={() => column.toggleVisibility(false)}\r\n >\r\n \r\n Hide\r\n \r\n )}\r\n \r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport {\n ChevronDown,\n ChevronsUpDown,\n ChevronUp,\n EyeOff,\n X,\n} from \"lucide-react\";\n\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableColumnHeaderProps\n extends React.ComponentProps {\n column: Column;\n label: string;\n}\n\nexport function DataTableColumnHeader({\n column,\n label,\n className,\n ...props\n}: DataTableColumnHeaderProps) {\n if (!column.getCanSort() && !column.getCanHide()) {\n return
{label}
;\n }\n\n return (\n \n \n {label}\n {column.getCanSort() &&\n (column.getIsSorted() === \"desc\" ? (\n \n ) : column.getIsSorted() === \"asc\" ? (\n \n ) : (\n \n ))}\n \n \n {column.getCanSort() && (\n <>\n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"asc\"}\n onClick={() => column.toggleSorting(false)}\n >\n \n Asc\n \n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={column.getIsSorted() === \"desc\"}\n onClick={() => column.toggleSorting(true)}\n >\n \n Desc\n \n {column.getIsSorted() && (\n column.clearSorting()}\n >\n \n Reset\n \n )}\n \n )}\n {column.getCanHide() && (\n span:first-child]:right-2 [&>span:first-child]:left-auto [&_svg]:text-muted-foreground\"\n checked={!column.getIsVisible()}\n onClick={() => column.toggleVisibility(false)}\n >\n \n Hide\n \n )}\n \n \n );\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-column-header.tsx" }, { "path": "src/components/data-table/data-table-pagination.tsx", - "content": "import type { Table } from \"@tanstack/react-table\";\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n} from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTablePaginationProps extends React.ComponentProps<\"div\"> {\n table: Table;\n pageSizeOptions?: number[];\n}\n\nexport function DataTablePagination({\n table,\n pageSizeOptions = [10, 20, 30, 40, 50],\n className,\n ...props\n}: DataTablePaginationProps) {\n return (\n \n
\n {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n {table.getFilteredRowModel().rows.length} row(s) selected.\n
\n
\n
\n

Rows per page

\n {\n table.setPageSize(Number(value));\n }}\n >\n \n \n \n \n {pageSizeOptions.map((pageSize) => (\n \n {pageSize}\n \n ))}\n \n \n
\n
\n Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n {table.getPageCount()}\n
\n
\n table.setPageIndex(0)}\n disabled={!table.getCanPreviousPage()}\n >\n \n \n table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n >\n \n \n table.nextPage()}\n disabled={!table.getCanNextPage()}\n >\n \n \n table.setPageIndex(table.getPageCount() - 1)}\n disabled={!table.getCanNextPage()}\n >\n \n \n
\n
\n
\n );\n}\n", + "content": "import type { Table } from \"@tanstack/react-table\";\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n} from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTablePaginationProps extends React.ComponentProps<\"div\"> {\n table: Table;\n pageSizeOptions?: number[];\n}\n\nexport function DataTablePagination({\n table,\n pageSizeOptions = [10, 20, 30, 40, 50],\n className,\n ...props\n}: DataTablePaginationProps) {\n return (\n \n
\n {table.getFilteredSelectedRowModel().rows.length} of{\" \"}\n {table.getFilteredRowModel().rows.length} row(s) selected.\n
\n
\n
\n

Rows per page

\n {\n table.setPageSize(Number(value));\n }}\n >\n \n \n \n \n {pageSizeOptions.map((pageSize) => (\n \n {pageSize}\n \n ))}\n \n \n
\n
\n Page {table.getState().pagination.pageIndex + 1} of{\" \"}\n {table.getPageCount()}\n
\n
\n table.setPageIndex(0)}\n disabled={!table.getCanPreviousPage()}\n >\n \n \n table.previousPage()}\n disabled={!table.getCanPreviousPage()}\n >\n \n \n table.nextPage()}\n disabled={!table.getCanNextPage()}\n >\n \n \n table.setPageIndex(table.getPageCount() - 1)}\n disabled={!table.getCanNextPage()}\n >\n \n \n
\n
\n
\n );\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-pagination.tsx" }, { "path": "src/components/data-table/data-table-view-options.tsx", - "content": "\"use client\";\r\n\r\nimport type { Table } from \"@tanstack/react-table\";\r\nimport { Check, Settings2 } from \"lucide-react\";\r\nimport * as React from \"react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n} from \"@/components/ui/command\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface DataTableViewOptionsProps\r\n extends React.ComponentProps {\r\n table: Table;\r\n}\r\n\r\nexport function DataTableViewOptions({\r\n table,\r\n ...props\r\n}: DataTableViewOptionsProps) {\r\n const columns = React.useMemo(\r\n () =>\r\n table\r\n .getAllColumns()\r\n .filter(\r\n (column) =>\r\n typeof column.accessorFn !== \"undefined\" && column.getCanHide(),\r\n ),\r\n [table],\r\n );\r\n\r\n return (\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n No columns found.\r\n \r\n {columns.map((column) => (\r\n \r\n column.toggleVisibility(!column.getIsVisible())\r\n }\r\n >\r\n \r\n {column.columnDef.meta?.label ?? column.id}\r\n \r\n \r\n \r\n ))}\r\n \r\n \r\n \r\n \r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Table } from \"@tanstack/react-table\";\nimport { Check, Settings2 } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableViewOptionsProps\n extends React.ComponentProps {\n table: Table;\n}\n\nexport function DataTableViewOptions({\n table,\n ...props\n}: DataTableViewOptionsProps) {\n const columns = React.useMemo(\n () =>\n table\n .getAllColumns()\n .filter(\n (column) =>\n typeof column.accessorFn !== \"undefined\" && column.getCanHide(),\n ),\n [table],\n );\n\n return (\n \n \n \n \n \n \n \n No columns found.\n \n {columns.map((column) => (\n \n column.toggleVisibility(!column.getIsVisible())\n }\n >\n \n {column.columnDef.meta?.label ?? column.id}\n \n \n \n ))}\n \n \n \n \n \n );\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-view-options.tsx" }, { "path": "src/components/data-table/data-table-faceted-filter.tsx", - "content": "\"use client\";\r\n\r\nimport type { Column } from \"@tanstack/react-table\";\r\nimport { Check, PlusCircle, XCircle } from \"lucide-react\";\r\nimport * as React from \"react\";\r\n\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Command,\r\n CommandEmpty,\r\n CommandGroup,\r\n CommandInput,\r\n CommandItem,\r\n CommandList,\r\n CommandSeparator,\r\n} from \"@/components/ui/command\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport type { Option } from \"@/types/data-table\";\r\n\r\ninterface DataTableFacetedFilterProps {\r\n column?: Column;\r\n title?: string;\r\n options: Option[];\r\n multiple?: boolean;\r\n}\r\n\r\nexport function DataTableFacetedFilter({\r\n column,\r\n title,\r\n options,\r\n multiple,\r\n}: DataTableFacetedFilterProps) {\r\n const [open, setOpen] = React.useState(false);\r\n\r\n const columnFilterValue = column?.getFilterValue();\r\n const selectedValues = new Set(\r\n Array.isArray(columnFilterValue) ? columnFilterValue : [],\r\n );\r\n\r\n const onItemSelect = React.useCallback(\r\n (option: Option, isSelected: boolean) => {\r\n if (!column) return;\r\n\r\n if (multiple) {\r\n const newSelectedValues = new Set(selectedValues);\r\n if (isSelected) {\r\n newSelectedValues.delete(option.value);\r\n } else {\r\n newSelectedValues.add(option.value);\r\n }\r\n const filterValues = Array.from(newSelectedValues);\r\n column.setFilterValue(filterValues.length ? filterValues : undefined);\r\n } else {\r\n column.setFilterValue(isSelected ? undefined : [option.value]);\r\n setOpen(false);\r\n }\r\n },\r\n [column, multiple, selectedValues],\r\n );\r\n\r\n const onReset = React.useCallback(\r\n (event?: React.MouseEvent) => {\r\n event?.stopPropagation();\r\n column?.setFilterValue(undefined);\r\n },\r\n [column],\r\n );\r\n\r\n return (\r\n \r\n \r\n \r\n {selectedValues?.size > 0 ? (\r\n \r\n \r\n
\r\n ) : (\r\n \r\n )}\r\n {title}\r\n {selectedValues?.size > 0 && (\r\n <>\r\n \r\n \r\n {selectedValues.size}\r\n \r\n
\r\n {selectedValues.size > 2 ? (\r\n \r\n {selectedValues.size} selected\r\n \r\n ) : (\r\n options\r\n .filter((option) => selectedValues.has(option.value))\r\n .map((option) => (\r\n \r\n {option.label}\r\n \r\n ))\r\n )}\r\n
\r\n \r\n )}\r\n \r\n \r\n \r\n \r\n \r\n \r\n No results found.\r\n \r\n {options.map((option) => {\r\n const isSelected = selectedValues.has(option.value);\r\n\r\n return (\r\n onItemSelect(option, isSelected)}\r\n >\r\n \r\n \r\n
\r\n {option.icon && }\r\n {option.label}\r\n {option.count && (\r\n \r\n {option.count}\r\n \r\n )}\r\n \r\n );\r\n })}\r\n \r\n {selectedValues.size > 0 && (\r\n <>\r\n \r\n \r\n onReset()}\r\n className=\"justify-center text-center\"\r\n >\r\n Clear filters\r\n \r\n \r\n \r\n )}\r\n \r\n \r\n \r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport { Check, PlusCircle, XCircle } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/utils\";\nimport type { Option } from \"@/types/data-table\";\n\ninterface DataTableFacetedFilterProps {\n column?: Column;\n title?: string;\n options: Option[];\n multiple?: boolean;\n}\n\nexport function DataTableFacetedFilter({\n column,\n title,\n options,\n multiple,\n}: DataTableFacetedFilterProps) {\n const [open, setOpen] = React.useState(false);\n\n const columnFilterValue = column?.getFilterValue();\n const selectedValues = new Set(\n Array.isArray(columnFilterValue) ? columnFilterValue : [],\n );\n\n const onItemSelect = React.useCallback(\n (option: Option, isSelected: boolean) => {\n if (!column) return;\n\n if (multiple) {\n const newSelectedValues = new Set(selectedValues);\n if (isSelected) {\n newSelectedValues.delete(option.value);\n } else {\n newSelectedValues.add(option.value);\n }\n const filterValues = Array.from(newSelectedValues);\n column.setFilterValue(filterValues.length ? filterValues : undefined);\n } else {\n column.setFilterValue(isSelected ? undefined : [option.value]);\n setOpen(false);\n }\n },\n [column, multiple, selectedValues],\n );\n\n const onReset = React.useCallback(\n (event?: React.MouseEvent) => {\n event?.stopPropagation();\n column?.setFilterValue(undefined);\n },\n [column],\n );\n\n return (\n \n \n \n {selectedValues?.size > 0 ? (\n \n \n
\n ) : (\n \n )}\n {title}\n {selectedValues?.size > 0 && (\n <>\n \n \n {selectedValues.size}\n \n
\n {selectedValues.size > 2 ? (\n \n {selectedValues.size} selected\n \n ) : (\n options\n .filter((option) => selectedValues.has(option.value))\n .map((option) => (\n \n {option.label}\n \n ))\n )}\n
\n \n )}\n \n \n \n \n \n \n No results found.\n \n {options.map((option) => {\n const isSelected = selectedValues.has(option.value);\n\n return (\n onItemSelect(option, isSelected)}\n >\n \n \n
\n {option.icon && }\n {option.label}\n {option.count && (\n \n {option.count}\n \n )}\n \n );\n })}\n \n {selectedValues.size > 0 && (\n <>\n \n \n onReset()}\n className=\"justify-center text-center\"\n >\n Clear filters\n \n \n \n )}\n \n \n \n \n );\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-faceted-filter.tsx" }, { "path": "src/components/data-table/data-table-toolbar.tsx", - "content": "\"use client\";\r\n\r\nimport type { Column, Table } from \"@tanstack/react-table\";\r\nimport { X } from \"lucide-react\";\r\nimport * as React from \"react\";\r\n\r\nimport { DataTableDateFilter } from \"@/components/data-table/data-table-date-filter\";\r\nimport { DataTableFacetedFilter } from \"@/components/data-table/data-table-faceted-filter\";\r\nimport { DataTableSliderFilter } from \"@/components/data-table/data-table-slider-filter\";\r\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface DataTableToolbarProps extends React.ComponentProps<\"div\"> {\r\n table: Table;\r\n}\r\n\r\nexport function DataTableToolbar({\r\n table,\r\n children,\r\n className,\r\n ...props\r\n}: DataTableToolbarProps) {\r\n const isFiltered = table.getState().columnFilters.length > 0;\r\n\r\n const columns = React.useMemo(\r\n () => table.getAllColumns().filter((column) => column.getCanFilter()),\r\n [table],\r\n );\r\n\r\n const onReset = React.useCallback(() => {\r\n table.resetColumnFilters();\r\n }, [table]);\r\n\r\n return (\r\n \r\n
\r\n {columns.map((column) => (\r\n \r\n ))}\r\n {isFiltered && (\r\n \r\n \r\n Reset\r\n \r\n )}\r\n
\r\n
\r\n {children}\r\n \r\n
\r\n
\r\n );\r\n}\r\ninterface DataTableToolbarFilterProps {\r\n column: Column;\r\n}\r\n\r\nfunction DataTableToolbarFilter({\r\n column,\r\n}: DataTableToolbarFilterProps) {\r\n {\r\n const columnMeta = column.columnDef.meta;\r\n\r\n const onFilterRender = React.useCallback(() => {\r\n if (!columnMeta?.variant) return null;\r\n\r\n switch (columnMeta.variant) {\r\n case \"text\":\r\n return (\r\n column.setFilterValue(event.target.value)}\r\n className=\"h-8 w-40 lg:w-56\"\r\n />\r\n );\r\n\r\n case \"number\":\r\n return (\r\n
\r\n column.setFilterValue(event.target.value)}\r\n className={cn(\"h-8 w-[120px]\", columnMeta.unit && \"pr-8\")}\r\n />\r\n {columnMeta.unit && (\r\n \r\n {columnMeta.unit}\r\n \r\n )}\r\n
\r\n );\r\n\r\n case \"range\":\r\n return (\r\n \r\n );\r\n\r\n case \"date\":\r\n case \"dateRange\":\r\n return (\r\n \r\n );\r\n\r\n case \"select\":\r\n case \"multiSelect\":\r\n return (\r\n \r\n );\r\n\r\n default:\r\n return null;\r\n }\r\n }, [column, columnMeta]);\r\n\r\n return onFilterRender();\r\n }\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Column, Table } from \"@tanstack/react-table\";\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport { DataTableDateFilter } from \"@/components/data-table/data-table-date-filter\";\nimport { DataTableFacetedFilter } from \"@/components/data-table/data-table-faceted-filter\";\nimport { DataTableSliderFilter } from \"@/components/data-table/data-table-slider-filter\";\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableToolbarProps extends React.ComponentProps<\"div\"> {\n table: Table;\n}\n\nexport function DataTableToolbar({\n table,\n children,\n className,\n ...props\n}: DataTableToolbarProps) {\n const isFiltered = table.getState().columnFilters.length > 0;\n\n const columns = React.useMemo(\n () => table.getAllColumns().filter((column) => column.getCanFilter()),\n [table],\n );\n\n const onReset = React.useCallback(() => {\n table.resetColumnFilters();\n }, [table]);\n\n return (\n \n
\n {columns.map((column) => (\n \n ))}\n {isFiltered && (\n \n \n Reset\n \n )}\n
\n
\n {children}\n \n
\n
\n );\n}\ninterface DataTableToolbarFilterProps {\n column: Column;\n}\n\nfunction DataTableToolbarFilter({\n column,\n}: DataTableToolbarFilterProps) {\n {\n const columnMeta = column.columnDef.meta;\n\n const onFilterRender = React.useCallback(() => {\n if (!columnMeta?.variant) return null;\n\n switch (columnMeta.variant) {\n case \"text\":\n return (\n column.setFilterValue(event.target.value)}\n className=\"h-8 w-40 lg:w-56\"\n />\n );\n\n case \"number\":\n return (\n
\n column.setFilterValue(event.target.value)}\n className={cn(\"h-8 w-[120px]\", columnMeta.unit && \"pr-8\")}\n />\n {columnMeta.unit && (\n \n {columnMeta.unit}\n \n )}\n
\n );\n\n case \"range\":\n return (\n \n );\n\n case \"date\":\n case \"dateRange\":\n return (\n \n );\n\n case \"select\":\n case \"multiSelect\":\n return (\n \n );\n\n default:\n return null;\n }\n }, [column, columnMeta]);\n\n return onFilterRender();\n }\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-toolbar.tsx" }, { "path": "src/components/data-table/data-table-slider-filter.tsx", - "content": "\"use client\";\r\n\r\nimport type { Column } from \"@tanstack/react-table\";\r\nimport { PlusCircle, XCircle } from \"lucide-react\";\r\nimport * as React from \"react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Slider } from \"@/components/ui/slider\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface Range {\r\n min: number;\r\n max: number;\r\n}\r\n\r\ntype RangeValue = [number, number];\r\n\r\nfunction getIsValidRange(value: unknown): value is RangeValue {\r\n return (\r\n Array.isArray(value) &&\r\n value.length === 2 &&\r\n typeof value[0] === \"number\" &&\r\n typeof value[1] === \"number\"\r\n );\r\n}\r\n\r\nfunction parseValuesAsNumbers(value: unknown): RangeValue | undefined {\r\n if (\r\n Array.isArray(value) &&\r\n value.length === 2 &&\r\n value.every(\r\n (v) =>\r\n (typeof v === \"string\" || typeof v === \"number\") && !Number.isNaN(v),\r\n )\r\n ) {\r\n return [Number(value[0]), Number(value[1])];\r\n }\r\n\r\n return undefined;\r\n}\r\n\r\ninterface DataTableSliderFilterProps {\r\n column: Column;\r\n title?: string;\r\n}\r\n\r\nexport function DataTableSliderFilter({\r\n column,\r\n title,\r\n}: DataTableSliderFilterProps) {\r\n const id = React.useId();\r\n\r\n const columnFilterValue = parseValuesAsNumbers(column.getFilterValue());\r\n\r\n const defaultRange = column.columnDef.meta?.range;\r\n const unit = column.columnDef.meta?.unit;\r\n\r\n const { min, max, step } = React.useMemo(() => {\r\n let minValue = 0;\r\n let maxValue = 100;\r\n\r\n if (defaultRange && getIsValidRange(defaultRange)) {\r\n [minValue, maxValue] = defaultRange;\r\n } else {\r\n const values = column.getFacetedMinMaxValues();\r\n if (values && Array.isArray(values) && values.length === 2) {\r\n const [facetMinValue, facetMaxValue] = values;\r\n if (\r\n typeof facetMinValue === \"number\" &&\r\n typeof facetMaxValue === \"number\"\r\n ) {\r\n minValue = facetMinValue;\r\n maxValue = facetMaxValue;\r\n }\r\n }\r\n }\r\n\r\n const rangeSize = maxValue - minValue;\r\n const step =\r\n rangeSize <= 20\r\n ? 1\r\n : rangeSize <= 100\r\n ? Math.ceil(rangeSize / 20)\r\n : Math.ceil(rangeSize / 50);\r\n\r\n return { min: minValue, max: maxValue, step };\r\n }, [column, defaultRange]);\r\n\r\n const range = React.useMemo((): RangeValue => {\r\n return columnFilterValue ?? [min, max];\r\n }, [columnFilterValue, min, max]);\r\n\r\n const formatValue = React.useCallback((value: number) => {\r\n return value.toLocaleString(undefined, { maximumFractionDigits: 0 });\r\n }, []);\r\n\r\n const onFromInputChange = React.useCallback(\r\n (event: React.ChangeEvent) => {\r\n const numValue = Number(event.target.value);\r\n if (!Number.isNaN(numValue) && numValue >= min && numValue <= range[1]) {\r\n column.setFilterValue([numValue, range[1]]);\r\n }\r\n },\r\n [column, min, range],\r\n );\r\n\r\n const onToInputChange = React.useCallback(\r\n (event: React.ChangeEvent) => {\r\n const numValue = Number(event.target.value);\r\n if (!Number.isNaN(numValue) && numValue <= max && numValue >= range[0]) {\r\n column.setFilterValue([range[0], numValue]);\r\n }\r\n },\r\n [column, max, range],\r\n );\r\n\r\n const onSliderValueChange = React.useCallback(\r\n (value: RangeValue) => {\r\n if (Array.isArray(value) && value.length === 2) {\r\n column.setFilterValue(value);\r\n }\r\n },\r\n [column],\r\n );\r\n\r\n const onReset = React.useCallback(\r\n (event: React.MouseEvent) => {\r\n if (event.target instanceof HTMLDivElement) {\r\n event.stopPropagation();\r\n }\r\n column.setFilterValue(undefined);\r\n },\r\n [column],\r\n );\r\n\r\n return (\r\n \r\n \r\n \r\n {columnFilterValue ? (\r\n \r\n \r\n \r\n ) : (\r\n \r\n )}\r\n {title}\r\n {columnFilterValue ? (\r\n <>\r\n \r\n {formatValue(columnFilterValue[0])} -{\" \"}\r\n {formatValue(columnFilterValue[1])}\r\n {unit ? ` ${unit}` : \"\"}\r\n \r\n ) : null}\r\n \r\n \r\n \r\n
\r\n

\r\n {title}\r\n

\r\n
\r\n \r\n
\r\n \r\n {unit && (\r\n \r\n {unit}\r\n \r\n )}\r\n
\r\n \r\n
\r\n \r\n {unit && (\r\n \r\n {unit}\r\n \r\n )}\r\n
\r\n
\r\n \r\n \r\n
\r\n \r\n Clear\r\n \r\n
\r\n
\r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport { PlusCircle, XCircle } from \"lucide-react\";\nimport * as React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { cn } from \"@/lib/utils\";\n\ninterface Range {\n min: number;\n max: number;\n}\n\ntype RangeValue = [number, number];\n\nfunction getIsValidRange(value: unknown): value is RangeValue {\n return (\n Array.isArray(value) &&\n value.length === 2 &&\n typeof value[0] === \"number\" &&\n typeof value[1] === \"number\"\n );\n}\n\nfunction parseValuesAsNumbers(value: unknown): RangeValue | undefined {\n if (\n Array.isArray(value) &&\n value.length === 2 &&\n value.every(\n (v) =>\n (typeof v === \"string\" || typeof v === \"number\") && !Number.isNaN(v),\n )\n ) {\n return [Number(value[0]), Number(value[1])];\n }\n\n return undefined;\n}\n\ninterface DataTableSliderFilterProps {\n column: Column;\n title?: string;\n}\n\nexport function DataTableSliderFilter({\n column,\n title,\n}: DataTableSliderFilterProps) {\n const id = React.useId();\n\n const columnFilterValue = parseValuesAsNumbers(column.getFilterValue());\n\n const defaultRange = column.columnDef.meta?.range;\n const unit = column.columnDef.meta?.unit;\n\n const { min, max, step } = React.useMemo(() => {\n let minValue = 0;\n let maxValue = 100;\n\n if (defaultRange && getIsValidRange(defaultRange)) {\n [minValue, maxValue] = defaultRange;\n } else {\n const values = column.getFacetedMinMaxValues();\n if (values && Array.isArray(values) && values.length === 2) {\n const [facetMinValue, facetMaxValue] = values;\n if (\n typeof facetMinValue === \"number\" &&\n typeof facetMaxValue === \"number\"\n ) {\n minValue = facetMinValue;\n maxValue = facetMaxValue;\n }\n }\n }\n\n const rangeSize = maxValue - minValue;\n const step =\n rangeSize <= 20\n ? 1\n : rangeSize <= 100\n ? Math.ceil(rangeSize / 20)\n : Math.ceil(rangeSize / 50);\n\n return { min: minValue, max: maxValue, step };\n }, [column, defaultRange]);\n\n const range = React.useMemo((): RangeValue => {\n return columnFilterValue ?? [min, max];\n }, [columnFilterValue, min, max]);\n\n const formatValue = React.useCallback((value: number) => {\n return value.toLocaleString(undefined, { maximumFractionDigits: 0 });\n }, []);\n\n const onFromInputChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const numValue = Number(event.target.value);\n if (!Number.isNaN(numValue) && numValue >= min && numValue <= range[1]) {\n column.setFilterValue([numValue, range[1]]);\n }\n },\n [column, min, range],\n );\n\n const onToInputChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const numValue = Number(event.target.value);\n if (!Number.isNaN(numValue) && numValue <= max && numValue >= range[0]) {\n column.setFilterValue([range[0], numValue]);\n }\n },\n [column, max, range],\n );\n\n const onSliderValueChange = React.useCallback(\n (value: RangeValue) => {\n if (Array.isArray(value) && value.length === 2) {\n column.setFilterValue(value);\n }\n },\n [column],\n );\n\n const onReset = React.useCallback(\n (event: React.MouseEvent) => {\n if (event.target instanceof HTMLDivElement) {\n event.stopPropagation();\n }\n column.setFilterValue(undefined);\n },\n [column],\n );\n\n return (\n \n \n \n {columnFilterValue ? (\n \n \n \n ) : (\n \n )}\n {title}\n {columnFilterValue ? (\n <>\n \n {formatValue(columnFilterValue[0])} -{\" \"}\n {formatValue(columnFilterValue[1])}\n {unit ? ` ${unit}` : \"\"}\n \n ) : null}\n \n \n \n
\n

\n {title}\n

\n
\n \n
\n \n {unit && (\n \n {unit}\n \n )}\n
\n \n
\n \n {unit && (\n \n {unit}\n \n )}\n
\n
\n \n \n
\n \n Clear\n \n
\n
\n );\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-slider-filter.tsx" }, { "path": "src/components/data-table/data-table-date-filter.tsx", - "content": "\"use client\";\r\n\r\nimport type { Column } from \"@tanstack/react-table\";\r\nimport { CalendarIcon, XCircle } from \"lucide-react\";\r\nimport * as React from \"react\";\r\nimport type { DateRange } from \"react-day-picker\";\r\n\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Calendar } from \"@/components/ui/calendar\";\r\nimport {\r\n Popover,\r\n PopoverContent,\r\n PopoverTrigger,\r\n} from \"@/components/ui/popover\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { formatDate } from \"@/lib/format\";\r\n\r\ntype DateSelection = Date[] | DateRange;\r\n\r\nfunction getIsDateRange(value: DateSelection): value is DateRange {\r\n return value && typeof value === \"object\" && !Array.isArray(value);\r\n}\r\n\r\nfunction parseAsDate(timestamp: number | string | undefined): Date | undefined {\r\n if (!timestamp) return undefined;\r\n const numericTimestamp =\r\n typeof timestamp === \"string\" ? Number(timestamp) : timestamp;\r\n const date = new Date(numericTimestamp);\r\n return !Number.isNaN(date.getTime()) ? date : undefined;\r\n}\r\n\r\nfunction parseColumnFilterValue(value: unknown) {\r\n if (value === null || value === undefined) {\r\n return [];\r\n }\r\n\r\n if (Array.isArray(value)) {\r\n return value.map((item) => {\r\n if (typeof item === \"number\" || typeof item === \"string\") {\r\n return item;\r\n }\r\n return undefined;\r\n });\r\n }\r\n\r\n if (typeof value === \"string\" || typeof value === \"number\") {\r\n return [value];\r\n }\r\n\r\n return [];\r\n}\r\n\r\ninterface DataTableDateFilterProps {\r\n column: Column;\r\n title?: string;\r\n multiple?: boolean;\r\n}\r\n\r\nexport function DataTableDateFilter({\r\n column,\r\n title,\r\n multiple,\r\n}: DataTableDateFilterProps) {\r\n const columnFilterValue = column.getFilterValue();\r\n\r\n const selectedDates = React.useMemo(() => {\r\n if (!columnFilterValue) {\r\n return multiple ? { from: undefined, to: undefined } : [];\r\n }\r\n\r\n if (multiple) {\r\n const timestamps = parseColumnFilterValue(columnFilterValue);\r\n return {\r\n from: parseAsDate(timestamps[0]),\r\n to: parseAsDate(timestamps[1]),\r\n };\r\n }\r\n\r\n const timestamps = parseColumnFilterValue(columnFilterValue);\r\n const date = parseAsDate(timestamps[0]);\r\n return date ? [date] : [];\r\n }, [columnFilterValue, multiple]);\r\n\r\n const onSelect = React.useCallback(\r\n (date: Date | DateRange | undefined) => {\r\n if (!date) {\r\n column.setFilterValue(undefined);\r\n return;\r\n }\r\n\r\n if (multiple && !(\"getTime\" in date)) {\r\n const from = date.from?.getTime();\r\n const to = date.to?.getTime();\r\n column.setFilterValue(from || to ? [from, to] : undefined);\r\n } else if (!multiple && \"getTime\" in date) {\r\n column.setFilterValue(date.getTime());\r\n }\r\n },\r\n [column, multiple],\r\n );\r\n\r\n const onReset = React.useCallback(\r\n (event: React.MouseEvent) => {\r\n event.stopPropagation();\r\n column.setFilterValue(undefined);\r\n },\r\n [column],\r\n );\r\n\r\n const hasValue = React.useMemo(() => {\r\n if (multiple) {\r\n if (!getIsDateRange(selectedDates)) return false;\r\n return selectedDates.from || selectedDates.to;\r\n }\r\n if (!Array.isArray(selectedDates)) return false;\r\n return selectedDates.length > 0;\r\n }, [multiple, selectedDates]);\r\n\r\n const formatDateRange = React.useCallback((range: DateRange) => {\r\n if (!range.from && !range.to) return \"\";\r\n if (range.from && range.to) {\r\n return `${formatDate(range.from)} - ${formatDate(range.to)}`;\r\n }\r\n return formatDate(range.from ?? range.to);\r\n }, []);\r\n\r\n const label = React.useMemo(() => {\r\n if (multiple) {\r\n if (!getIsDateRange(selectedDates)) return null;\r\n\r\n const hasSelectedDates = selectedDates.from || selectedDates.to;\r\n const dateText = hasSelectedDates\r\n ? formatDateRange(selectedDates)\r\n : \"Select date range\";\r\n\r\n return (\r\n \r\n {title}\r\n {hasSelectedDates && (\r\n <>\r\n \r\n {dateText}\r\n \r\n )}\r\n \r\n );\r\n }\r\n\r\n if (getIsDateRange(selectedDates)) return null;\r\n\r\n const hasSelectedDate = selectedDates.length > 0;\r\n const dateText = hasSelectedDate\r\n ? formatDate(selectedDates[0])\r\n : \"Select date\";\r\n\r\n return (\r\n \r\n {title}\r\n {hasSelectedDate && (\r\n <>\r\n \r\n {dateText}\r\n \r\n )}\r\n \r\n );\r\n }, [selectedDates, multiple, formatDateRange, title]);\r\n\r\n return (\r\n \r\n \r\n \r\n {hasValue ? (\r\n \r\n \r\n \r\n ) : (\r\n \r\n )}\r\n {label}\r\n \r\n \r\n \r\n {multiple ? (\r\n \r\n ) : (\r\n \r\n )}\r\n \r\n \r\n );\r\n}\r\n", + "content": "\"use client\";\n\nimport type { Column } from \"@tanstack/react-table\";\nimport { CalendarIcon, XCircle } from \"lucide-react\";\nimport * as React from \"react\";\nimport type { DateRange } from \"react-day-picker\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { formatDate } from \"@/lib/format\";\n\ntype DateSelection = Date[] | DateRange;\n\nfunction getIsDateRange(value: DateSelection): value is DateRange {\n return value && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction parseAsDate(timestamp: number | string | undefined): Date | undefined {\n if (!timestamp) return undefined;\n const numericTimestamp =\n typeof timestamp === \"string\" ? Number(timestamp) : timestamp;\n const date = new Date(numericTimestamp);\n return !Number.isNaN(date.getTime()) ? date : undefined;\n}\n\nfunction parseColumnFilterValue(value: unknown) {\n if (value === null || value === undefined) {\n return [];\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => {\n if (typeof item === \"number\" || typeof item === \"string\") {\n return item;\n }\n return undefined;\n });\n }\n\n if (typeof value === \"string\" || typeof value === \"number\") {\n return [value];\n }\n\n return [];\n}\n\ninterface DataTableDateFilterProps {\n column: Column;\n title?: string;\n multiple?: boolean;\n}\n\nexport function DataTableDateFilter({\n column,\n title,\n multiple,\n}: DataTableDateFilterProps) {\n const columnFilterValue = column.getFilterValue();\n\n const selectedDates = React.useMemo(() => {\n if (!columnFilterValue) {\n return multiple ? { from: undefined, to: undefined } : [];\n }\n\n if (multiple) {\n const timestamps = parseColumnFilterValue(columnFilterValue);\n return {\n from: parseAsDate(timestamps[0]),\n to: parseAsDate(timestamps[1]),\n };\n }\n\n const timestamps = parseColumnFilterValue(columnFilterValue);\n const date = parseAsDate(timestamps[0]);\n return date ? [date] : [];\n }, [columnFilterValue, multiple]);\n\n const onSelect = React.useCallback(\n (date: Date | DateRange | undefined) => {\n if (!date) {\n column.setFilterValue(undefined);\n return;\n }\n\n if (multiple && !(\"getTime\" in date)) {\n const from = date.from?.getTime();\n const to = date.to?.getTime();\n column.setFilterValue(from || to ? [from, to] : undefined);\n } else if (!multiple && \"getTime\" in date) {\n column.setFilterValue(date.getTime());\n }\n },\n [column, multiple],\n );\n\n const onReset = React.useCallback(\n (event: React.MouseEvent) => {\n event.stopPropagation();\n column.setFilterValue(undefined);\n },\n [column],\n );\n\n const hasValue = React.useMemo(() => {\n if (multiple) {\n if (!getIsDateRange(selectedDates)) return false;\n return selectedDates.from || selectedDates.to;\n }\n if (!Array.isArray(selectedDates)) return false;\n return selectedDates.length > 0;\n }, [multiple, selectedDates]);\n\n const formatDateRange = React.useCallback((range: DateRange) => {\n if (!range.from && !range.to) return \"\";\n if (range.from && range.to) {\n return `${formatDate(range.from)} - ${formatDate(range.to)}`;\n }\n return formatDate(range.from ?? range.to);\n }, []);\n\n const label = React.useMemo(() => {\n if (multiple) {\n if (!getIsDateRange(selectedDates)) return null;\n\n const hasSelectedDates = selectedDates.from || selectedDates.to;\n const dateText = hasSelectedDates\n ? formatDateRange(selectedDates)\n : \"Select date range\";\n\n return (\n \n {title}\n {hasSelectedDates && (\n <>\n \n {dateText}\n \n )}\n \n );\n }\n\n if (getIsDateRange(selectedDates)) return null;\n\n const hasSelectedDate = selectedDates.length > 0;\n const dateText = hasSelectedDate\n ? formatDate(selectedDates[0])\n : \"Select date\";\n\n return (\n \n {title}\n {hasSelectedDate && (\n <>\n \n {dateText}\n \n )}\n \n );\n }, [selectedDates, multiple, formatDateRange, title]);\n\n return (\n \n \n \n {hasValue ? (\n \n \n \n ) : (\n \n )}\n {label}\n \n \n \n {multiple ? (\n \n ) : (\n \n )}\n \n \n );\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-date-filter.tsx" }, { "path": "src/components/data-table/data-table-skeleton.tsx", - "content": "import { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableSkeletonProps extends React.ComponentProps<\"div\"> {\n columnCount: number;\n rowCount?: number;\n filterCount?: number;\n cellWidths?: string[];\n withViewOptions?: boolean;\n withPagination?: boolean;\n shrinkZero?: boolean;\n}\n\nexport function DataTableSkeleton({\n columnCount,\n rowCount = 10,\n filterCount = 0,\n cellWidths = [\"auto\"],\n withViewOptions = true,\n withPagination = true,\n shrinkZero = false,\n className,\n ...props\n}: DataTableSkeletonProps) {\n const cozyCellWidths = Array.from(\n { length: columnCount },\n (_, index) => cellWidths[index % cellWidths.length] ?? \"auto\",\n );\n\n return (\n \n
\n
\n {filterCount > 0\n ? Array.from({ length: filterCount }).map((_, i) => (\n \n ))\n : null}\n
\n {withViewOptions ? (\n
\n
\n \n \n {Array.from({ length: 1 }).map((_, i) => (\n \n {Array.from({ length: columnCount }).map((_, j) => (\n \n \n \n ))}\n \n ))}\n \n \n {Array.from({ length: rowCount }).map((_, i) => (\n \n {Array.from({ length: columnCount }).map((_, j) => (\n \n \n \n ))}\n \n ))}\n \n
\n
\n {withPagination ? (\n
\n \n
\n
\n \n \n
\n
\n \n
\n
\n \n \n \n \n
\n
\n
\n ) : null}\n \n );\n}\n", + "content": "import { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableSkeletonProps extends React.ComponentProps<\"div\"> {\n columnCount: number;\n rowCount?: number;\n filterCount?: number;\n cellWidths?: string[];\n withViewOptions?: boolean;\n withPagination?: boolean;\n shrinkZero?: boolean;\n}\n\nexport function DataTableSkeleton({\n columnCount,\n rowCount = 10,\n filterCount = 0,\n cellWidths = [\"auto\"],\n withViewOptions = true,\n withPagination = true,\n shrinkZero = false,\n className,\n ...props\n}: DataTableSkeletonProps) {\n const cozyCellWidths = Array.from(\n { length: columnCount },\n (_, index) => cellWidths[index % cellWidths.length] ?? \"auto\",\n );\n\n return (\n \n
\n
\n {filterCount > 0\n ? Array.from({ length: filterCount }).map((_, i) => (\n \n ))\n : null}\n
\n {withViewOptions ? (\n
\n
\n \n \n {Array.from({ length: 1 }).map((_, i) => (\n \n {Array.from({ length: columnCount }).map((_, j) => (\n \n \n \n ))}\n \n ))}\n \n \n {Array.from({ length: rowCount }).map((_, i) => (\n \n {Array.from({ length: columnCount }).map((_, j) => (\n \n \n \n ))}\n \n ))}\n \n
\n
\n {withPagination ? (\n
\n \n
\n
\n \n \n
\n
\n \n
\n
\n \n \n \n \n
\n
\n
\n ) : null}\n \n );\n}\n", "type": "registry:component", "target": "src/components/data-table/data-table-skeleton.tsx" }, @@ -84,7 +84,7 @@ }, { "path": "src/hooks/use-data-table.ts", - "content": "\"use client\";\r\n\r\nimport {\r\n type ColumnFiltersState,\r\n getCoreRowModel,\r\n getFacetedMinMaxValues,\r\n getFacetedRowModel,\r\n getFacetedUniqueValues,\r\n getFilteredRowModel,\r\n getPaginationRowModel,\r\n getSortedRowModel,\r\n type PaginationState,\r\n type RowSelectionState,\r\n type SortingState,\r\n type TableOptions,\r\n type TableState,\r\n type Updater,\r\n useReactTable,\r\n type VisibilityState,\r\n} from \"@tanstack/react-table\";\r\nimport {\r\n parseAsArrayOf,\r\n parseAsInteger,\r\n parseAsString,\r\n type SingleParser,\r\n type UseQueryStateOptions,\r\n useQueryState,\r\n useQueryStates,\r\n} from \"nuqs\";\r\nimport * as React from \"react\";\r\n\r\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\r\nimport { getSortingStateParser } from \"@/lib/parsers\";\r\nimport type { ExtendedColumnSort, QueryKeys } from \"@/types/data-table\";\r\n\r\nconst PAGE_KEY = \"page\";\r\nconst PER_PAGE_KEY = \"perPage\";\r\nconst SORT_KEY = \"sort\";\r\nconst FILTERS_KEY = \"filters\";\r\nconst JOIN_OPERATOR_KEY = \"joinOperator\";\r\nconst ARRAY_SEPARATOR = \",\";\r\nconst DEBOUNCE_MS = 300;\r\nconst THROTTLE_MS = 50;\r\n\r\ninterface UseDataTableProps\r\n extends Omit<\r\n TableOptions,\r\n | \"state\"\r\n | \"pageCount\"\r\n | \"getCoreRowModel\"\r\n | \"manualFiltering\"\r\n | \"manualPagination\"\r\n | \"manualSorting\"\r\n >,\r\n Required, \"pageCount\">> {\r\n initialState?: Omit, \"sorting\"> & {\r\n sorting?: ExtendedColumnSort[];\r\n };\r\n queryKeys?: Partial;\r\n history?: \"push\" | \"replace\";\r\n debounceMs?: number;\r\n throttleMs?: number;\r\n clearOnDefault?: boolean;\r\n enableAdvancedFilter?: boolean;\r\n scroll?: boolean;\r\n shallow?: boolean;\r\n startTransition?: React.TransitionStartFunction;\r\n}\r\n\r\nexport function useDataTable(props: UseDataTableProps) {\r\n const {\r\n columns,\r\n pageCount = -1,\r\n initialState,\r\n queryKeys,\r\n history = \"replace\",\r\n debounceMs = DEBOUNCE_MS,\r\n throttleMs = THROTTLE_MS,\r\n clearOnDefault = false,\r\n enableAdvancedFilter = false,\r\n scroll = false,\r\n shallow = true,\r\n startTransition,\r\n ...tableProps\r\n } = props;\r\n const pageKey = queryKeys?.page ?? PAGE_KEY;\r\n const perPageKey = queryKeys?.perPage ?? PER_PAGE_KEY;\r\n const sortKey = queryKeys?.sort ?? SORT_KEY;\r\n const filtersKey = queryKeys?.filters ?? FILTERS_KEY;\r\n const joinOperatorKey = queryKeys?.joinOperator ?? JOIN_OPERATOR_KEY;\r\n\r\n const queryStateOptions = React.useMemo<\r\n Omit, \"parse\">\r\n >(\r\n () => ({\r\n history,\r\n scroll,\r\n shallow,\r\n throttleMs,\r\n debounceMs,\r\n clearOnDefault,\r\n startTransition,\r\n }),\r\n [\r\n history,\r\n scroll,\r\n shallow,\r\n throttleMs,\r\n debounceMs,\r\n clearOnDefault,\r\n startTransition,\r\n ],\r\n );\r\n\r\n const [rowSelection, setRowSelection] = React.useState(\r\n initialState?.rowSelection ?? {},\r\n );\r\n const [columnVisibility, setColumnVisibility] =\r\n React.useState(initialState?.columnVisibility ?? {});\r\n\r\n const [page, setPage] = useQueryState(\r\n pageKey,\r\n parseAsInteger.withOptions(queryStateOptions).withDefault(1),\r\n );\r\n const [perPage, setPerPage] = useQueryState(\r\n perPageKey,\r\n parseAsInteger\r\n .withOptions(queryStateOptions)\r\n .withDefault(initialState?.pagination?.pageSize ?? 10),\r\n );\r\n\r\n const pagination: PaginationState = React.useMemo(() => {\r\n return {\r\n pageIndex: page - 1, // zero-based index -> one-based index\r\n pageSize: perPage,\r\n };\r\n }, [page, perPage]);\r\n\r\n const onPaginationChange = React.useCallback(\r\n (updaterOrValue: Updater) => {\r\n if (typeof updaterOrValue === \"function\") {\r\n const newPagination = updaterOrValue(pagination);\r\n void setPage(newPagination.pageIndex + 1);\r\n void setPerPage(newPagination.pageSize);\r\n } else {\r\n void setPage(updaterOrValue.pageIndex + 1);\r\n void setPerPage(updaterOrValue.pageSize);\r\n }\r\n },\r\n [pagination, setPage, setPerPage],\r\n );\r\n\r\n const columnIds = React.useMemo(() => {\r\n return new Set(\r\n columns.map((column) => column.id).filter(Boolean) as string[],\r\n );\r\n }, [columns]);\r\n\r\n const [sorting, setSorting] = useQueryState(\r\n sortKey,\r\n getSortingStateParser(columnIds)\r\n .withOptions(queryStateOptions)\r\n .withDefault(initialState?.sorting ?? []),\r\n );\r\n\r\n const onSortingChange = React.useCallback(\r\n (updaterOrValue: Updater) => {\r\n if (typeof updaterOrValue === \"function\") {\r\n const newSorting = updaterOrValue(sorting);\r\n setSorting(newSorting as ExtendedColumnSort[]);\r\n } else {\r\n setSorting(updaterOrValue as ExtendedColumnSort[]);\r\n }\r\n },\r\n [sorting, setSorting],\r\n );\r\n\r\n const filterableColumns = React.useMemo(() => {\r\n if (enableAdvancedFilter) return [];\r\n\r\n return columns.filter((column) => column.enableColumnFilter);\r\n }, [columns, enableAdvancedFilter]);\r\n\r\n const filterParsers = React.useMemo(() => {\r\n if (enableAdvancedFilter) return {};\r\n\r\n return filterableColumns.reduce<\r\n Record | SingleParser>\r\n >((acc, column) => {\r\n if (column.meta?.options) {\r\n acc[column.id ?? \"\"] = parseAsArrayOf(\r\n parseAsString,\r\n ARRAY_SEPARATOR,\r\n ).withOptions(queryStateOptions);\r\n } else {\r\n acc[column.id ?? \"\"] = parseAsString.withOptions(queryStateOptions);\r\n }\r\n return acc;\r\n }, {});\r\n }, [filterableColumns, queryStateOptions, enableAdvancedFilter]);\r\n\r\n const [filterValues, setFilterValues] = useQueryStates(filterParsers);\r\n\r\n const debouncedSetFilterValues = useDebouncedCallback(\r\n (values: typeof filterValues) => {\r\n void setPage(1);\r\n void setFilterValues(values);\r\n },\r\n debounceMs,\r\n );\r\n\r\n const initialColumnFilters: ColumnFiltersState = React.useMemo(() => {\r\n if (enableAdvancedFilter) return [];\r\n\r\n return Object.entries(filterValues).reduce(\r\n (filters, [key, value]) => {\r\n if (value !== null) {\r\n const processedValue = Array.isArray(value)\r\n ? value\r\n : typeof value === \"string\" && /[^a-zA-Z0-9]/.test(value)\r\n ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean)\r\n : [value];\r\n\r\n filters.push({\r\n id: key,\r\n value: processedValue,\r\n });\r\n }\r\n return filters;\r\n },\r\n [],\r\n );\r\n }, [filterValues, enableAdvancedFilter]);\r\n\r\n const [columnFilters, setColumnFilters] =\r\n React.useState(initialColumnFilters);\r\n\r\n const onColumnFiltersChange = React.useCallback(\r\n (updaterOrValue: Updater) => {\r\n if (enableAdvancedFilter) return;\r\n\r\n setColumnFilters((prev) => {\r\n const next =\r\n typeof updaterOrValue === \"function\"\r\n ? updaterOrValue(prev)\r\n : updaterOrValue;\r\n\r\n const filterUpdates = next.reduce<\r\n Record\r\n >((acc, filter) => {\r\n if (filterableColumns.find((column) => column.id === filter.id)) {\r\n acc[filter.id] = filter.value as string | string[];\r\n }\r\n return acc;\r\n }, {});\r\n\r\n for (const prevFilter of prev) {\r\n if (!next.some((filter) => filter.id === prevFilter.id)) {\r\n filterUpdates[prevFilter.id] = null;\r\n }\r\n }\r\n\r\n debouncedSetFilterValues(filterUpdates);\r\n return next;\r\n });\r\n },\r\n [debouncedSetFilterValues, filterableColumns, enableAdvancedFilter],\r\n );\r\n\r\n const table = useReactTable({\r\n ...tableProps,\r\n columns,\r\n initialState,\r\n pageCount,\r\n state: {\r\n pagination,\r\n sorting,\r\n columnVisibility,\r\n rowSelection,\r\n columnFilters,\r\n },\r\n defaultColumn: {\r\n ...tableProps.defaultColumn,\r\n enableColumnFilter: false,\r\n },\r\n enableRowSelection: true,\r\n onRowSelectionChange: setRowSelection,\r\n onPaginationChange,\r\n onSortingChange,\r\n onColumnFiltersChange,\r\n onColumnVisibilityChange: setColumnVisibility,\r\n getCoreRowModel: getCoreRowModel(),\r\n getFilteredRowModel: getFilteredRowModel(),\r\n getPaginationRowModel: getPaginationRowModel(),\r\n getSortedRowModel: getSortedRowModel(),\r\n getFacetedRowModel: getFacetedRowModel(),\r\n getFacetedUniqueValues: getFacetedUniqueValues(),\r\n getFacetedMinMaxValues: getFacetedMinMaxValues(),\r\n manualPagination: true,\r\n manualSorting: true,\r\n manualFiltering: true,\r\n meta: {\r\n ...tableProps.meta,\r\n queryKeys: {\r\n page: pageKey,\r\n perPage: perPageKey,\r\n sort: sortKey,\r\n filters: filtersKey,\r\n joinOperator: joinOperatorKey,\r\n },\r\n },\r\n });\r\n\r\n return { table, shallow, debounceMs, throttleMs };\r\n}\r\n", + "content": "\"use client\";\n\nimport {\n type ColumnFiltersState,\n getCoreRowModel,\n getFacetedMinMaxValues,\n getFacetedRowModel,\n getFacetedUniqueValues,\n getFilteredRowModel,\n getPaginationRowModel,\n getSortedRowModel,\n type PaginationState,\n type RowSelectionState,\n type SortingState,\n type TableOptions,\n type TableState,\n type Updater,\n useReactTable,\n type VisibilityState,\n} from \"@tanstack/react-table\";\nimport {\n parseAsArrayOf,\n parseAsInteger,\n parseAsString,\n type SingleParser,\n type UseQueryStateOptions,\n useQueryState,\n useQueryStates,\n} from \"nuqs\";\nimport * as React from \"react\";\n\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getSortingStateParser } from \"@/lib/parsers\";\nimport type { ExtendedColumnSort, QueryKeys } from \"@/types/data-table\";\n\nconst PAGE_KEY = \"page\";\nconst PER_PAGE_KEY = \"perPage\";\nconst SORT_KEY = \"sort\";\nconst FILTERS_KEY = \"filters\";\nconst JOIN_OPERATOR_KEY = \"joinOperator\";\nconst ARRAY_SEPARATOR = \",\";\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\n\ninterface UseDataTableProps\n extends Omit<\n TableOptions,\n | \"state\"\n | \"pageCount\"\n | \"getCoreRowModel\"\n | \"manualFiltering\"\n | \"manualPagination\"\n | \"manualSorting\"\n >,\n Required, \"pageCount\">> {\n initialState?: Omit, \"sorting\"> & {\n sorting?: ExtendedColumnSort[];\n };\n queryKeys?: Partial;\n history?: \"push\" | \"replace\";\n debounceMs?: number;\n throttleMs?: number;\n clearOnDefault?: boolean;\n enableAdvancedFilter?: boolean;\n scroll?: boolean;\n shallow?: boolean;\n startTransition?: React.TransitionStartFunction;\n}\n\nexport function useDataTable(props: UseDataTableProps) {\n const {\n columns,\n pageCount = -1,\n initialState,\n queryKeys,\n history = \"replace\",\n debounceMs = DEBOUNCE_MS,\n throttleMs = THROTTLE_MS,\n clearOnDefault = false,\n enableAdvancedFilter = false,\n scroll = false,\n shallow = true,\n startTransition,\n ...tableProps\n } = props;\n const pageKey = queryKeys?.page ?? PAGE_KEY;\n const perPageKey = queryKeys?.perPage ?? PER_PAGE_KEY;\n const sortKey = queryKeys?.sort ?? SORT_KEY;\n const filtersKey = queryKeys?.filters ?? FILTERS_KEY;\n const joinOperatorKey = queryKeys?.joinOperator ?? JOIN_OPERATOR_KEY;\n\n const queryStateOptions = React.useMemo<\n Omit, \"parse\">\n >(\n () => ({\n history,\n scroll,\n shallow,\n throttleMs,\n debounceMs,\n clearOnDefault,\n startTransition,\n }),\n [\n history,\n scroll,\n shallow,\n throttleMs,\n debounceMs,\n clearOnDefault,\n startTransition,\n ],\n );\n\n const [rowSelection, setRowSelection] = React.useState(\n initialState?.rowSelection ?? {},\n );\n const [columnVisibility, setColumnVisibility] =\n React.useState(initialState?.columnVisibility ?? {});\n\n const [page, setPage] = useQueryState(\n pageKey,\n parseAsInteger.withOptions(queryStateOptions).withDefault(1),\n );\n const [perPage, setPerPage] = useQueryState(\n perPageKey,\n parseAsInteger\n .withOptions(queryStateOptions)\n .withDefault(initialState?.pagination?.pageSize ?? 10),\n );\n\n const pagination: PaginationState = React.useMemo(() => {\n return {\n pageIndex: page - 1, // zero-based index -> one-based index\n pageSize: perPage,\n };\n }, [page, perPage]);\n\n const onPaginationChange = React.useCallback(\n (updaterOrValue: Updater) => {\n if (typeof updaterOrValue === \"function\") {\n const newPagination = updaterOrValue(pagination);\n void setPage(newPagination.pageIndex + 1);\n void setPerPage(newPagination.pageSize);\n } else {\n void setPage(updaterOrValue.pageIndex + 1);\n void setPerPage(updaterOrValue.pageSize);\n }\n },\n [pagination, setPage, setPerPage],\n );\n\n const columnIds = React.useMemo(() => {\n return new Set(\n columns.map((column) => column.id).filter(Boolean) as string[],\n );\n }, [columns]);\n\n const [sorting, setSorting] = useQueryState(\n sortKey,\n getSortingStateParser(columnIds)\n .withOptions(queryStateOptions)\n .withDefault(initialState?.sorting ?? []),\n );\n\n const onSortingChange = React.useCallback(\n (updaterOrValue: Updater) => {\n if (typeof updaterOrValue === \"function\") {\n const newSorting = updaterOrValue(sorting);\n setSorting(newSorting as ExtendedColumnSort[]);\n } else {\n setSorting(updaterOrValue as ExtendedColumnSort[]);\n }\n },\n [sorting, setSorting],\n );\n\n const filterableColumns = React.useMemo(() => {\n if (enableAdvancedFilter) return [];\n\n return columns.filter((column) => column.enableColumnFilter);\n }, [columns, enableAdvancedFilter]);\n\n const filterParsers = React.useMemo(() => {\n if (enableAdvancedFilter) return {};\n\n return filterableColumns.reduce<\n Record | SingleParser>\n >((acc, column) => {\n if (column.meta?.options) {\n acc[column.id ?? \"\"] = parseAsArrayOf(\n parseAsString,\n ARRAY_SEPARATOR,\n ).withOptions(queryStateOptions);\n } else {\n acc[column.id ?? \"\"] = parseAsString.withOptions(queryStateOptions);\n }\n return acc;\n }, {});\n }, [filterableColumns, queryStateOptions, enableAdvancedFilter]);\n\n const [filterValues, setFilterValues] = useQueryStates(filterParsers);\n\n const debouncedSetFilterValues = useDebouncedCallback(\n (values: typeof filterValues) => {\n void setPage(1);\n void setFilterValues(values);\n },\n debounceMs,\n );\n\n const initialColumnFilters: ColumnFiltersState = React.useMemo(() => {\n if (enableAdvancedFilter) return [];\n\n return Object.entries(filterValues).reduce(\n (filters, [key, value]) => {\n if (value !== null) {\n const processedValue = Array.isArray(value)\n ? value\n : typeof value === \"string\" && /[^a-zA-Z0-9]/.test(value)\n ? value.split(/[^a-zA-Z0-9]+/).filter(Boolean)\n : [value];\n\n filters.push({\n id: key,\n value: processedValue,\n });\n }\n return filters;\n },\n [],\n );\n }, [filterValues, enableAdvancedFilter]);\n\n const [columnFilters, setColumnFilters] =\n React.useState(initialColumnFilters);\n\n const onColumnFiltersChange = React.useCallback(\n (updaterOrValue: Updater) => {\n if (enableAdvancedFilter) return;\n\n setColumnFilters((prev) => {\n const next =\n typeof updaterOrValue === \"function\"\n ? updaterOrValue(prev)\n : updaterOrValue;\n\n const filterUpdates = next.reduce<\n Record\n >((acc, filter) => {\n if (filterableColumns.find((column) => column.id === filter.id)) {\n acc[filter.id] = filter.value as string | string[];\n }\n return acc;\n }, {});\n\n for (const prevFilter of prev) {\n if (!next.some((filter) => filter.id === prevFilter.id)) {\n filterUpdates[prevFilter.id] = null;\n }\n }\n\n debouncedSetFilterValues(filterUpdates);\n return next;\n });\n },\n [debouncedSetFilterValues, filterableColumns, enableAdvancedFilter],\n );\n\n const table = useReactTable({\n ...tableProps,\n columns,\n initialState,\n pageCount,\n state: {\n pagination,\n sorting,\n columnVisibility,\n rowSelection,\n columnFilters,\n },\n defaultColumn: {\n ...tableProps.defaultColumn,\n enableColumnFilter: false,\n },\n enableRowSelection: true,\n onRowSelectionChange: setRowSelection,\n onPaginationChange,\n onSortingChange,\n onColumnFiltersChange,\n onColumnVisibilityChange: setColumnVisibility,\n getCoreRowModel: getCoreRowModel(),\n getFilteredRowModel: getFilteredRowModel(),\n getPaginationRowModel: getPaginationRowModel(),\n getSortedRowModel: getSortedRowModel(),\n getFacetedRowModel: getFacetedRowModel(),\n getFacetedUniqueValues: getFacetedUniqueValues(),\n getFacetedMinMaxValues: getFacetedMinMaxValues(),\n manualPagination: true,\n manualSorting: true,\n manualFiltering: true,\n meta: {\n ...tableProps.meta,\n queryKeys: {\n page: pageKey,\n perPage: perPageKey,\n sort: sortKey,\n filters: filtersKey,\n joinOperator: joinOperatorKey,\n },\n },\n });\n\n return { table, shallow, debounceMs, throttleMs };\n}\n", "type": "registry:hook" }, { @@ -94,7 +94,7 @@ }, { "path": "src/lib/data-table.ts", - "content": "import type { Column } from \"@tanstack/react-table\";\r\nimport { dataTableConfig } from \"@/config/data-table\";\r\nimport type {\r\n ExtendedColumnFilter,\r\n FilterOperator,\r\n FilterVariant,\r\n} from \"@/types/data-table\";\r\n\r\nexport function getCommonPinningStyles({\r\n column,\r\n withBorder = false,\r\n}: {\r\n column: Column;\r\n withBorder?: boolean;\r\n}): React.CSSProperties {\r\n const isPinned = column.getIsPinned();\r\n const isLastLeftPinnedColumn =\r\n isPinned === \"left\" && column.getIsLastColumn(\"left\");\r\n const isFirstRightPinnedColumn =\r\n isPinned === \"right\" && column.getIsFirstColumn(\"right\");\r\n\r\n return {\r\n boxShadow: withBorder\r\n ? isLastLeftPinnedColumn\r\n ? \"-4px 0 4px -4px var(--border) inset\"\r\n : isFirstRightPinnedColumn\r\n ? \"4px 0 4px -4px var(--border) inset\"\r\n : undefined\r\n : undefined,\r\n left: isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\r\n right: isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined,\r\n opacity: isPinned ? 0.97 : 1,\r\n position: isPinned ? \"sticky\" : \"relative\",\r\n background: isPinned ? \"var(--background)\" : \"var(--background)\",\r\n width: column.getSize(),\r\n zIndex: isPinned ? 1 : undefined,\r\n };\r\n}\r\n\r\nexport function getFilterOperators(filterVariant: FilterVariant) {\r\n const operatorMap: Record<\r\n FilterVariant,\r\n { label: string; value: FilterOperator }[]\r\n > = {\r\n text: dataTableConfig.textOperators,\r\n number: dataTableConfig.numericOperators,\r\n range: dataTableConfig.numericOperators,\r\n date: dataTableConfig.dateOperators,\r\n dateRange: dataTableConfig.dateOperators,\r\n boolean: dataTableConfig.booleanOperators,\r\n select: dataTableConfig.selectOperators,\r\n multiSelect: dataTableConfig.multiSelectOperators,\r\n };\r\n\r\n return operatorMap[filterVariant] ?? dataTableConfig.textOperators;\r\n}\r\n\r\nexport function getDefaultFilterOperator(filterVariant: FilterVariant) {\r\n const operators = getFilterOperators(filterVariant);\r\n\r\n return operators[0]?.value ?? (filterVariant === \"text\" ? \"iLike\" : \"eq\");\r\n}\r\n\r\nexport function getValidFilters(\r\n filters: ExtendedColumnFilter[],\r\n): ExtendedColumnFilter[] {\r\n return filters.filter(\r\n (filter) =>\r\n filter.operator === \"isEmpty\" ||\r\n filter.operator === \"isNotEmpty\" ||\r\n (Array.isArray(filter.value)\r\n ? filter.value.length > 0\r\n : filter.value !== \"\" &&\r\n filter.value !== null &&\r\n filter.value !== undefined),\r\n );\r\n}\r\n", + "content": "import type { Column } from \"@tanstack/react-table\";\nimport { dataTableConfig } from \"@/config/data-table\";\nimport type {\n ExtendedColumnFilter,\n FilterOperator,\n FilterVariant,\n} from \"@/types/data-table\";\n\nexport function getCommonPinningStyles({\n column,\n withBorder = false,\n}: {\n column: Column;\n withBorder?: boolean;\n}): React.CSSProperties {\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn =\n isPinned === \"left\" && column.getIsLastColumn(\"left\");\n const isFirstRightPinnedColumn =\n isPinned === \"right\" && column.getIsFirstColumn(\"right\");\n\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isPinned === \"left\" ? `${column.getStart(\"left\")}px` : undefined,\n right: isPinned === \"right\" ? `${column.getAfter(\"right\")}px` : undefined,\n opacity: isPinned ? 0.97 : 1,\n position: isPinned ? \"sticky\" : \"relative\",\n background: isPinned ? \"var(--background)\" : \"var(--background)\",\n width: column.getSize(),\n zIndex: isPinned ? 1 : undefined,\n };\n}\n\nexport function getFilterOperators(filterVariant: FilterVariant) {\n const operatorMap: Record<\n FilterVariant,\n { label: string; value: FilterOperator }[]\n > = {\n text: dataTableConfig.textOperators,\n number: dataTableConfig.numericOperators,\n range: dataTableConfig.numericOperators,\n date: dataTableConfig.dateOperators,\n dateRange: dataTableConfig.dateOperators,\n boolean: dataTableConfig.booleanOperators,\n select: dataTableConfig.selectOperators,\n multiSelect: dataTableConfig.multiSelectOperators,\n };\n\n return operatorMap[filterVariant] ?? dataTableConfig.textOperators;\n}\n\nexport function getDefaultFilterOperator(filterVariant: FilterVariant) {\n const operators = getFilterOperators(filterVariant);\n\n return operators[0]?.value ?? (filterVariant === \"text\" ? \"iLike\" : \"eq\");\n}\n\nexport function getValidFilters(\n filters: ExtendedColumnFilter[],\n): ExtendedColumnFilter[] {\n return filters.filter(\n (filter) =>\n filter.operator === \"isEmpty\" ||\n filter.operator === \"isNotEmpty\" ||\n (Array.isArray(filter.value)\n ? filter.value.length > 0\n : filter.value !== \"\" &&\n filter.value !== null &&\n filter.value !== undefined),\n );\n}\n", "type": "registry:lib" }, { @@ -115,7 +115,7 @@ }, { "path": "src/types/data-table.ts", - "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\r\nimport type { DataTableConfig } from \"@/config/data-table\";\r\nimport type { FilterItemSchema } from \"@/lib/parsers\";\r\n\r\ndeclare module \"@tanstack/react-table\" {\r\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\r\n interface TableMeta {\r\n queryKeys?: QueryKeys;\r\n }\r\n\r\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\r\n interface ColumnMeta {\r\n label?: string;\r\n placeholder?: string;\r\n variant?: FilterVariant;\r\n options?: Option[];\r\n range?: [number, number];\r\n unit?: string;\r\n icon?: React.FC>;\r\n }\r\n}\r\n\r\nexport interface QueryKeys {\r\n page: string;\r\n perPage: string;\r\n sort: string;\r\n filters: string;\r\n joinOperator: string;\r\n}\r\n\r\nexport interface Option {\r\n label: string;\r\n value: string;\r\n count?: number;\r\n icon?: React.FC>;\r\n}\r\n\r\nexport type FilterOperator = DataTableConfig[\"operators\"][number];\r\nexport type FilterVariant = DataTableConfig[\"filterVariants\"][number];\r\nexport type JoinOperator = DataTableConfig[\"joinOperators\"][number];\r\n\r\nexport interface ExtendedColumnSort extends Omit {\r\n id: Extract;\r\n}\r\n\r\nexport interface ExtendedColumnFilter extends FilterItemSchema {\r\n id: Extract;\r\n}\r\n\r\nexport interface DataTableRowAction {\r\n row: Row;\r\n variant: \"update\" | \"delete\";\r\n}\r\n", + "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\nimport type { DataTableConfig } from \"@/config/data-table\";\nimport type { FilterItemSchema } from \"@/lib/parsers\";\n\ndeclare module \"@tanstack/react-table\" {\n // biome-ignore lint/correctness/noUnusedVariables: TData is used in the TableMeta interface\n interface TableMeta {\n queryKeys?: QueryKeys;\n }\n\n // biome-ignore lint/correctness/noUnusedVariables: TData and TValue are used in the ColumnMeta interface\n interface ColumnMeta {\n label?: string;\n placeholder?: string;\n variant?: FilterVariant;\n options?: Option[];\n range?: [number, number];\n unit?: string;\n icon?: React.FC>;\n }\n}\n\nexport interface QueryKeys {\n page: string;\n perPage: string;\n sort: string;\n filters: string;\n joinOperator: string;\n}\n\nexport interface Option {\n label: string;\n value: string;\n count?: number;\n icon?: React.FC>;\n}\n\nexport type FilterOperator = DataTableConfig[\"operators\"][number];\nexport type FilterVariant = DataTableConfig[\"filterVariants\"][number];\nexport type JoinOperator = DataTableConfig[\"joinOperators\"][number];\n\nexport interface ExtendedColumnSort extends Omit {\n id: Extract;\n}\n\nexport interface ExtendedColumnFilter extends FilterItemSchema {\n id: Extract;\n}\n\nexport interface DataTableRowAction {\n row: Row;\n variant: \"update\" | \"delete\";\n}\n", "type": "registry:file", "target": "src/types/data-table.ts" } diff --git a/src/app/components/tasks-table-columns.tsx b/src/app/components/tasks-table-columns.tsx index 4ca2524ab..e227d8d72 100644 --- a/src/app/components/tasks-table-columns.tsx +++ b/src/app/components/tasks-table-columns.tsx @@ -100,7 +100,7 @@ export function getTasksTableColumns({ return (
{label && {label}} - + {row.getValue("title")}
diff --git a/src/app/data-grid/page.tsx b/src/app/data-grid/page.tsx index b96292eb8..9773e4d46 100644 --- a/src/app/data-grid/page.tsx +++ b/src/app/data-grid/page.tsx @@ -1,5 +1,16 @@ +import { Suspense } from "react"; import { DataGridDemo } from "@/components/data-grid/data-grid-demo"; export default async function DataGridPage() { - return ; + return ( + + Loading... + + } + > + + + ); } diff --git a/src/app/lib/actions.ts b/src/app/lib/actions.ts index d36d31f8e..92849a83b 100644 --- a/src/app/lib/actions.ts +++ b/src/app/lib/actions.ts @@ -2,7 +2,7 @@ import { asc, eq, inArray, not } from "drizzle-orm"; import { customAlphabet } from "nanoid"; -import { revalidateTag, unstable_noStore } from "next/cache"; +import { updateTag } from "next/cache"; import { db } from "@/db/index"; import { type Task, tasks } from "@/db/schema"; import { takeFirstOrThrow } from "@/db/utils"; @@ -33,7 +33,6 @@ export async function seedTasks(input: { count: number }) { } export async function createTask(input: CreateTaskSchema) { - unstable_noStore(); try { await db.transaction(async (tx) => { const newTask = await tx @@ -69,9 +68,9 @@ export async function createTask(input: CreateTaskSchema) { ); }); - revalidateTag("tasks"); - revalidateTag("task-status-counts"); - revalidateTag("task-priority-counts"); + updateTag("tasks"); + updateTag("task-status-counts"); + updateTag("task-priority-counts"); return { data: null, @@ -86,7 +85,6 @@ export async function createTask(input: CreateTaskSchema) { } export async function updateTask(input: UpdateTaskSchema & { id: string }) { - unstable_noStore(); try { const data = await db .update(tasks) @@ -103,12 +101,12 @@ export async function updateTask(input: UpdateTaskSchema & { id: string }) { }) .then(takeFirstOrThrow); - revalidateTag("tasks"); + updateTag("tasks"); if (data.status === input.status) { - revalidateTag("task-status-counts"); + updateTag("task-status-counts"); } if (data.priority === input.priority) { - revalidateTag("task-priority-counts"); + updateTag("task-priority-counts"); } return { @@ -129,7 +127,6 @@ export async function updateTasks(input: { status?: Task["status"]; priority?: Task["priority"]; }) { - unstable_noStore(); try { const data = await db .update(tasks) @@ -145,12 +142,12 @@ export async function updateTasks(input: { }) .then(takeFirstOrThrow); - revalidateTag("tasks"); + updateTag("tasks"); if (data.status === input.status) { - revalidateTag("task-status-counts"); + updateTag("task-status-counts"); } if (data.priority === input.priority) { - revalidateTag("task-priority-counts"); + updateTag("task-priority-counts"); } return { @@ -166,7 +163,6 @@ export async function updateTasks(input: { } export async function deleteTask(input: { id: string }) { - unstable_noStore(); try { await db.transaction(async (tx) => { await tx.delete(tasks).where(eq(tasks.id, input.id)); @@ -175,9 +171,9 @@ export async function deleteTask(input: { id: string }) { await tx.insert(tasks).values(generateRandomTask()); }); - revalidateTag("tasks"); - revalidateTag("task-status-counts"); - revalidateTag("task-priority-counts"); + updateTag("tasks"); + updateTag("task-status-counts"); + updateTag("task-priority-counts"); return { data: null, @@ -192,7 +188,6 @@ export async function deleteTask(input: { id: string }) { } export async function deleteTasks(input: { ids: string[] }) { - unstable_noStore(); try { await db.transaction(async (tx) => { await tx.delete(tasks).where(inArray(tasks.id, input.ids)); @@ -201,9 +196,9 @@ export async function deleteTasks(input: { ids: string[] }) { await tx.insert(tasks).values(input.ids.map(() => generateRandomTask())); }); - revalidateTag("tasks"); - revalidateTag("task-status-counts"); - revalidateTag("task-priority-counts"); + updateTag("tasks"); + updateTag("task-status-counts"); + updateTag("task-priority-counts"); return { data: null, diff --git a/src/app/lib/queries.ts b/src/app/lib/queries.ts index 5b0e920bd..34b9b3d6c 100644 --- a/src/app/lib/queries.ts +++ b/src/app/lib/queries.ts @@ -1,3 +1,5 @@ +"use cache"; + import "server-only"; import { @@ -12,220 +14,199 @@ import { lte, sql, } from "drizzle-orm"; +import { cacheLife, cacheTag } from "next/cache"; import { db } from "@/db"; import { tasks } from "@/db/schema"; import { filterColumns } from "@/lib/filter-columns"; -import { unstable_cache } from "@/lib/unstable-cache"; import type { GetTasksSchema } from "./validations"; export async function getTasks(input: GetTasksSchema) { - return await unstable_cache( - async () => { - try { - const offset = (input.page - 1) * input.perPage; - const advancedTable = - input.filterFlag === "advancedFilters" || - input.filterFlag === "commandFilters"; - - const advancedWhere = filterColumns({ - table: tasks, - filters: input.filters, - joinOperator: input.joinOperator, - }); - - const where = advancedTable - ? advancedWhere - : and( - input.title ? ilike(tasks.title, `%${input.title}%`) : undefined, - input.status.length > 0 - ? inArray(tasks.status, input.status) - : undefined, - input.priority.length > 0 - ? inArray(tasks.priority, input.priority) - : undefined, - input.estimatedHours.length > 0 - ? and( - input.estimatedHours[0] - ? gte(tasks.estimatedHours, input.estimatedHours[0]) - : undefined, - input.estimatedHours[1] - ? lte(tasks.estimatedHours, input.estimatedHours[1]) - : undefined, - ) - : undefined, - input.createdAt.length > 0 - ? and( - input.createdAt[0] - ? gte( - tasks.createdAt, - (() => { - const date = new Date(input.createdAt[0]); - date.setHours(0, 0, 0, 0); - return date; - })(), - ) - : undefined, - input.createdAt[1] - ? lte( - tasks.createdAt, - (() => { - const date = new Date(input.createdAt[1]); - date.setHours(23, 59, 59, 999); - return date; - })(), - ) - : undefined, - ) - : undefined, - ); - - const orderBy = - input.sort.length > 0 - ? input.sort.map((item) => - item.desc ? desc(tasks[item.id]) : asc(tasks[item.id]), + cacheLife({ revalidate: 1, stale: 1, expire: 60 }); + cacheTag("tasks"); + + try { + const offset = (input.page - 1) * input.perPage; + const advancedTable = + input.filterFlag === "advancedFilters" || + input.filterFlag === "commandFilters"; + + const advancedWhere = filterColumns({ + table: tasks, + filters: input.filters, + joinOperator: input.joinOperator, + }); + + const where = advancedTable + ? advancedWhere + : and( + input.title ? ilike(tasks.title, `%${input.title}%`) : undefined, + input.status.length > 0 + ? inArray(tasks.status, input.status) + : undefined, + input.priority.length > 0 + ? inArray(tasks.priority, input.priority) + : undefined, + input.estimatedHours.length > 0 + ? and( + input.estimatedHours[0] + ? gte(tasks.estimatedHours, input.estimatedHours[0]) + : undefined, + input.estimatedHours[1] + ? lte(tasks.estimatedHours, input.estimatedHours[1]) + : undefined, ) - : [asc(tasks.createdAt)]; - - const { data, total } = await db.transaction(async (tx) => { - const data = await tx - .select() - .from(tasks) - .limit(input.perPage) - .offset(offset) - .where(where) - .orderBy(...orderBy); - - const total = await tx - .select({ - count: count(), - }) - .from(tasks) - .where(where) - .execute() - .then((res) => res[0]?.count ?? 0); - - return { - data, - total, - }; - }); - - const pageCount = Math.ceil(total / input.perPage); - return { data, pageCount }; - } catch (_err) { - return { data: [], pageCount: 0 }; - } - }, - [JSON.stringify(input)], - { - revalidate: 1, - tags: ["tasks"], - }, - )(); + : undefined, + input.createdAt.length > 0 + ? and( + input.createdAt[0] + ? gte( + tasks.createdAt, + (() => { + const date = new Date(input.createdAt[0]); + date.setHours(0, 0, 0, 0); + return date; + })(), + ) + : undefined, + input.createdAt[1] + ? lte( + tasks.createdAt, + (() => { + const date = new Date(input.createdAt[1]); + date.setHours(23, 59, 59, 999); + return date; + })(), + ) + : undefined, + ) + : undefined, + ); + + const orderBy = + input.sort.length > 0 + ? input.sort.map((item) => + item.desc ? desc(tasks[item.id]) : asc(tasks[item.id]), + ) + : [asc(tasks.createdAt)]; + + const { data, total } = await db.transaction(async (tx) => { + const data = await tx + .select() + .from(tasks) + .limit(input.perPage) + .offset(offset) + .where(where) + .orderBy(...orderBy); + + const total = await tx + .select({ + count: count(), + }) + .from(tasks) + .where(where) + .execute() + .then((res) => res[0]?.count ?? 0); + + return { + data, + total, + }; + }); + + const pageCount = Math.ceil(total / input.perPage); + return { data, pageCount }; + } catch { + return { data: [], pageCount: 0 }; + } } export async function getTaskStatusCounts() { - return unstable_cache( - async () => { - try { - return await db - .select({ - status: tasks.status, - count: count(), - }) - .from(tasks) - .groupBy(tasks.status) - .having(gt(count(tasks.status), 0)) - .then((res) => - res.reduce( - (acc, { status, count }) => { - acc[status] = count; - return acc; - }, - { - todo: 0, - "in-progress": 0, - done: 0, - canceled: 0, - }, - ), - ); - } catch (_err) { - return { - todo: 0, - "in-progress": 0, - done: 0, - canceled: 0, - }; - } - }, - ["task-status-counts"], - { - revalidate: 3600, - }, - )(); + cacheLife("hours"); + cacheTag("task-status-counts"); + + try { + return await db + .select({ + status: tasks.status, + count: count(), + }) + .from(tasks) + .groupBy(tasks.status) + .having(gt(count(tasks.status), 0)) + .then((res) => + res.reduce( + (acc, { status, count }) => { + acc[status] = count; + return acc; + }, + { + todo: 0, + "in-progress": 0, + done: 0, + canceled: 0, + }, + ), + ); + } catch { + return { + todo: 0, + "in-progress": 0, + done: 0, + canceled: 0, + }; + } } export async function getTaskPriorityCounts() { - return unstable_cache( - async () => { - try { - return await db - .select({ - priority: tasks.priority, - count: count(), - }) - .from(tasks) - .groupBy(tasks.priority) - .having(gt(count(), 0)) - .then((res) => - res.reduce( - (acc, { priority, count }) => { - acc[priority] = count; - return acc; - }, - { - low: 0, - medium: 0, - high: 0, - }, - ), - ); - } catch (_err) { - return { - low: 0, - medium: 0, - high: 0, - }; - } - }, - ["task-priority-counts"], - { - revalidate: 3600, - }, - )(); + cacheLife("hours"); + cacheTag("task-priority-counts"); + + try { + return await db + .select({ + priority: tasks.priority, + count: count(), + }) + .from(tasks) + .groupBy(tasks.priority) + .having(gt(count(), 0)) + .then((res) => + res.reduce( + (acc, { priority, count }) => { + acc[priority] = count; + return acc; + }, + { + low: 0, + medium: 0, + high: 0, + }, + ), + ); + } catch { + return { + low: 0, + medium: 0, + high: 0, + }; + } } export async function getEstimatedHoursRange() { - return unstable_cache( - async () => { - try { - return await db - .select({ - min: sql`min(${tasks.estimatedHours})`, - max: sql`max(${tasks.estimatedHours})`, - }) - .from(tasks) - .then((res) => res[0] ?? { min: 0, max: 0 }); - } catch (_err) { - return { min: 0, max: 0 }; - } - }, - ["estimated-hours-range"], - { - revalidate: 3600, - }, - )(); + cacheLife("hours"); + cacheTag("estimated-hours-range"); + + try { + return await db + .select({ + min: sql`min(${tasks.estimatedHours})`, + max: sql`max(${tasks.estimatedHours})`, + }) + .from(tasks) + .then((res) => res[0] ?? { min: 0, max: 0 }); + } catch { + return { min: 0, max: 0 }; + } } diff --git a/src/app/page.tsx b/src/app/page.tsx index b58fc9ae7..26b6c93a3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,4 @@ -import * as React from "react"; +import { Suspense } from "react"; import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; import { Shell } from "@/components/shell"; import { getValidFilters } from "@/lib/data-table"; @@ -17,7 +17,36 @@ interface IndexPageProps { searchParams: Promise; } -export default async function IndexPage(props: IndexPageProps) { +export default function IndexPage(props: IndexPageProps) { + return ( + + + } + > + + + + + + ); +} + +async function TasksTableWrapper(props: IndexPageProps) { const searchParams = await props.searchParams; const search = searchParamsCache.parse(searchParams); @@ -33,30 +62,5 @@ export default async function IndexPage(props: IndexPageProps) { getEstimatedHoursRange(), ]); - return ( - - - - } - > - - - - - ); + return ; } diff --git a/src/components/data-grid/data-grid-cell-wrapper.tsx b/src/components/data-grid/data-grid-cell-wrapper.tsx index 23bf56835..87ac6b3a8 100644 --- a/src/components/data-grid/data-grid-cell-wrapper.tsx +++ b/src/components/data-grid/data-grid-cell-wrapper.tsx @@ -146,7 +146,7 @@ export function DataGridCellWrapper({ data-selected={isSelected ? "" : undefined} tabIndex={isFocused && !isEditing ? 0 : -1} className={cn( - "size-full px-2 py-1.5 text-left text-sm outline-none has-[[data-slot=checkbox]]:pt-2.5", + "size-full px-2 py-1.5 text-left text-sm outline-none has-data-[slot=checkbox]:pt-2.5", { "ring-1 ring-ring ring-inset": isFocused, "bg-yellow-100 dark:bg-yellow-900/30": @@ -154,13 +154,13 @@ export function DataGridCellWrapper({ "bg-orange-200 dark:bg-orange-900/50": isActiveSearchMatch, "bg-primary/10": isSelected && !isEditing, "cursor-default": !isEditing, - "[&_[data-slot=grid-cell-content]]:line-clamp-1": + "**:data-[slot=grid-cell-content]:line-clamp-1": !isEditing && rowHeight === "short", - "[&_[data-slot=grid-cell-content]]:line-clamp-2": + "**:data-[slot=grid-cell-content]:line-clamp-2": !isEditing && rowHeight === "medium", - "[&_[data-slot=grid-cell-content]]:line-clamp-3": + "**:data-[slot=grid-cell-content]:line-clamp-3": !isEditing && rowHeight === "tall", - "[&_[data-slot=grid-cell-content]]:line-clamp-4": + "**:data-[slot=grid-cell-content]:line-clamp-4": !isEditing && rowHeight === "extra-tall", }, className, diff --git a/src/components/data-grid/data-grid-column-header.tsx b/src/components/data-grid/data-grid-column-header.tsx index b98d6d1dc..22b45930c 100644 --- a/src/components/data-grid/data-grid-column-header.tsx +++ b/src/components/data-grid/data-grid-column-header.tsx @@ -309,7 +309,7 @@ function DataGridColumnResizerImpl({ aria-valuemax={defaultColumnDef.maxSize} tabIndex={0} className={cn( - "after:-translate-x-1/2 absolute top-0 right-[-1px] z-50 h-full w-0.5 cursor-ew-resize touch-none select-none bg-border transition-opacity after:absolute after:inset-y-0 after:left-1/2 after:h-full after:w-[18px] after:content-[''] hover:bg-primary focus:bg-primary focus:outline-none", + "after:-translate-x-1/2 -right-px absolute top-0 z-50 h-full w-0.5 cursor-ew-resize touch-none select-none bg-border transition-opacity after:absolute after:inset-y-0 after:left-1/2 after:h-full after:w-[18px] after:content-[''] hover:bg-primary focus:bg-primary focus:outline-none", header.column.getIsResizing() ? "bg-primary" : "opacity-0 hover:opacity-100", diff --git a/src/components/data-grid/data-grid-demo.tsx b/src/components/data-grid/data-grid-demo.tsx index 835ae5263..2d0b16d1d 100644 --- a/src/components/data-grid/data-grid-demo.tsx +++ b/src/components/data-grid/data-grid-demo.tsx @@ -315,7 +315,7 @@ export function DataGridDemo() { const height = Math.max(400, windowSize.height - 150); return ( -
+
diff --git a/src/components/data-grid/data-grid-keyboard-shortcuts.tsx b/src/components/data-grid/data-grid-keyboard-shortcuts.tsx index 7bd445597..2b8c9bfd3 100644 --- a/src/components/data-grid/data-grid-keyboard-shortcuts.tsx +++ b/src/components/data-grid/data-grid-keyboard-shortcuts.tsx @@ -274,7 +274,7 @@ function DataGridKeyboardShortcutsImpl({ />
- +
{filteredGroups.length === 0 ? (
diff --git a/src/components/data-grid/data-grid-render-count.tsx b/src/components/data-grid/data-grid-render-count.tsx index 2c3c51277..a53167155 100644 --- a/src/components/data-grid/data-grid-render-count.tsx +++ b/src/components/data-grid/data-grid-render-count.tsx @@ -26,7 +26,7 @@ export function DataGridRenderCount({ return (
({
@@ -331,7 +331,7 @@ function DataTableSortItem({ @@ -362,13 +362,13 @@ function DataTableSortItem({ > {SORT_ORDERS.map((order) => ( diff --git a/src/components/data-table/data-table-faceted-filter.tsx b/src/components/data-table/data-table-faceted-filter.tsx index 924226440..77554c8fb 100644 --- a/src/components/data-table/data-table-faceted-filter.tsx +++ b/src/components/data-table/data-table-faceted-filter.tsx @@ -133,7 +133,7 @@ export function DataTableFacetedFilter({ )} - + diff --git a/src/components/data-table/data-table-filter-list.tsx b/src/components/data-table/data-table-filter-list.tsx index 21b941211..0b1519382 100644 --- a/src/components/data-table/data-table-filter-list.tsx +++ b/src/components/data-table/data-table-filter-list.tsx @@ -242,7 +242,7 @@ export function DataTableFilterList({
@@ -402,7 +402,7 @@ function DataTableFilterItem({ @@ -442,7 +442,7 @@ function DataTableFilterItem({ @@ -498,16 +498,13 @@ function DataTableFilterItem({ >
- + {filterOperators.map((operator) => ( ({ id={inputId} aria-controls={inputListboxId} aria-label={`${columnMeta?.label} boolean filter`} - className="h-8 w-full rounded [&[data-size]]:h-8" + className="h-8 w-full rounded data-size:h-8" > @@ -700,10 +697,7 @@ function onFilterInputRender({ /> - + ({ {filter.operator === "isBetween" ? ( ({ @@ -403,10 +403,7 @@ function DataTableFilterItem({ {columnMeta?.label ?? column.id} - + @@ -464,14 +461,11 @@ function DataTableFilterItem({ > - + {filterOperators.map((operator) => ( ({ column={column} inputId={inputId} onFilterUpdate={onFilterUpdate} - className="size-full max-w-28 gap-0 [&_[data-slot='range-min']]:border-r-0 [&_input]:rounded-none [&_input]:px-1.5" + className="size-full max-w-28 gap-0 **:data-[slot='range-min']:border-r-0 [&_input]:rounded-none [&_input]:px-1.5" /> ); } @@ -744,7 +738,7 @@ function onFilterInputRender({ @@ -824,7 +818,7 @@ function onFilterInputRender({ {filter.operator === "isBetween" ? ( ({ table.setPageSize(Number(value)); }} > - + diff --git a/src/components/data-table/data-table-skeleton.tsx b/src/components/data-table/data-table-skeleton.tsx index ae7d8b329..df678e274 100644 --- a/src/components/data-table/data-table-skeleton.tsx +++ b/src/components/data-table/data-table-skeleton.tsx @@ -44,12 +44,12 @@ export function DataTableSkeleton({
{filterCount > 0 ? Array.from({ length: filterCount }).map((_, i) => ( - + )) : null}
{withViewOptions ? ( - + ) : null}
@@ -96,7 +96,7 @@ export function DataTableSkeleton({
- +
diff --git a/src/components/data-table/data-table-sort-list.tsx b/src/components/data-table/data-table-sort-list.tsx index 6adb5799c..393ef7f97 100644 --- a/src/components/data-table/data-table-sort-list.tsx +++ b/src/components/data-table/data-table-sort-list.tsx @@ -187,7 +187,7 @@ export function DataTableSortList({
@@ -327,7 +327,7 @@ function DataTableSortItem({ @@ -358,13 +358,13 @@ function DataTableSortItem({ > {dataTableConfig.sortOrders.map((order) => ( diff --git a/src/components/shell.tsx b/src/components/shell.tsx index ed136a00c..86c271f8b 100644 --- a/src/components/shell.tsx +++ b/src/components/shell.tsx @@ -3,19 +3,23 @@ import type * as React from "react"; import { cn } from "@/lib/utils"; -const shellVariants = cva("grid items-center gap-8 pt-6 pb-8 md:py-8", { - variants: { - variant: { - default: "container", - sidebar: "", - centered: "container flex h-dvh max-w-2xl flex-col justify-center py-16", - markdown: "container max-w-3xl py-8 md:py-10 lg:py-10", +const shellVariants = cva( + "grid grid-cols-1 items-center gap-8 pt-6 pb-8 md:py-8", + { + variants: { + variant: { + default: "container", + sidebar: "", + centered: + "container flex h-dvh max-w-2xl flex-col justify-center py-16", + markdown: "container max-w-3xl py-8 md:py-10 lg:py-10", + }, + }, + defaultVariants: { + variant: "default", }, }, - defaultVariants: { - variant: "default", - }, -}); +); interface ShellProps extends React.HTMLAttributes, diff --git a/src/hooks/use-data-grid.tsx b/src/hooks/use-data-grid.tsx index 28a868f11..d06971b71 100644 --- a/src/hooks/use-data-grid.tsx +++ b/src/hooks/use-data-grid.tsx @@ -30,7 +30,7 @@ const VIEWPORT_OFFSET = 1; const MIN_COLUMN_SIZE = 60; const MAX_COLUMN_SIZE = 800; const SEARCH_SHORTCUT_KEY = "f"; -const NON_NAVIGABLE_COLUMN_IDS = ["select", "actions"] as const; +const NON_NAVIGABLE_COLUMN_IDS = ["select", "actions"]; const useIsomorphicLayoutEffect = typeof window !== "undefined" ? React.useLayoutEffect : React.useEffect; diff --git a/src/lib/unstable-cache.ts b/src/lib/unstable-cache.ts deleted file mode 100644 index 499864258..000000000 --- a/src/lib/unstable-cache.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @see https://github.com/ethanniser/NextMaster/blob/main/src/lib/unstable-cache.ts - */ - -import { unstable_cache as next_unstable_cache } from "next/cache"; -import { cache } from "react"; - -// next_unstable_cache doesn't handle deduplication, so we wrap it in React's cache -export const unstable_cache = ( - cb: (...args: Inputs) => Promise, - keyParts: string[], - options?: { - /** - * The revalidation interval in seconds. - */ - revalidate?: number | false; - tags?: string[]; - }, -) => cache(next_unstable_cache(cb, keyParts, options)); diff --git a/src/styles/globals.css b/src/styles/globals.css index 007e9958b..4147e8299 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -153,8 +153,8 @@ max-width: none; } - @media (width >= 1400px) { - max-width: 1400px; + @media (width >= 1600px) { + max-width: 1600px; } @media (max-width: 640px) { diff --git a/tsconfig.json b/tsconfig.json index ff790a01d..7b409bead 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,21 +8,22 @@ "resolveJsonModule": true, "moduleDetection": "force", "isolatedModules": true, - /* Strictness */ "strict": true, "noUncheckedIndexedAccess": true, "checkJs": true, - /* Bundled projects */ "lib": ["dom", "dom.iterable", "ES2022"], "noEmit": true, "module": "ESNext", "moduleResolution": "Bundler", - "jsx": "preserve", - "plugins": [{ "name": "next" }], + "jsx": "react-jsx", + "plugins": [ + { + "name": "next" + } + ], "incremental": true, - /* Path Aliases */ "baseUrl": ".", "paths": { @@ -35,7 +36,9 @@ "**/*.tsx", "**/*.cjs", "**/*.js", - ".next/types/**/*.ts" + ".next/types/**/*.ts", + ".next\\dev/types/**/*.ts", + ".next\\dev/types/**/*.ts" ], "exclude": ["node_modules"] }