{"_id":"@astroapps/datagrid-search","_rev":"6-e5f4780a1a7e02e7f9263a2a9413b94e","name":"@astroapps/datagrid-search","dist-tags":{"latest":"1.1.0"},"versions":{"0.1.0":{"name":"@astroapps/datagrid-search","version":"0.1.0","keywords":["react","datagrid","search","filter","sort"],"author":{"name":"Astrolabe Enterprises"},"license":"ISC","_id":"@astroapps/datagrid-search@0.1.0","maintainers":[{"name":"doolse","email":"doolse@gmail.com"},{"name":"connorz","email":"connorzhao97@gmail.com"},{"name":"instagibb","email":"instagibb@gmail.com"}],"homepage":"https://github.com/astrolabe-apps/astrolabe-common#readme","bugs":{"url":"https://github.com/astrolabe-apps/astrolabe-common/issues"},"dist":{"shasum":"694519bd5f4dda80845e039524c2edd75ad0fbfe","tarball":"https://registry.npmjs.org/@astroapps/datagrid-search/-/datagrid-search-0.1.0.tgz","fileCount":37,"integrity":"sha512-rduc763PWWoC3KZU/WL2WG99UKLSRgkxue9Hw3M3MXkpWQE5oMNyrB/gISaRvuKoQ3NbQgvbmkSJObkqsMrFmw==","signatures":[{"sig":"MEQCID/rDOud0c/XjTwScpzoQJzBG1M5iRzsC2U6RwhbGNWTAiAWkhATD/XmxxedhDKxYDoWcnMHq3xe6/odQGAr0IGp6w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":441498},"main":"lib/index.cjs","type":"module","_from":"file:astroapps-datagrid-search-0.1.0.tgz","types":"lib/index.d.ts","module":"lib/index.js","exports":{"types":"./lib/index.d.ts","default":"./lib/index.js","require":"./lib/index.cjs"},"gitHead":"0a65049f88d9244d49815c9dc5007bb0486f1873","scripts":{"test":"jest --coverage","build":"rimraf lib/ node_modules/.cache && microbundle -f modern,cjs --no-compress","watch":"microbundle -f modern,cjs --no-compress"},"_npmUser":{"name":"doolse","email":"doolse@gmail.com"},"_resolved":"/tmp/45253b762b2d5de6ba0dd8a0c656754b/astroapps-datagrid-search-0.1.0.tgz","_integrity":"sha512-rduc763PWWoC3KZU/WL2WG99UKLSRgkxue9Hw3M3MXkpWQE5oMNyrB/gISaRvuKoQ3NbQgvbmkSJObkqsMrFmw==","_npmVersion":"11.6.2","description":"Headless sort, filter and paging for @astroapps/datagrid, over @astroapps/searchstate state","directories":{},"_nodeVersion":"24.11.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","react":"^18.2.0 || ^19","rimraf":"^3.0.2","ts-jest":"^29.2.5","prettier":"^3.0.3","typescript":"^5.6.2","microbundle":"^0.15.1","@types/react":"^18.2.37","@jest/globals":"^29.7.0","@astroapps/datagrid":"1.2.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"2.1.0","@testing-library/react":"^16.0.0","jest-environment-jsdom":"^29.7.0","@react-typed-forms/core":"^4.6.0","@react-typed-forms/transform":"^0.4.0"},"peerDependencies":{"react":"^18.2.0 || ^19","@astroapps/datagrid":"^1.2.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"^2.1.0","@react-typed-forms/core":"^4.6.0"},"_npmOperationalInternal":{"tmp":"tmp/datagrid-search_0.1.0_1785911389331_0.8867784602719975","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@astroapps/datagrid-search","version":"0.2.0","keywords":["react","datagrid","search","filter","sort"],"author":{"name":"Astrolabe Enterprises"},"license":"ISC","_id":"@astroapps/datagrid-search@0.2.0","maintainers":[{"name":"doolse","email":"doolse@gmail.com"},{"name":"connorz","email":"connorzhao97@gmail.com"},{"name":"instagibb","email":"instagibb@gmail.com"}],"homepage":"https://github.com/astrolabe-apps/astrolabe-common#readme","bugs":{"url":"https://github.com/astrolabe-apps/astrolabe-common/issues"},"dist":{"shasum":"0dde92be56304423652d2876c728b68b86891a30","tarball":"https://registry.npmjs.org/@astroapps/datagrid-search/-/datagrid-search-0.2.0.tgz","fileCount":41,"integrity":"sha512-QUR2RK6izPMgJgHjCt2C+P4zGym8GZNuLEC7tF7CdVGL3I74wZI/HnbJ90lfwdQ4K4cHVtu4VFr+dYjCnHfmRA==","signatures":[{"sig":"MEUCIQCfLh7RzN3JyHIUMGGH1cNpMskbu9TK0EVmkUHPeZW1gAIgXFWCPcEx+KsFryQYdXBIVke6e7EyjfLc/HRiBKY9ei4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":486627},"main":"lib/index.cjs","type":"module","_from":"file:astroapps-datagrid-search-0.2.0.tgz","types":"lib/index.d.ts","module":"lib/index.js","exports":{"types":"./lib/index.d.ts","default":"./lib/index.js","require":"./lib/index.cjs"},"gitHead":"26e9d16b61e1ac1aa0c3d73a662f0703174c7ead","scripts":{"test":"jest --coverage","build":"rimraf lib/ node_modules/.cache && microbundle -f modern,cjs --no-compress","watch":"microbundle -f modern,cjs --no-compress"},"_npmUser":{"name":"doolse","email":"doolse@gmail.com"},"_resolved":"/tmp/abad800c7af52a4c8a1e80300de28f32/astroapps-datagrid-search-0.2.0.tgz","_integrity":"sha512-QUR2RK6izPMgJgHjCt2C+P4zGym8GZNuLEC7tF7CdVGL3I74wZI/HnbJ90lfwdQ4K4cHVtu4VFr+dYjCnHfmRA==","_npmVersion":"11.6.2","description":"Headless sort, filter and paging for @astroapps/datagrid, over @astroapps/searchstate state","directories":{},"_nodeVersion":"24.11.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","react":"^18.2.0 || ^19","rimraf":"^3.0.2","ts-jest":"^29.2.5","prettier":"^3.0.3","typescript":"^5.6.2","microbundle":"^0.15.1","@types/react":"^18.2.37","@jest/globals":"^29.7.0","@astroapps/datagrid":"1.2.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"2.1.0","@testing-library/react":"^16.0.0","jest-environment-jsdom":"^29.7.0","@react-typed-forms/core":"^4.6.0","@react-typed-forms/transform":"^0.4.0"},"peerDependencies":{"react":"^18.2.0 || ^19","@astroapps/datagrid":"^1.2.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"^2.1.0","@react-typed-forms/core":"^4.6.0"},"_npmOperationalInternal":{"tmp":"tmp/datagrid-search_0.2.0_1785995886821_0.39841981985493047","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@astroapps/datagrid-search","version":"0.2.1","keywords":["react","datagrid","search","filter","sort"],"author":{"name":"Astrolabe Enterprises"},"license":"ISC","_id":"@astroapps/datagrid-search@0.2.1","maintainers":[{"name":"doolse","email":"doolse@gmail.com"},{"name":"connorz","email":"connorzhao97@gmail.com"},{"name":"instagibb","email":"instagibb@gmail.com"}],"homepage":"https://github.com/astrolabe-apps/astrolabe-common#readme","bugs":{"url":"https://github.com/astrolabe-apps/astrolabe-common/issues"},"dist":{"shasum":"37c5ce506ef4e7e123481a6a4da62c745f6a58fb","tarball":"https://registry.npmjs.org/@astroapps/datagrid-search/-/datagrid-search-0.2.1.tgz","fileCount":41,"integrity":"sha512-JjnD9pAhcbIOFS0hKco4Iwr344xkjOcdNnDp9luWcJsfSOUsMnhJxhmdVVdDwaAJBYKmSzlhCECshzcK8zYaLw==","signatures":[{"sig":"MEQCIHo+p4U+6bWOrQmocbgqDmnWE8Cq2jbmEv2eRtwywRsYAiAn35MimntSkRyEzYTjLRhmPwPbsjQPOVbjplDpYwBfgg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":487111},"main":"lib/index.cjs","type":"module","_from":"file:astroapps-datagrid-search-0.2.1.tgz","types":"lib/index.d.ts","module":"lib/index.js","exports":{"types":"./lib/index.d.ts","default":"./lib/index.js","require":"./lib/index.cjs"},"gitHead":"f086278fa23bab53bab37166d032ce531cbdb046","scripts":{"test":"jest --coverage","build":"rimraf lib/ node_modules/.cache && microbundle -f modern,cjs --no-compress","watch":"microbundle -f modern,cjs --no-compress"},"_npmUser":{"name":"doolse","email":"doolse@gmail.com"},"_resolved":"/tmp/6bd0a91767d9a93a1a5f51ad8a0b2c51/astroapps-datagrid-search-0.2.1.tgz","_integrity":"sha512-JjnD9pAhcbIOFS0hKco4Iwr344xkjOcdNnDp9luWcJsfSOUsMnhJxhmdVVdDwaAJBYKmSzlhCECshzcK8zYaLw==","_npmVersion":"11.6.2","description":"Headless sort, filter and paging for @astroapps/datagrid, over @astroapps/searchstate state","directories":{},"_nodeVersion":"24.11.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","react":"^18.2.0 || ^19","rimraf":"^3.0.2","ts-jest":"^29.2.5","prettier":"^3.0.3","typescript":"^5.6.2","microbundle":"^0.15.1","@types/react":"^18.2.37","@jest/globals":"^29.7.0","@astroapps/datagrid":"1.3.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"2.1.0","@testing-library/react":"^16.0.0","jest-environment-jsdom":"^29.7.0","@react-typed-forms/core":"^4.6.0","@react-typed-forms/transform":"^0.4.0"},"peerDependencies":{"react":"^18.2.0 || ^19","@astroapps/datagrid":"^1.3.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"^2.1.0","@react-typed-forms/core":"^4.6.0"},"_npmOperationalInternal":{"tmp":"tmp/datagrid-search_0.2.1_1788398245877_0.8149556105433322","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@astroapps/datagrid-search","version":"0.3.0","keywords":["react","datagrid","search","filter","sort"],"author":{"name":"Astrolabe Enterprises"},"license":"ISC","_id":"@astroapps/datagrid-search@0.3.0","maintainers":[{"name":"doolse","email":"doolse@gmail.com"},{"name":"connorz","email":"connorzhao97@gmail.com"},{"name":"instagibb","email":"instagibb@gmail.com"}],"homepage":"https://github.com/astrolabe-apps/astrolabe-common#readme","bugs":{"url":"https://github.com/astrolabe-apps/astrolabe-common/issues"},"dist":{"shasum":"d3b2251382b14cdc8ba3e48376734f89c1ef24e1","tarball":"https://registry.npmjs.org/@astroapps/datagrid-search/-/datagrid-search-0.3.0.tgz","fileCount":41,"integrity":"sha512-m5/FR1+2xFJzMNAMLfMIN/2Ikm54Hb50Mh5H33lB74aSlmqrKwqI8p6ayXrX0j8BFd2gTxxfeW8oGNVRmKKtjQ==","signatures":[{"sig":"MEUCIC8N8hhIzSI8O/5bmC+/D4HGtXt1yCNYLjCZHTeJwG9jAiEA7P2gg16MArm92YZD99DtDtAhp1Ezt/tYvwS0lHK2fgI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":522799},"main":"lib/index.cjs","type":"module","_from":"file:astroapps-datagrid-search-0.3.0.tgz","types":"lib/index.d.ts","module":"lib/index.js","exports":{"types":"./lib/index.d.ts","default":"./lib/index.js","require":"./lib/index.cjs"},"gitHead":"027e98eb659b6d2b98c296ec934ee8782197dfed","scripts":{"test":"jest --coverage","build":"rimraf lib/ node_modules/.cache && microbundle -f modern,cjs --no-compress","watch":"microbundle -f modern,cjs --no-compress"},"_npmUser":{"name":"doolse","email":"doolse@gmail.com"},"_resolved":"/tmp/939699eb198f5d484dac7c22c6e37a74/astroapps-datagrid-search-0.3.0.tgz","_integrity":"sha512-m5/FR1+2xFJzMNAMLfMIN/2Ikm54Hb50Mh5H33lB74aSlmqrKwqI8p6ayXrX0j8BFd2gTxxfeW8oGNVRmKKtjQ==","_npmVersion":"11.6.2","description":"Headless sort, filter and paging for @astroapps/datagrid, over @astroapps/searchstate state","directories":{},"_nodeVersion":"24.11.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","react":"^18.2.0 || ^19","rimraf":"^3.0.2","ts-jest":"^29.2.5","prettier":"^3.0.3","typescript":"^5.6.2","microbundle":"^0.15.1","@types/react":"^18.2.37","@jest/globals":"^29.7.0","@astroapps/datagrid":"1.3.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"2.1.0","@testing-library/react":"^16.0.0","jest-environment-jsdom":"^29.7.0","@react-typed-forms/core":"^4.6.0","@react-typed-forms/transform":"^0.4.0"},"peerDependencies":{"react":"^18.2.0 || ^19","@astroapps/datagrid":"^1.3.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"^2.1.0","@react-typed-forms/core":"^4.6.0"},"_npmOperationalInternal":{"tmp":"tmp/datagrid-search_0.3.0_1788421032851_0.07530244231828642","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@astroapps/datagrid-search","version":"1.0.0","keywords":["react","datagrid","search","filter","sort"],"author":{"name":"Astrolabe Enterprises"},"license":"ISC","_id":"@astroapps/datagrid-search@1.0.0","maintainers":[{"name":"doolse","email":"doolse@gmail.com"},{"name":"connorz","email":"connorzhao97@gmail.com"},{"name":"instagibb","email":"instagibb@gmail.com"}],"homepage":"https://github.com/astrolabe-apps/astrolabe-common#readme","bugs":{"url":"https://github.com/astrolabe-apps/astrolabe-common/issues"},"dist":{"shasum":"a6f7fda8f3e4f1eb919fa4c5b4316bcb741ea3eb","tarball":"https://registry.npmjs.org/@astroapps/datagrid-search/-/datagrid-search-1.0.0.tgz","fileCount":83,"integrity":"sha512-qA2E3weWVDXPXIqrBh5hiBXtTtFxyvIa7K7nDGdqhpbgq5vSn3n12lY1S4xfBecaVbMLaQXCGGlPMJh1wkjOmg==","signatures":[{"sig":"MEUCIQCbjb9tJ6Lr0/X8oPbYAkTUsm4KAcQRxgo6YG7QJ6E4KwIgR6RKBPxVXaEkxFPWwd93cGowxQ2sBkz6qZ4wcxIvJmU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIHybl4qH/Uz/f/hjL1PmFyegCTPx3BrwHirykFuFPxRpAiAU4QRIzpYCFgsdkRqiWJveHAgSbTTNyP78ff9OvBvgwA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1325821},"main":"lib/index.cjs","type":"module","_from":"file:astroapps-datagrid-search-1.0.0.tgz","types":"lib/index.d.ts","module":"lib/index.js","exports":{"types":"./lib/index.d.ts","default":"./lib/index.js","require":"./lib/index.cjs"},"gitHead":"53c623dc38a2e9ed167751ed99ea67f93c981bba","scripts":{"test":"jest --coverage","build":"rimraf lib/ node_modules/.cache && microbundle -f modern,cjs --no-compress","watch":"microbundle -f modern,cjs --no-compress"},"_npmUser":{"name":"doolse","email":"doolse@gmail.com"},"_resolved":"/tmp/6efb60c8a6ac77528bd83f6aef2162d7/astroapps-datagrid-search-1.0.0.tgz","_integrity":"sha512-qA2E3weWVDXPXIqrBh5hiBXtTtFxyvIa7K7nDGdqhpbgq5vSn3n12lY1S4xfBecaVbMLaQXCGGlPMJh1wkjOmg==","_npmVersion":"11.6.2","description":"Headless sort, filter and paging for @astroapps/datagrid, over @astroapps/searchstate state","directories":{},"_nodeVersion":"24.11.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","react":"^18.2.0 || ^19","rimraf":"^3.0.2","ts-jest":"^29.2.5","prettier":"^3.0.3","babel-jest":"^29.7.0","typescript":"^5.6.2","microbundle":"^0.15.1","@types/react":"^18.2.37","@jest/globals":"^29.7.0","@babel/preset-env":"^7.26.0","@astroapps/datagrid":"1.3.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"2.1.0","@testing-library/react":"^16.0.0","jest-environment-jsdom":"^29.7.0","@react-typed-forms/core":"^5.0.0","@react-typed-forms/transform":"^0.4.0"},"peerDependencies":{"react":"^18.2.0 || ^19","@astroapps/datagrid":"^1.3.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"^2.1.0","@react-typed-forms/core":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/datagrid-search_1.0.0_1790229960623_0.0694852959295118","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"_id":"@astroapps/datagrid-search@1.1.0","bugs":{"url":"https://github.com/astrolabe-apps/astrolabe-common/issues"},"dist":{"shasum":"a8ca34e150af182ffa412495683da200b450fa3e","tarball":"https://registry.npmjs.org/@astroapps/datagrid-search/-/datagrid-search-1.1.0.tgz","fileCount":83,"integrity":"sha512-6YsKl1KznwS3271weorVrNi0ehqiWtZ6lEIPCTFkgBaUxOZAKH9M1GaiD+RWdHixZsgqNzVdDaqrthnWq3Tfzg==","signatures":[{"sig":"MEQCIDduYsfiOr/F3lReWSLZ6+3v+udVoida2EyqIog5eB5/AiBB/r9UJ7S6z+xP10m/Ls24RhlDHKwGrAsytLZlxUeN/w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD1xcoRLv2RPYjgUVummZf/UIQgZDGvLzTdPL6QTjCAWAIhANVUM4K/xj31M61GrCo5TgbA9AC+/N/JtCUOVW/pWNZW"}],"unpackedSize":1329648},"main":"lib/index.cjs","name":"@astroapps/datagrid-search","type":"module","_from":"file:astroapps-datagrid-search-1.1.0.tgz","types":"lib/index.d.ts","author":{"name":"Astrolabe Enterprises"},"module":"lib/index.js","exports":{"types":"./lib/index.d.ts","default":"./lib/index.js","require":"./lib/index.cjs"},"gitHead":"77a11b2e17064f4749d481bbd90239414fbb986d","license":"ISC","scripts":{"test":"jest --coverage","build":"rimraf lib/ node_modules/.cache && microbundle -f modern,cjs --no-compress","watch":"microbundle -f modern,cjs --no-compress"},"version":"1.1.0","_npmUser":{"name":"doolse","email":"doolse@gmail.com"},"homepage":"https://github.com/astrolabe-apps/astrolabe-common#readme","keywords":["react","datagrid","search","filter","sort"],"_resolved":"/tmp/bf855186b4551b55f28ceb6a058d7b17/astroapps-datagrid-search-1.1.0.tgz","_integrity":"sha512-6YsKl1KznwS3271weorVrNi0ehqiWtZ6lEIPCTFkgBaUxOZAKH9M1GaiD+RWdHixZsgqNzVdDaqrthnWq3Tfzg==","_npmVersion":"11.6.2","description":"Headless sort, filter and paging for @astroapps/datagrid, over @astroapps/searchstate state","directories":{},"maintainers":[{"name":"doolse","email":"doolse@gmail.com"},{"name":"connorz","email":"connorzhao97@gmail.com"},{"name":"instagibb","email":"instagibb@gmail.com"}],"_nodeVersion":"24.11.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","react":"^18.2.0 || ^19","rimraf":"^3.0.2","ts-jest":"^29.2.5","prettier":"^3.0.3","babel-jest":"^29.7.0","typescript":"^5.6.2","microbundle":"^0.15.1","@types/react":"^18.2.37","@jest/globals":"^29.7.0","@babel/preset-env":"^7.26.0","@astroapps/datagrid":"1.3.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"2.1.0","@testing-library/react":"^16.0.0","jest-environment-jsdom":"^29.7.0","@react-typed-forms/core":"^5.0.0","@react-typed-forms/transform":"^0.4.0"},"peerDependencies":{"react":"^18.2.0 || ^19","@astroapps/datagrid":"^1.3.0","@tanstack/react-query":"^5.59.0","@astroapps/searchstate":"^2.1.0","@react-typed-forms/core":"^5.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/datagrid-search_1.1.0_1790564307220_0.6828649868737917"}}},"time":{"created":"2026-08-05T06:29:49.109Z","modified":"2026-09-28T02:58:27.552Z","0.1.0":"2026-08-05T06:29:49.478Z","0.2.0":"2026-08-06T05:58:07.015Z","0.2.1":"2026-09-03T01:17:26.021Z","0.3.0":"2026-09-03T07:37:12.989Z","1.0.0":"2026-09-24T06:06:00.708Z","1.1.0":"2026-09-28T02:58:27.346Z"},"bugs":{"url":"https://github.com/astrolabe-apps/astrolabe-common/issues"},"author":{"name":"Astrolabe Enterprises"},"license":"ISC","homepage":"https://github.com/astrolabe-apps/astrolabe-common#readme","keywords":["react","datagrid","search","filter","sort"],"description":"Headless sort, filter and paging for @astroapps/datagrid, over @astroapps/searchstate state","maintainers":[{"name":"doolse","email":"doolse@gmail.com"},{"name":"connorz","email":"connorzhao97@gmail.com"},{"name":"instagibb","email":"instagibb@gmail.com"}],"readme":"# @astroapps/datagrid-search\n\nHeadless sort, filter, paging and filter-option resolution for\n[`@astroapps/datagrid`](../astrolabe-datagrid), over\n[`@astroapps/searchstate`](../astrolabe-searchstate) state.\n\nRenders nothing. It turns a `Control<SearchRequest>` plus a set of columns into\nthe things a renderer needs — which column is sorted, what a header click should\ndo, which values a filter offers, and the current page of rows.\n[`@astroapps/datagrid-fluent-ui`](../datagrid-fluent-ui) is the Fluent v9\nrenderer built on it.\n\n```bash\nnpm i @astroapps/datagrid-search\n```\n\n## The shape of it\n\n```tsx\nconst state = useControl<SearchRequest>({\n  ...defaultSearchOptions,\n  length: 25,\n});\n\n// ── swap the data source to move between client and server ──\nconst data = useClientData(state, { rows: allRows, columns });\n// const data = useServerData(state, { queryKey: \"files\", search }); // react-query\n\nconst search = useGridSearch(state, { columns, data });\n```\n\n`search` is everything a renderer consumes: `sort`, `filter`, `data`,\n`canFilter(column)` and `useFilterOptions(column)`. Every source produces the same\n`GridData`, so nothing downstream can tell which mode it's in.\n\nTwo things are worth internalising:\n\n- **One state shape, and it's yours to extend.** `Control<SearchRequest>` holds\n  query, sort, filters, offset and length. URL sync, persistence and \"sort by X\"\n  buttons elsewhere on the page all just read and write it. Real pages usually\n  have filtering that isn't a column filter — a date range, a tenant, a \"show\n  archived\" toggle — so **put those fields in the same state**:\n\n  ```tsx\n  interface FilesSearch extends SearchRequest {\n    dateFrom: string | null;\n    includeArchived: boolean;\n  }\n  const state = useControl<FilesSearch>({\n    ...defaultSearchOptions,\n    dateFrom: null,\n    includeArchived: false,\n  });\n  ```\n\n  Every hook is generic over the state, so `fetch` receives the whole thing,\n  changing an extra field refetches, and the count key includes it — no `deps`\n  wiring to forget. Client-side, pass `additionalFilter` to `useClientData`, since\n  the library can't know what your fields mean.\n\n- **Affordances follow column metadata.** A column with a `sortField` is\n  sortable; a column whose filter options resolve is filterable. There are no\n  `enableSorting` flags.\n\n## Sorting\n\n`SearchRequest.sort` is searchstate's `string[]`, each entry a direction\ncharacter followed by the field: `[\"dfile\"]` is descending by `file`.\n\n```tsx\nconst sort = makeGridSort(state, { mode: \"shift\", cycleUnsorted: false });\nsort.isSortable(column); // has a sortField\nsort.direction(column); // \"asc\" | \"desc\" | undefined\nsort.priority(column); // 1-based, multi-sort only\nsort.toggle(column, ev); // ev?.shiftKey matters in \"shift\" mode\n```\n\n| `mode`               | A header click                                                  |\n| -------------------- | --------------------------------------------------------------- |\n| `\"single\"` (default) | replaces any other sort — Fluent's behaviour                    |\n| `\"multiple\"`         | keeps the others; a new column is appended as least significant |\n| `\"shift\"`            | plain click replaces, shift-click appends                       |\n\n`cycleUnsorted` adds the third step, so clicks go default → reverse → unsorted\ninstead of flipping between two.\n\n`newSortFirst` puts a newly added column first instead of last, so the column\njust clicked becomes the primary sort and the others move down. A click on a\ncolumn that's already sorted still only changes its direction, keeping its place.\n\nNote what isn't here: applying the sort to rows. That belongs to the data source,\nand its absence is how a server source says \"these rows are already ordered\".\n\n## Filtering\n\n`SearchRequest.filters` is `Record<string, string[]>` — filter values are always\nstrings. `GridFilter` is the typed accessor over it:\n\n```tsx\nconst filter = makeGridFilter(state, { filterFor });\nfilter.values(field); // string[]\nfilter.selected(field); // Control<string[] | undefined>, writable\nfilter.toggle(field, value, on);\nfilter.setValues(field, next); // removes the key when `next` is empty\nfilter.clear(); // or clear(field)\nfilter.activeFields(); // for a chip bar\n```\n\nAn emptied filter deletes its key rather than storing `[]`, since an empty array\nis a visible difference in a URL and a different query key for an identical\nsearch.\n\n### Which columns filter, and how\n\nOne grid-level function, called with the whole `ColumnDef`:\n\n```tsx\nconst getColumnFilter: GetColumnFilter<Row, Meta> = (column) => {\n  switch (column.data?.kind) {\n    case \"enum\":\n      return { options: fetchEnumValues };\n    case \"number\":\n      return { render: RangePopup, matches: rangeMatches };\n    default:\n      return column.filterField ? {} : undefined;\n  }\n};\n```\n\nA function rather than a field-keyed map because filtering is usually\n**patterned**: one rule keyed off `column.data` covers every enum column, which\nis also how a schema-generated column gets its behaviour without being registered\nby hand. `byFilterField({ status: {...} })` is available when you do want to\nenumerate.\n\n`{}` means \"filterable, all defaults\". With no `getColumnFilter` at all the\ndefault is `column.filterField ? {} : undefined`, so the zero-config path is\nunchanged.\n\n**`getColumnFilter` must be pure.** It's called per column; returning a fresh\n`options` array or `render` closure each call breaks memo dependencies downstream\nand, against an async source, can loop. Results are cached per column id.\n\n`matches` is the escape hatch for filters whose selected values aren't row values\n— ranges, dates, free text. **Client-side only**: a server has to implement the\nequivalent itself.\n\nThe rest of `ColumnFilter` is presentation, per column: `multiple` (default true)\nfor checkboxes vs radios, `searchable` for an options-search box (on past ~12\noptions), and `showCounts` (default true) for the `Document (3)` counts beside\neach option. `showCounts: false` only hides a count — to avoid computing one, a\nderived source takes `counts: false`.\n\n### Filter modes\n\n`filterMode` decides when a popup's selection reaches the search, and what a tick\nmeans. Three settings:\n\n| `filterMode`  | unfiltered shows | a click        | Apply |\n| ------------- | ---------------- | -------------- | ----- |\n| `immediate`   | nothing ticked   | searches       | —     |\n| `apply`       | nothing ticked   | edits a draft  | yes   |\n| `excel`       | everything ticked, with a select-all | edits a draft | yes |\n\n```tsx\nconst search = useGridSearch(state, { columns, data, filterMode: \"apply\" });\n// three values ticked = one search, not three\n```\n\n**Grid-level, not per column** — which click searches, and what a tick means,\nshouldn't vary between one funnel and the next. It lands on `search.filter.mode`\n(with `search.filter.deferApply` as the derived \"not immediate\"), and\n`useFilterDraft` reads it from there: a renderer asks that hook for `values`,\ncalls `toggle`/`setAll`/`clear`/`apply`, and reads `canApply`/`canClear` for its\nbuttons, without knowing which mode it's in.\n\n`apply` is worth it against a server, where each click would otherwise be a\nrequest, two of them already stale before they land. Closing the popup any other\nway discards — there's no cancel to write, because nothing was written.\n\n`deferApply: true` is the older spelling of `filterMode: \"apply\"` and still\nworks; `filterMode` wins if both are given.\n\n#### Excel mode\n\n`excel` is `apply` plus Excel's inversion: an unfiltered column opens with every\nvalue **ticked** rather than none, above a `(Select All)` row that toggles the\nlot. Under an active options-search the select-all covers the matches, as Excel's\ndoes.\n\nThe storage doesn't change — an absent key still means unfiltered — so this is a\npresentation layer over the same `string[]`:\n\n| applied     | opens as             | Apply writes                |\n| ----------- | -------------------- | --------------------------- |\n| absent      | everything ticked    | nothing, if still everything |\n| `[\"doc\"]`   | just `doc` ticked    | `[\"doc\"]`                    |\n| —           | nothing ticked       | refused                      |\n\nTwo consequences worth knowing:\n\n- **Applying with everything ticked clears the column.** It isn't narrowing\n  anything, so the field leaves no trace in URLs or query keys and the funnel\n  goes back to idle.\n- **Applying with nothing ticked is refused** (`canApply` is false, and the\n  renderers disable the button). The empty array is already spoken for — it's\n  what an absent filter reads as — so \"match none\" and \"match everything\" would\n  be the same stored value. Excel refuses it too. Clear is the way back out of\n  that state: in excel mode it re-ticks everything rather than emptying the\n  selection, which is the same thing said the other way round.\n\nA single-select column (`multiple: false`) keeps the plain behaviour whatever the\ngrid is set to — \"everything selected\" isn't a state a radio group can be in.\n\nIn the other modes, Clear empties the selection (the draft when deferred), so\nremoving a deferred filter is Clear then Apply.\n\nA `ColumnFilter.render` of your own is the one exception, unavoidably: it gets the\nreal `selected` control and `close`, so when a selection is final is its own call —\nwhich is what the range popup in the demo does by hand.\n\n## Filter options\n\nFour source shapes, one result:\n\n```tsx\noptions: [{ value: \"doc\", label: \"Document\" }]        // static\noptions: async (ctx) => api.facets(ctx.field, ctx.signal)  // async, per field\noptions: { fromRows: () => rows, max: 50 }            // derived\noptions: { hook: (ctx) => makeFilterOptions(useQuery(...)) }  // your library\n```\n\nResolution order, three deep:\n\n|     | Source                            | Typically                                    |\n| --- | --------------------------------- | -------------------------------------------- |\n| 1   | `getColumnFilter(column).options` | a column that knows its own values           |\n| 2   | `data.facets[field]`              | server-side: counts returned with the page   |\n| 3   | `data.optionRows(field)`          | client-side: derived from the rows on screen |\n| —   | nothing resolves                  | **no filter control is rendered**            |\n\nOptions load **lazily**, because `useFilterOptions` is called inside the popup\nsurface, which only mounts when the popover opens. Nothing is fetched for a\ncolumn nobody filters, and in-flight requests abort on unmount.\n\n**Nothing is cached here.** State lives in the hook, so closing the popover\ndiscards it and reopening fetches again. An internal cache only bought surviving\nclose/reopen, and a second caching layer can disagree with the real one — so if\nyou want caching, deduping, retries or stale-while-revalidate, use the `{ hook }`\nsource and let your query library do all of it:\n\n```tsx\noptions: {\n  hook: ({ field }) =>\n    makeFilterOptions(\n      useQuery({\n        queryKey: [\"facets\", field],\n        queryFn: () => api.facets(field),\n        staleTime: 5 * 60_000,\n      }),\n    ),\n}\n```\n\nClient-derived options ignore the column's **own** filter by default, so picking\none value doesn't hide the others — Excel's behaviour. Turn it off with\n`optionsIgnoreOwnFilter: false`.\n\n## Server-side\n\n`useServerData` is the server counterpart to `useClientData`. It's built on\n**react-query** (a peer dependency), so the fetching — abort on change,\nstale-response ordering, keep-previous, cross-component cache sharing — is the\nquery library's, not a reimplementation. You provide a `search`; it returns the\nsame `GridData` a client grid does. Wrap the app in a `QueryClientProvider`.\n\n```tsx\nconst data = useServerData(state, {\n  queryKey: \"files\", // cache-key prefix; [\"files\", tenantId] to scope it\n  search: (options, includeTotal, signal) =>\n    api.search(options, signal, includeTotal), // returns a GridPage\n  debounce: 300, // `query` only; sort/filter/paging fetch immediately\n  keepPrevious: true, // hold the old page while the next loads\n});\n```\n\n`search` gets the state's whole value, so a state that extends `SearchRequest` with\nits own filtering is carried through — and because it's part of the query key,\nchanging it refetches with no extra wiring.\n\n### The total is optional\n\nCounting is usually a second query over the whole filtered set, so `GridPage.total`\nis optional and `GridData.total` may be `undefined`. `undefined` and `0` are\ndifferent answers: the first means \"not counted\", the second \"counted, nothing\nmatched\" — use `pageInfo(options, data)` rather than reading `total` directly, and\nthe uncounted case (pager shows `1-10`, infers Next from a full page) is handled\nfor you.\n\nA page's `total` also accepts `null`, so a generated response type can be returned\nas-is — `SearchResults<T>`'s `int?` becomes `total: number | null`, and a null\ncount means the same as an absent one. `makeGridData` folds it to `undefined`, so\n`GridData.total` still has exactly one \"not counted\" value.\n\n`useServerData` counts **once per search**, not once per page. `includeTotal` — the\nflag handed to `search` — is true only when there's no total for the current\nsearch; the total is then cached on a key that excludes `offset`/`length`/`sort`,\nso paging and sorting reuse it and a filter or query change re-counts. That\ncondition is \"the search changed\", not \"offset is 0\", so a restored URL like\n`?offset=30` still gets its total. Honour it however suits the endpoint:\n\n- **Cheap combined count** (`COUNT(*) OVER()`): ignore `includeTotal`, always\n  return `total`. Harmless — it just counts on every page.\n- **Expensive count**: return `total` only when `includeTotal`, and skip it\n  otherwise. This maps straight onto a `SearchHelper`-style endpoint, whose\n  `SearchResults<T>(Total, Entries)` is a `GridPage` and whose `includeTotal` is\n  exactly this flag.\n- **No count**: pass `count: false`. Nothing is ever asked for and nothing is ever\n  reported — not even a total another grid sharing the key prefix cached, since a\n  grid opts out because a total would be wrong or unwanted for it.\n\nA `search` that's asked but returns no `total` is recorded as \"asked, none came\"\nand not retried until the search moves; an error surfaces as `GridData.error`.\n\nReturn facets with the page and server-side filter options need no second request:\n\n```ts\n{ rows, total, facets: { category: [{ value: \"Video\", count: 12 }] } }\n```\n\n### Another query library, or none\n\n`GridData` and `GridPage` are plain interfaces, so `useServerData` isn't the only\nway in. Anything that produces a `GridPage` drives a grid through `makeGridData`,\nand `useDebouncedSearchRequest` — the text-debounce a query library lacks — is\nusable on its own:\n\n```tsx\nconst options = useDebouncedSearchRequest(state, 300);\nconst query = useQuery({ queryKey: [\"files\", options], queryFn: ... });\nconst data = makeGridData({\n  page: query.data,\n  loading: query.isFetching, // not isPending: with placeholderData there *is*\n  error: query.error, //         data during a refetch, so isPending is false\n  reload: query.refetch,\n});\n```\n\nReach for this when you want the count wired your own way, or a library other than\nreact-query.\n\n## What can't be uniform across modes\n\nTwo things don't transfer when you swap client for server, and both are silent\nrather than loud:\n\n1. **Free-text `query` scope.** Client-side it searches every leaf column with a\n   `getter`, joined so a query can't match across a column boundary. Server-side\n   it searches whatever your API searches.\n2. **`matches` predicates.** They run in JavaScript over in-memory rows. A range\n   filter with a `matches` will simply not filter against an API unless the API\n   implements the equivalent.\n\n## Custom filter popups\n\nA popup gets a `Control<string[] | undefined>` scoped to its own column, and that\nis the entire contract:\n\n```tsx\nfunction RangePopup({ selected, values, close }: FilterPopupProps<Row>) {\n  return (\n    <Range\n      value={values[0]}\n      onApply={(from, to) => {\n        selected.value = [`${from}..${to}`];\n        close();\n      }}\n    />\n  );\n}\n```\n\nPair it with a `matches` that interprets whatever string it wrote. The popup never\nlearns that a shared filters map exists.\n\n## Shared by every renderer\n\nThree things here render nothing but exist because more than one renderer needs\nthem, and each is somewhere a renderer would otherwise get it subtly wrong on its\nown.\n\n**`makeGridSelection` / `arraySelection`** — page-scoped row selection. The\nheader checkbox reflects and acts on the rows currently rendered and never\ndisturbs a selection made on another page; a renderer supplies only the checkbox\ncolumn over it. Cross-page \"select all N matching\" is deliberately absent — it\nneeds the filtered total, the live search and a way to fetch every matching id,\nat which point it isn't a renderer's business. Neither function is a hook,\ndespite taking a control: they read `.value` when called, so call them during\nrender.\n\n**`shouldIgnoreRowClick(event)`** — whether a click that landed on a row was\naimed at the row. False for two cases that are easy to miss: content that owns\nits own clicks (a checkbox, a link, a `role=\"button\"`, a label wrapping a\ncontrol) and the click that ends a text drag. A row-click handler without this\nlooks fine until someone tries to copy a cell.\n\n**`pagerVisible(options, data, { pageSizes })`** — whether a pager is worth\nrendering. One page and nowhere to go means dead prev/next chrome, *unless* the\npager also carries a page-size selector, which is the only way back from a size\nthat fits every row.\n\n## Testing\n\nMost of the package is plain functions over controls — `makeGridSort`,\n`makeGridFilter`, `clientSearch`, `deriveFilterOptions` — testable without a DOM\nor a renderer. Only the data hooks and `useFilterOptions` need React.\n\n`makeGridSort` / `makeGridFilter` are deliberately **not** named `use*`: they read\n`.value` when called, so they must run on every render and must not be memoised.\n","readmeFilename":"README.md"}