diff --git a/.gitignore b/.gitignore index 7ab505cb6..c1e9287a5 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,10 @@ yarn-error.log* # typescript *.tsbuildinfo + +# generated registry output (rebuilt by `pnpm build:registry` on every deploy). +# styles/index.json is the published style-id list; everything else under +# public/r is produced from registry.json + the authored base trees. +/public/r/styles/* +!/public/r/styles/index.json +/public/r/*.json diff --git a/components.json b/components.json index d19915276..5233ece6b 100644 --- a/components.json +++ b/components.json @@ -14,13 +14,13 @@ "aliases": { "components": "@/components", "utils": "@/lib/utils", - "ui": "@/components/ui", + "ui": "@/registry/bases/radix/ui", "lib": "@/lib", "hooks": "@/hooks" }, "menuColor": "default", "menuAccent": "subtle", "registries": { - "@diceui": "https://diceui.com/r/{name}.json" + "@diceui": "https://diceui.com/r/{style}/{name}.json" } } diff --git a/next.config.ts b/next.config.ts index 8065dff4a..bef86b76b 100644 --- a/next.config.ts +++ b/next.config.ts @@ -6,9 +6,25 @@ import type { NextConfig } from "next"; */ // Import environment validation synchronously import "./src/env.js"; +import { DEFAULT_STYLE_ID, getStyleIds } from "./src/registry/styles"; const nextConfig: NextConfig = { cacheComponents: true, + async rewrites() { + return [ + // Published style ids resolve to their static directory. + { + source: `/r/:style(${getStyleIds().join("|")})/:name.json`, + destination: "/r/styles/:style/:name.json", + }, + // Bases we don't author (`aria-*`) and legacy v3 ids fall back to the + // default rather than 404ing on an otherwise valid install. + { + source: "/r/:style/:name.json", + destination: `/r/styles/${DEFAULT_STYLE_ID}/:name.json`, + }, + ]; + }, // Already doing typechecking as separate task in CI typescript: { ignoreBuildErrors: true }, experimental: { diff --git a/package.json b/package.json index 5f3b27cb9..e02d679f3 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "typecheck": "tsc --noEmit", "check": "oxlint . && oxfmt --check && tsc --noEmit", "shadcn": "pnpm dlx shadcn@latest", - "build:registry": "shadcn build", + "build:registry": "tsx scripts/build-registry.ts", + "clean:registry": "tsx scripts/clean-registry.ts", "db:start": "docker compose up -d", "db:stop": "docker compose down", "db:generate": "dotenv drizzle-kit generate", @@ -34,6 +35,8 @@ "db:studio": "dotenv drizzle-kit studio" }, "dependencies": { + "@base-ui/react": "^1.8.0", + "@base-ui/utils": "^0.4.0", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b715c9bd0..e0a7ce5e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -166,6 +166,12 @@ importers: .: dependencies: + '@base-ui/react': + specifier: ^1.8.0 + version: 1.8.0(@date-fns/tz@1.5.0)(@types/react@19.3.0)(date-fns@4.4.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@base-ui/utils': + specifier: ^0.4.0 + version: 0.4.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -201,7 +207,7 @@ importers: version: 3.14.13(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@uploadthing/react': specifier: ^7.3.3 - version: 7.3.3(next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(uploadthing@7.7.4(express@5.2.1)(next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(tailwindcss@4.3.3)) + version: 7.3.3(next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(uploadthing@7.7.4(express@5.2.1(supports-color@10.2.2))(next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(tailwindcss@4.3.3)) '@upstash/ratelimit': specifier: ^2.1.0 version: 2.1.0(@upstash/redis@1.38.4) @@ -228,19 +234,19 @@ importers: version: 1.5.0 geist: specifier: ^1.7.2 - version: 1.7.2(next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)) + version: 1.7.2(next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)) lucide-react: specifier: ^1.47.0 version: 1.47.0(react@19.3.0) next: specifier: ^16.3.5 - version: 16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + version: 16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.3.0(react@19.3.0))(react@19.3.0) nuqs: specifier: ^2.10.1 - version: 2.10.1(next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0) + version: 2.10.1(next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0) partysocket: specifier: ^1.3.0 version: 1.3.0(react@19.3.0) @@ -267,7 +273,7 @@ importers: version: 0.0.1 shadcn: specifier: 4.21.0 - version: 4.21.0(typescript@7.0.2) + version: 4.21.0(supports-color@10.2.2)(typescript@7.0.2) sonner: specifier: ^2.0.8 version: 2.0.8(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -276,7 +282,7 @@ importers: version: 3.7.0 uploadthing: specifier: ^7.7.4 - version: 7.7.4(express@5.2.1)(next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(tailwindcss@4.3.3) + version: 7.7.4(express@5.2.1(supports-color@10.2.2))(next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(tailwindcss@4.3.3) vaul: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) @@ -334,7 +340,7 @@ importers: version: 7.0.2002 partykit: specifier: ^0.0.115 - version: 0.0.115 + version: 0.0.115(supports-color@10.2.2) postcss: specifier: ^8.5.28 version: 8.5.28 @@ -507,6 +513,33 @@ packages: resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} + '@base-ui/react@1.8.0': + resolution: {integrity: sha512-P0/1sxo6SBVZOklKMIedvTWqw2s2IQzi9x5bIVsXu980cuSOD4NeuRSs+/L7LZQfDkZP/uRZyGPyfFl/B1oH+Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@date-fns/tz': ^1.2.0 + '@types/react': ^17 || ^18 || ^19 + date-fns: ^4.0.0 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@date-fns/tz': + optional: true + '@types/react': + optional: true + date-fns: + optional: true + + '@base-ui/utils@0.4.0': + resolution: {integrity: sha512-bO9fz25kKtPf+aZVyfQrC0PDmJdmVni31W2hCS5/Owb+inwdIL3XU26pCPRPlt4LSxZrBgLwubXQXQlKaFEZzw==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@bramus/specificity@2.4.2': resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} hasBin: true @@ -4743,6 +4776,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + reselect@5.3.0: + resolution: {integrity: sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -5398,20 +5434,20 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.7': + '@babel/core@7.29.7(supports-color@10.2.2)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) '@babel/helpers': 7.29.7 '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@10.2.2) '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -5438,41 +5474,41 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@10.2.2) '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/traverse': 7.29.8 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@10.2.2) + '@babel/traverse': 7.29.8(supports-color@10.2.2) semver: 6.3.1 transitivePeerDependencies: - supports-color '@babel/helper-globals@7.29.7': {} - '@babel/helper-member-expression-to-functions@7.29.7': + '@babel/helper-member-expression-to-functions@7.29.7(supports-color@10.2.2)': dependencies: - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@10.2.2) '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.29.7': + '@babel/helper-module-imports@7.29.7(supports-color@10.2.2)': dependencies: - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@10.2.2) '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/helper-module-imports': 7.29.7(supports-color@10.2.2) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -5482,18 +5518,18 @@ snapshots: '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@10.2.2) '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@10.2.2) transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + '@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@10.2.2)': dependencies: - '@babel/traverse': 7.29.8 + '@babel/traverse': 7.29.8(supports-color@10.2.2) '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color @@ -5513,43 +5549,43 @@ snapshots: dependencies: '@babel/types': 7.29.8 - '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@10.2.2) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@10.2.2) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/preset-typescript@7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2)) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -5561,7 +5597,7 @@ snapshots: '@babel/parser': 7.29.8 '@babel/types': 7.29.8 - '@babel/traverse@7.29.8': + '@babel/traverse@7.29.8(supports-color@10.2.2)': dependencies: '@babel/code-frame': 7.29.7 '@babel/generator': 7.29.8 @@ -5569,7 +5605,7 @@ snapshots: '@babel/parser': 7.29.8 '@babel/template': 7.29.7 '@babel/types': 7.29.8 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -5578,6 +5614,31 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@base-ui/react@1.8.0(@date-fns/tz@1.5.0)(@types/react@19.3.0)(date-fns@4.4.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@base-ui/utils': 0.4.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@floating-ui/react-dom': 2.1.9(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@floating-ui/utils': 0.2.12 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + use-sync-external-store: 1.7.0(react@19.3.0) + optionalDependencies: + '@date-fns/tz': 1.5.0 + '@types/react': 19.3.0 + date-fns: 4.4.0 + + '@base-ui/utils@0.4.0(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@floating-ui/utils': 0.2.12 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + reselect: 5.3.0 + use-sync-external-store: 1.7.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.3.0 + '@bramus/specificity@2.4.2': dependencies: css-tree: 3.2.1 @@ -6169,7 +6230,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 - '@modelcontextprotocol/sdk@1.30.0(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.30.0(supports-color@10.2.2)(zod@3.25.76)': dependencies: '@hono/node-server': 2.1.1(hono@4.13.8) ajv: 8.20.0 @@ -6179,8 +6240,8 @@ snapshots: cross-spawn: 7.0.6 eventsource: 3.0.7 eventsource-parser: 3.1.1 - express: 5.2.1 - express-rate-limit: 8.7.0(express@5.2.1) + express: 5.2.1(supports-color@10.2.2) + express-rate-limit: 8.7.0(express@5.2.1(supports-color@10.2.2))(supports-color@10.2.2) hono: 4.13.8 jose: 6.2.12 json-schema-typed: 8.0.2 @@ -7457,14 +7518,14 @@ snapshots: '@uploadthing/mime-types@0.3.6': {} - '@uploadthing/react@7.3.3(next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(uploadthing@7.7.4(express@5.2.1)(next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(tailwindcss@4.3.3))': + '@uploadthing/react@7.3.3(next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0)(uploadthing@7.7.4(express@5.2.1(supports-color@10.2.2))(next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(tailwindcss@4.3.3))': dependencies: '@uploadthing/shared': 7.1.10 file-selector: 0.6.0 react: 19.3.0 - uploadthing: 7.7.4(express@5.2.1)(next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(tailwindcss@4.3.3) + uploadthing: 7.7.4(express@5.2.1(supports-color@10.2.2))(next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(tailwindcss@4.3.3) optionalDependencies: - next: 16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + next: 16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) '@uploadthing/shared@7.1.10': dependencies: @@ -7567,11 +7628,11 @@ snapshots: dependencies: require-from-string: 2.0.2 - body-parser@2.3.0: + body-parser@2.3.0(supports-color@10.2.2): dependencies: bytes: 3.1.2 content-type: 2.1.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) http-errors: 2.0.1 iconv-lite: 0.7.3 on-finished: 2.4.1 @@ -7619,9 +7680,9 @@ snapshots: caniuse-lite@1.0.30001810: {} - capnp-ts@0.7.0: + capnp-ts@0.7.0(supports-color@10.2.2): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -7756,9 +7817,11 @@ snapshots: dependencies: mimic-fn: 3.1.0 - debug@4.4.3: + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 decimal.js@10.6.0: {} @@ -8044,28 +8107,28 @@ snapshots: export-to-csv@1.5.0: {} - express-rate-limit@8.7.0(express@5.2.1): + express-rate-limit@8.7.0(express@5.2.1(supports-color@10.2.2))(supports-color@10.2.2): dependencies: - debug: 4.4.3 - express: 5.2.1 + debug: 4.4.3(supports-color@10.2.2) + express: 5.2.1(supports-color@10.2.2) ip-address: 10.7.2 transitivePeerDependencies: - supports-color - express@5.2.1: + express@5.2.1(supports-color@10.2.2): dependencies: accepts: 2.0.0 - body-parser: 2.3.0 + body-parser: 2.3.0(supports-color@10.2.2) content-disposition: 1.1.0 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 2.1.1 + finalhandler: 2.1.1(supports-color@10.2.2) fresh: 2.0.0 http-errors: 2.0.1 merge-descriptors: 2.0.0 @@ -8076,9 +8139,9 @@ snapshots: proxy-addr: 2.0.8 qs: 6.16.0 range-parser: 1.3.0 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 + router: 2.2.0(supports-color@10.2.2) + send: 1.2.1(supports-color@10.2.2) + serve-static: 2.2.1(supports-color@10.2.2) statuses: 2.0.2 type-is: 2.1.0 vary: 1.1.2 @@ -8121,9 +8184,9 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@2.1.1: + finalhandler@2.1.1(supports-color@10.2.2): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -8157,9 +8220,9 @@ snapshots: fuzzysort@3.1.0: {} - geist@1.7.2(next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)): + geist@1.7.2(next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)): dependencies: - next: 16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + next: 16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) gensync@1.0.0-beta.2: {} @@ -8546,12 +8609,12 @@ snapshots: mimic-function@5.0.1: {} - miniflare@3.20240718.0: + miniflare@3.20240718.0(supports-color@10.2.2): dependencies: '@cspotcode/source-map-support': 0.8.1 acorn: 8.18.0 acorn-walk: 8.3.5 - capnp-ts: 0.7.0 + capnp-ts: 0.7.0(supports-color@10.2.2) exit-hook: 2.2.1 glob-to-regexp: 0.4.1 stoppable: 1.1.0 @@ -8613,7 +8676,7 @@ snapshots: react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): + next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0): dependencies: '@next/env': 16.3.5 '@swc/helpers': 0.5.23 @@ -8622,7 +8685,7 @@ snapshots: postcss: 8.5.23 react: 19.3.0 react-dom: 19.3.0(react@19.3.0) - styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.3.0) + styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0) optionalDependencies: '@next/swc-darwin-arm64': 16.3.5 '@next/swc-darwin-x64': 16.3.5 @@ -8658,12 +8721,12 @@ snapshots: path-key: 4.0.0 unicorn-magic: 0.3.0 - nuqs@2.10.1(next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0): + nuqs@2.10.1(next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react@19.3.0): dependencies: '@standard-schema/spec': 1.1.0 react: 19.3.0 optionalDependencies: - next: 16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + next: 16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) object-assign@4.1.1: {} @@ -8809,12 +8872,12 @@ snapshots: parseurl@1.3.3: {} - partykit@0.0.115: + partykit@0.0.115(supports-color@10.2.2): dependencies: '@cloudflare/workers-types': 4.20240718.0 clipboardy: 4.0.0 esbuild: 0.21.5 - miniflare: 3.20240718.0 + miniflare: 3.20240718.0(supports-color@10.2.2) ts-dedent: 2.3.0 unenv: 2.0.0-rc.0 yoga-wasm-web: 0.3.3 @@ -9054,6 +9117,8 @@ snapshots: require-from-string@2.0.2: {} + reselect@5.3.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -9091,9 +9156,9 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.9 '@rolldown/binding-win32-x64-msvc': 1.2.9 - router@2.2.0: + router@2.2.0(supports-color@10.2.2): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -9123,9 +9188,9 @@ snapshots: semver@7.8.5: {} - send@1.2.1: + send@1.2.1(supports-color@10.2.2): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -9139,12 +9204,12 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@2.2.1: + serve-static@2.2.1(supports-color@10.2.2): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 1.2.1 + send: 1.2.1(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -9152,14 +9217,14 @@ snapshots: setprototypeof@1.2.0: {} - shadcn@4.21.0(typescript@7.0.2): + shadcn@4.21.0(supports-color@10.2.2)(typescript@7.0.2): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) '@babel/parser': 7.29.8 - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) - '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@10.2.2))(supports-color@10.2.2) '@dotenvx/dotenvx': 1.75.1 - '@modelcontextprotocol/sdk': 1.30.0(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.30.0(supports-color@10.2.2)(zod@3.25.76) '@types/validate-npm-package-name': 4.0.2 browserslist: 4.29.0 cn: 0.2.6 @@ -9341,12 +9406,12 @@ snapshots: strip-final-newline@4.0.0: {} - styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.3.0): + styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@10.2.2))(react@19.3.0): dependencies: client-only: 0.0.1 react: 19.3.0 optionalDependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@10.2.2) supports-color@10.2.2: {} @@ -9481,7 +9546,7 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 - uploadthing@7.7.4(express@5.2.1)(next@16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(tailwindcss@4.3.3): + uploadthing@7.7.4(express@5.2.1(supports-color@10.2.2))(next@16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(tailwindcss@4.3.3): dependencies: '@effect/platform': 0.90.3(effect@3.17.7) '@standard-schema/spec': 1.0.0-beta.4 @@ -9489,8 +9554,8 @@ snapshots: '@uploadthing/shared': 7.1.10 effect: 3.17.7 optionalDependencies: - express: 5.2.1 - next: 16.3.5(@babel/core@7.29.7)(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + express: 5.2.1(supports-color@10.2.2) + next: 16.3.5(@babel/core@7.29.7(supports-color@10.2.2))(@types/node@26.6.1)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) tailwindcss: 4.3.3 use-callback-ref@1.3.3(@types/react@19.3.0)(react@19.3.0): diff --git a/public/r/data-grid-filter-menu.json b/public/r/data-grid-filter-menu.json deleted file mode 100644 index 442b5dca4..000000000 --- a/public/r/data-grid-filter-menu.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-grid-filter-menu", - "title": "Data Grid Filter Menu", - "description": "A filter menu component for the data grid with drag-and-drop reordering and advanced filtering", - "dependencies": [ - "@tanstack/react-table", - "lucide-react" - ], - "registryDependencies": [ - "badge", - "button", - "calendar", - "command", - "input", - "popover", - "select" - ], - "files": [ - { - "path": "src/components/data-grid/data-grid-filter-menu.tsx", - "content": "\"use client\";\n\nimport type {\n Column,\n ColumnFilter,\n RowData,\n Table,\n} from \"@tanstack/react-table\";\n\nimport {\n CalendarIcon,\n Check,\n ChevronsUpDown,\n GripVertical,\n ListFilter,\n Trash2,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\nimport type { FilterOperator, FilterValue } from \"@/lib/data-grid-types\";\n\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 { useDirection } from \"@/components/ui/direction\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\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 { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport {\n getDefaultOperator,\n getOperatorsForVariant,\n} from \"@/lib/data-grid-filters\";\nimport { formatDate } from \"@/lib/format\";\nimport { cn } from \"@/lib/utils\";\n\nconst FILTER_SHORTCUT_KEY = \"f\";\nconst REMOVE_FILTER_SHORTCUTS = new Set([\"backspace\", \"delete\"]);\nconst FILTER_DEBOUNCE_MS = 300;\nconst OPERATORS_WITHOUT_VALUE = new Set([\n \"isEmpty\",\n \"isNotEmpty\",\n \"isTrue\",\n \"isFalse\",\n]);\n\ninterface DataGridFilterMenuProps<\n TData extends RowData,\n> extends React.ComponentProps {\n table: Table;\n disabled?: boolean;\n}\n\nexport function DataGridFilterMenu({\n table,\n disabled,\n className,\n ...props\n}: DataGridFilterMenuProps) {\n const dir = useDirection();\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 columnFilters = table.store.state.columnFilters;\n\n const { columnLabels, columns, columnVariants } = React.useMemo(() => {\n const labels = new Map();\n const variants = new Map();\n const filteringIds = new Set(columnFilters.map((f) => f.id));\n const availableColumns: { id: string; label: string }[] = [];\n\n for (const column of table.getAllColumns()) {\n if (!column.getCanFilter()) continue;\n\n const label = column.columnDef.meta?.label ?? column.id;\n const variant = column.columnDef.meta?.cell?.variant ?? \"short-text\";\n\n labels.set(column.id, label);\n variants.set(column.id, variant);\n\n if (!filteringIds.has(column.id)) {\n availableColumns.push({ id: column.id, label });\n }\n }\n\n return {\n columnLabels: labels,\n columns: availableColumns,\n columnVariants: variants,\n };\n }, [columnFilters, table]);\n\n const onFilterAdd = React.useCallback(() => {\n const firstColumn = columns[0];\n if (!firstColumn) return;\n\n const variant = columnVariants.get(firstColumn.id) ?? \"short-text\";\n const defaultOperator = getDefaultOperator(variant);\n\n table.setColumnFilters((prevFilters) => [\n ...prevFilters,\n {\n id: firstColumn.id,\n value: {\n operator: defaultOperator,\n value: \"\",\n },\n },\n ]);\n }, [columns, columnVariants, table]);\n\n const onFilterUpdate = React.useCallback(\n (filterId: string, updates: Partial) => {\n table.setColumnFilters((prevFilters) => {\n if (!prevFilters) return prevFilters;\n return prevFilters.map((filter) =>\n filter.id === filterId ? { ...filter, ...updates } : filter,\n );\n });\n },\n [table],\n );\n\n const onFilterRemove = React.useCallback(\n (filterId: string) => {\n table.setColumnFilters((prevFilters) =>\n prevFilters.filter((item) => item.id !== filterId),\n );\n },\n [table],\n );\n\n const onFiltersReset = React.useCallback(() => {\n table.setColumnFilters(table.initialState.columnFilters ?? []);\n }, [table]);\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.has(event.key.toLowerCase()) &&\n columnFilters.length > 0\n ) {\n event.preventDefault();\n onFiltersReset();\n }\n },\n [columnFilters.length, onFiltersReset],\n );\n\n return (\n item.id}\n >\n \n \n \n \n Filter\n {columnFilters.length > 0 && (\n \n {columnFilters.length}\n \n )}\n \n \n \n
\n

\n {columnFilters.length > 0 ? \"Filter by\" : \"No filters applied\"}\n

\n 0 && \"sr-only\",\n )}\n >\n {columnFilters.length > 0\n ? \"Modify filters to narrow down your data.\"\n : \"Add filters to narrow down your data.\"}\n

\n
\n {columnFilters.length > 0 && (\n \n \n {columnFilters.map((filter, index) => (\n \n ))}\n \n \n )}\n
\n \n Add filter\n \n {columnFilters.length > 0 && (\n \n Reset filters\n \n )}\n
\n \n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n \n );\n}\n\ninterface DataGridFilterItemProps {\n filter: ColumnFilter;\n index: number;\n filterItemId: string;\n dir: \"ltr\" | \"rtl\";\n columns: { id: string; label: string }[];\n columnLabels: Map;\n columnVariants: Map;\n table: Table;\n onFilterUpdate: (filterId: string, updates: Partial) => void;\n onFilterRemove: (filterId: string) => void;\n}\n\nfunction DataGridFilterItem({\n filter,\n index,\n filterItemId,\n dir,\n columns,\n columnLabels,\n columnVariants,\n table,\n onFilterUpdate,\n onFilterRemove,\n}: DataGridFilterItemProps) {\n const fieldListboxId = `${filterItemId}-field-listbox`;\n const fieldTriggerId = `${filterItemId}-field-trigger`;\n const operatorListboxId = `${filterItemId}-operator-listbox`;\n const inputId = `${filterItemId}-input`;\n\n const [showFieldSelector, setShowFieldSelector] = React.useState(false);\n const [showOperatorSelector, setShowOperatorSelector] = React.useState(false);\n\n const variant = columnVariants.get(filter.id) ?? \"short-text\";\n const filterValue = filter.value as FilterValue | undefined;\n const operator = filterValue?.operator ?? getDefaultOperator(variant);\n\n const operators = getOperatorsForVariant(variant);\n const needsValue = !OPERATORS_WITHOUT_VALUE.has(operator);\n\n const column = table.getColumn(filter.id);\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) {\n return;\n }\n\n if (REMOVE_FILTER_SHORTCUTS.has(event.key.toLowerCase())) {\n event.preventDefault();\n onFilterRemove(filter.id);\n }\n },\n [filter.id, showFieldSelector, showOperatorSelector, onFilterRemove],\n );\n\n const onOperatorChange = React.useCallback(\n (newOperator: FilterOperator) => {\n onFilterUpdate(filter.id, {\n value: {\n operator: newOperator,\n value: filterValue?.value,\n endValue: filterValue?.endValue,\n },\n });\n },\n [filter.id, filterValue?.value, filterValue?.endValue, onFilterUpdate],\n );\n\n const onValueChange = React.useCallback(\n (newValue: string | number | string[] | undefined) => {\n onFilterUpdate(filter.id, {\n value: {\n operator,\n value: newValue,\n endValue: filterValue?.endValue,\n },\n });\n },\n [filter.id, operator, filterValue?.endValue, onFilterUpdate],\n );\n\n const onEndValueChange = React.useCallback(\n (newValue: string | number | string[] | undefined) => {\n onFilterUpdate(filter.id, {\n value: {\n operator,\n value: filterValue?.value,\n endValue: newValue as string | number | undefined,\n },\n });\n },\n [filter.id, operator, filterValue?.value, onFilterUpdate],\n );\n\n return (\n \n \n
\n {index === 0 ? (\n Where\n ) : (\n And\n )}\n
\n \n \n \n {columnLabels.get(filter.id)}\n \n \n \n \n \n \n \n No fields found.\n \n {columns.map((column) => (\n {\n const newVariant =\n columnVariants.get(value) ?? \"short-text\";\n const newOperator = getDefaultOperator(newVariant);\n\n table.setColumnFilters((prevFilters) =>\n prevFilters.map((f) =>\n f.id === filter.id\n ? {\n id: value,\n value: {\n operator: newOperator,\n value: \"\",\n },\n }\n : f,\n ),\n );\n setShowFieldSelector(false);\n }}\n >\n {column.label}\n \n \n ))}\n \n \n \n \n \n \n \n
\n \n
\n \n \n \n {operators.map((op) => (\n \n {op.label}\n \n ))}\n \n \n \n
\n {needsValue && column ? (\n \n ) : (\n \n )}\n
\n onFilterRemove(filter.id)}\n >\n \n \n \n \n \n
\n \n );\n}\n\ninterface DataGridFilterInputProps {\n variant: string;\n operator: FilterOperator;\n dir: \"ltr\" | \"rtl\";\n placeholder?: string;\n value: string | number | string[] | undefined;\n endValue?: string | number;\n column: Column;\n inputId: string;\n onValueChange: (value: string | number | string[] | undefined) => void;\n onEndValueChange?: (value: string | number | string[] | undefined) => void;\n}\n\nfunction DataGridFilterInput({\n variant,\n operator,\n dir,\n placeholder = \"Value\",\n value,\n endValue,\n column,\n inputId,\n onValueChange,\n onEndValueChange,\n}: DataGridFilterInputProps) {\n const [showValueSelector, setShowValueSelector] = React.useState(false);\n const [localValue, setLocalValue] = React.useState(value);\n const [localEndValue, setLocalEndValue] = React.useState(endValue);\n\n const debouncedOnChange = useDebouncedCallback(\n (newValue: string | number | string[] | undefined) => {\n onValueChange(newValue);\n },\n FILTER_DEBOUNCE_MS,\n );\n\n const debouncedOnEndValueChange = useDebouncedCallback(\n (newValue: string | number | string[] | undefined) => {\n onEndValueChange?.(newValue);\n },\n FILTER_DEBOUNCE_MS,\n );\n\n const cellVariant = column.columnDef.meta?.cell;\n\n const selectOptions = React.useMemo(() => {\n return cellVariant?.variant === \"select\" ||\n cellVariant?.variant === \"multi-select\"\n ? cellVariant.options\n : [];\n }, [cellVariant]);\n\n const isBetween = operator === \"isBetween\";\n\n if (variant === \"number\") {\n if (isBetween) {\n return (\n
\n {\n const val = event.target.value;\n const newValue = val === \"\" ? undefined : Number(val);\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n }}\n className=\"h-8 w-full flex-1 rounded\"\n />\n {\n const val = event.target.value;\n const newValue = val === \"\" ? undefined : Number(val);\n setLocalEndValue(newValue);\n debouncedOnEndValueChange(newValue);\n }}\n className=\"h-8 w-full flex-1 rounded\"\n />\n
\n );\n }\n\n return (\n {\n const val = event.target.value;\n const newValue = val === \"\" ? undefined : Number(val);\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n }}\n className=\"h-8 w-full rounded\"\n />\n );\n }\n\n if (variant === \"date\") {\n const inputListboxId = `${inputId}-listbox`;\n\n if (isBetween) {\n const startDate =\n localValue && typeof localValue === \"string\"\n ? new Date(localValue)\n : undefined;\n const endDate =\n localEndValue && typeof localEndValue === \"string\"\n ? new Date(localEndValue)\n : undefined;\n\n const isSameDate =\n startDate &&\n endDate &&\n startDate.toDateString() === endDate.toDateString();\n\n const displayValue =\n startDate && endDate && !isSameDate\n ? `${formatDate(startDate, { month: \"short\" })} - ${formatDate(endDate, { month: \"short\" })}`\n : startDate\n ? formatDate(startDate, { month: \"short\" })\n : \"Pick a range\";\n\n return (\n \n \n \n \n {displayValue}\n \n \n \n {\n const fromValue = range?.from\n ? range.from.toISOString()\n : undefined;\n const toValue = range?.to ? range.to.toISOString() : undefined;\n setLocalValue(fromValue);\n setLocalEndValue(toValue);\n onValueChange(fromValue);\n onEndValueChange?.(toValue);\n }}\n />\n \n \n );\n }\n\n const dateValue =\n localValue && typeof localValue === \"string\"\n ? new Date(localValue)\n : undefined;\n\n return (\n \n \n \n \n \n {dateValue\n ? formatDate(dateValue, { month: \"short\" })\n : \"Pick a date\"}\n \n \n \n \n {\n const newValue = date ? date.toISOString() : undefined;\n setLocalValue(newValue);\n onValueChange(newValue);\n setShowValueSelector(false);\n }}\n />\n \n \n );\n }\n\n const isSelectVariant = variant === \"select\" || variant === \"multi-select\";\n const isMultiValueOperator =\n operator === \"isAnyOf\" || operator === \"isNoneOf\";\n\n if (isSelectVariant && selectOptions.length > 0) {\n const inputListboxId = `${inputId}-listbox`;\n\n if (isMultiValueOperator) {\n const selectedValues = Array.isArray(value) ? value : [];\n const selectedOptions = selectOptions.filter((option) =>\n selectedValues.includes(option.value),\n );\n\n const selectedOptionsWithIcons = selectedOptions.filter(\n (selectedOption) => selectedOption.icon,\n );\n\n return (\n \n \n \n {selectedOptions.length === 0 ? (\n {placeholder}\n ) : (\n <>\n {selectedOptionsWithIcons.length > 0 && (\n
\n {selectedOptionsWithIcons.map(\n (selectedOption) =>\n selectedOption.icon && (\n \n \n
\n ),\n )}\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 {selectOptions.map((option) => {\n const isSelected = selectedValues.includes(option.value);\n return (\n {\n const newValues = isSelected\n ? selectedValues.filter((v) => v !== option.value)\n : [...selectedValues, option.value];\n onValueChange(\n newValues.length > 0 ? newValues : undefined,\n );\n }}\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 );\n }\n\n const selectedOption = selectOptions.find(\n (opt) => opt.value === (value as string),\n );\n\n return (\n \n \n \n {selectedOption ? (\n <>\n {selectedOption.icon && }\n {selectedOption.label}\n \n ) : (\n {placeholder}\n )}\n \n \n \n \n \n \n No options found.\n \n {selectOptions.map((option) => (\n {\n onValueChange(option.value);\n setShowValueSelector(false);\n }}\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 }\n\n if (isBetween) {\n return (\n
\n {\n const val = event.target.value;\n const newValue = val === \"\" ? undefined : val;\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n }}\n />\n {\n const val = event.target.value;\n const newValue = val === \"\" ? undefined : val;\n setLocalEndValue(newValue);\n debouncedOnEndValueChange(newValue);\n }}\n />\n
\n );\n }\n\n return (\n {\n const val = event.target.value;\n const newValue = val === \"\" ? undefined : val;\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n }}\n />\n );\n}\n", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-filter-menu.tsx" - }, - { - "path": "src/components/ui/direction.tsx", - "content": "\"use client\";\n\nimport type * as React from \"react\";\n\nimport { Direction } from \"radix-ui\";\n\nfunction DirectionProvider({\n dir,\n direction,\n children,\n}: React.ComponentProps & {\n direction?: React.ComponentProps[\"dir\"];\n}) {\n return (\n \n {children}\n \n );\n}\n\nconst useDirection = Direction.useDirection;\n\nexport { DirectionProvider, useDirection };\n", - "type": "registry:ui", - "target": "src/components/ui/direction.tsx" - }, - { - "path": "src/components/ui/sortable.tsx", - "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 as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\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 SortableProps = 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 Sortable(props: SortableProps) {\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 ...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(\n \"`getItemValue` is required when using array of objects\",\n );\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 sortableProps.onDragStart?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(event.active.id);\n },\n [sortableProps.onDragStart],\n );\n\n const onDragEnd = React.useCallback(\n (event: DragEndEvent) => {\n sortableProps.onDragEnd?.(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, sortableProps.onDragEnd],\n );\n\n const onDragCancel = React.useCallback(\n (event: DragEndEvent) => {\n sortableProps.onDragCancel?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(null);\n },\n [sortableProps.onDragCancel],\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 ? SlotPrimitive.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 ? SlotPrimitive.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 ? SlotPrimitive.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 extends Omit<\n React.ComponentProps,\n \"children\"\n> {\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\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 Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n type SortableProps,\n};\n", - "type": "registry:ui", - "target": "src/components/ui/sortable.tsx" - }, - { - "path": "src/hooks/use-debounced-callback.ts", - "content": "/**\n * @see https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts\n */\n\nimport * as React from \"react\";\n\nimport { useCallbackRef } from \"@/hooks/use-callback-ref\";\n\nexport function useDebouncedCallback unknown>(\n callback: T,\n delay: number,\n) {\n const handleCallback = useCallbackRef(callback);\n const debounceTimerRef = React.useRef(0);\n React.useEffect(\n () => () => window.clearTimeout(debounceTimerRef.current),\n [],\n );\n\n const setValue = React.useCallback(\n (...args: Parameters) => {\n window.clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = window.setTimeout(\n () => handleCallback(...args),\n delay,\n );\n },\n [handleCallback, delay],\n );\n\n return setValue;\n}\n", - "type": "registry:hook" - }, - { - "path": "src/lib/compose-refs.ts", - "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef = React.Ref | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef(ref: PossibleRef, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n }\n\n if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs(...refs: PossibleRef[]): React.RefCallback {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup === \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup === \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs(...refs: PossibleRef[]): React.RefCallback {\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-filters.ts", - "content": "import type { FilterFn, Row, RowData } from \"@tanstack/react-table\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\nimport type {\n BooleanFilterOperator,\n DateFilterOperator,\n FilterOperator,\n FilterValue,\n NumberFilterOperator,\n SelectFilterOperator,\n TextFilterOperator,\n} from \"@/lib/data-grid-types\";\n\nimport { stringifyUnknown } from \"@/lib/data-grid-utils\";\n\nexport const TEXT_FILTER_OPERATORS: ReadonlyArray<{\n label: string;\n value: TextFilterOperator;\n}> = [\n { label: \"Contains\", value: \"contains\" },\n { label: \"Does not contain\", value: \"notContains\" },\n { label: \"Is\", value: \"equals\" },\n { label: \"Is not\", value: \"notEquals\" },\n { label: \"Starts with\", value: \"startsWith\" },\n { label: \"Ends with\", value: \"endsWith\" },\n { label: \"Is empty\", value: \"isEmpty\" },\n { label: \"Is not empty\", value: \"isNotEmpty\" },\n];\n\nexport const NUMBER_FILTER_OPERATORS: ReadonlyArray<{\n label: string;\n value: NumberFilterOperator;\n}> = [\n { label: \"Is\", value: \"equals\" },\n { label: \"Is not\", value: \"notEquals\" },\n { label: \"Is less than\", value: \"lessThan\" },\n { label: \"Is less than or equal to\", value: \"lessThanOrEqual\" },\n { label: \"Is greater than\", value: \"greaterThan\" },\n { label: \"Is greater than or equal to\", value: \"greaterThanOrEqual\" },\n { label: \"Is between\", value: \"isBetween\" },\n { label: \"Is empty\", value: \"isEmpty\" },\n { label: \"Is not empty\", value: \"isNotEmpty\" },\n];\n\nexport const DATE_FILTER_OPERATORS: ReadonlyArray<{\n label: string;\n value: DateFilterOperator;\n}> = [\n { label: \"Is\", value: \"equals\" },\n { label: \"Is not\", value: \"notEquals\" },\n { label: \"Is before\", value: \"before\" },\n { label: \"Is after\", value: \"after\" },\n { label: \"Is on or before\", value: \"onOrBefore\" },\n { label: \"Is on or after\", value: \"onOrAfter\" },\n { label: \"Is between\", value: \"isBetween\" },\n { label: \"Is empty\", value: \"isEmpty\" },\n { label: \"Is not empty\", value: \"isNotEmpty\" },\n];\n\nexport const SELECT_FILTER_OPERATORS: ReadonlyArray<{\n label: string;\n value: SelectFilterOperator;\n}> = [\n { label: \"Is\", value: \"is\" },\n { label: \"Is not\", value: \"isNot\" },\n { label: \"Has any of\", value: \"isAnyOf\" },\n { label: \"Has none of\", value: \"isNoneOf\" },\n { label: \"Is empty\", value: \"isEmpty\" },\n { label: \"Is not empty\", value: \"isNotEmpty\" },\n];\n\nexport const BOOLEAN_FILTER_OPERATORS: ReadonlyArray<{\n label: string;\n value: BooleanFilterOperator;\n}> = [\n { label: \"Is\", value: \"isTrue\" },\n { label: \"Is not\", value: \"isFalse\" },\n];\n\nexport function getDefaultOperator(variant: string): FilterOperator {\n switch (variant) {\n case \"number\":\n return \"equals\";\n case \"date\":\n return \"equals\";\n case \"select\":\n case \"multi-select\":\n return \"is\";\n case \"checkbox\":\n return \"isTrue\";\n default:\n return \"contains\";\n }\n}\n\nexport function getOperatorsForVariant(variant: string): ReadonlyArray<{\n label: string;\n value: FilterOperator;\n}> {\n switch (variant) {\n case \"number\":\n return NUMBER_FILTER_OPERATORS;\n case \"date\":\n return DATE_FILTER_OPERATORS;\n case \"select\":\n case \"multi-select\":\n return SELECT_FILTER_OPERATORS;\n case \"checkbox\":\n return BOOLEAN_FILTER_OPERATORS;\n default:\n return TEXT_FILTER_OPERATORS;\n }\n}\n\nexport function getFilterFn(): FilterFn<\n DataGridFeatures,\n TData\n> {\n return (\n row: Row,\n columnId: string,\n filterValue: unknown,\n ): boolean => {\n if (!filterValue || typeof filterValue !== \"object\") {\n return true;\n }\n\n const filter = filterValue as FilterValue;\n const { operator, value, endValue } = filter;\n\n const cellValue = row.getValue(columnId);\n\n if (operator === \"isEmpty\") {\n return (\n cellValue === null ||\n cellValue === undefined ||\n cellValue === \"\" ||\n (Array.isArray(cellValue) && cellValue.length === 0)\n );\n }\n\n if (operator === \"isNotEmpty\") {\n return !(\n cellValue === null ||\n cellValue === undefined ||\n cellValue === \"\" ||\n (Array.isArray(cellValue) && cellValue.length === 0)\n );\n }\n\n if (operator === \"isTrue\") {\n return cellValue === true;\n }\n\n if (operator === \"isFalse\") {\n return cellValue === false || !cellValue;\n }\n\n if (value === undefined || value === null || value === \"\") {\n return true;\n }\n\n const cellValueStr = stringifyUnknown(cellValue).toLowerCase();\n const filterValueStr = stringifyUnknown(value).toLowerCase();\n\n if (operator === \"contains\") {\n return cellValueStr.includes(filterValueStr);\n }\n\n if (operator === \"notContains\") {\n return !cellValueStr.includes(filterValueStr);\n }\n\n if (operator === \"equals\") {\n if (typeof cellValue === \"number\" && typeof value === \"number\") {\n return cellValue === value;\n }\n if (cellValue instanceof Date && typeof value === \"string\") {\n const cellDate = new Date(cellValue);\n const filterDate = new Date(value);\n return cellDate.toDateString() === filterDate.toDateString();\n }\n return cellValueStr === filterValueStr;\n }\n\n if (operator === \"notEquals\") {\n if (typeof cellValue === \"number\" && typeof value === \"number\") {\n return cellValue !== value;\n }\n if (cellValue instanceof Date && typeof value === \"string\") {\n const cellDate = new Date(cellValue);\n const filterDate = new Date(value);\n return cellDate.toDateString() !== filterDate.toDateString();\n }\n return cellValueStr !== filterValueStr;\n }\n\n if (operator === \"startsWith\") {\n return cellValueStr.startsWith(filterValueStr);\n }\n\n if (operator === \"endsWith\") {\n return cellValueStr.endsWith(filterValueStr);\n }\n\n if (typeof cellValue === \"number\" && typeof value === \"number\") {\n if (operator === \"greaterThan\") {\n return cellValue > value;\n }\n\n if (operator === \"greaterThanOrEqual\") {\n return cellValue >= value;\n }\n\n if (operator === \"lessThan\") {\n return cellValue < value;\n }\n\n if (operator === \"lessThanOrEqual\") {\n return cellValue <= value;\n }\n\n if (operator === \"isBetween\" && typeof endValue === \"number\") {\n return cellValue >= value && cellValue <= endValue;\n }\n }\n\n if (cellValue instanceof Date || typeof cellValue === \"string\") {\n const cellDate = new Date(cellValue);\n if (!Number.isNaN(cellDate.getTime()) && typeof value === \"string\") {\n const filterDate = new Date(value);\n\n if (operator === \"before\") {\n return cellDate < filterDate;\n }\n\n if (operator === \"after\") {\n return cellDate > filterDate;\n }\n\n if (operator === \"onOrBefore\") {\n return cellDate <= filterDate;\n }\n\n if (operator === \"onOrAfter\") {\n return cellDate >= filterDate;\n }\n\n if (operator === \"isBetween\" && typeof endValue === \"string\") {\n const filterDate2 = new Date(endValue);\n return cellDate >= filterDate && cellDate <= filterDate2;\n }\n }\n }\n\n if (operator === \"is\") {\n if (Array.isArray(cellValue)) {\n return cellValue.some(\n (v) => stringifyUnknown(v) === stringifyUnknown(value),\n );\n }\n return stringifyUnknown(cellValue) === stringifyUnknown(value);\n }\n\n if (operator === \"isNot\") {\n if (Array.isArray(cellValue)) {\n return !cellValue.some(\n (v) => stringifyUnknown(v) === stringifyUnknown(value),\n );\n }\n return stringifyUnknown(cellValue) !== stringifyUnknown(value);\n }\n\n if (operator === \"isAnyOf\" && Array.isArray(value)) {\n if (Array.isArray(cellValue)) {\n return cellValue.some((v) =>\n value.some((fv) => stringifyUnknown(v) === fv),\n );\n }\n return value.some((fv) => stringifyUnknown(cellValue) === fv);\n }\n\n if (operator === \"isNoneOf\" && Array.isArray(value)) {\n if (Array.isArray(cellValue)) {\n return !cellValue.some((v) =>\n value.some((fv) => stringifyUnknown(v) === fv),\n );\n }\n return !value.some((fv) => stringifyUnknown(cellValue) === fv);\n }\n\n return true;\n };\n}\n", - "type": "registry:lib", - "target": "src/lib/data-grid-filters.ts" - }, - { - "path": "src/lib/format.ts", - "content": "export function formatDate(\n date: Date | string | number | undefined,\n opts: Intl.DateTimeFormatOptions = {},\n) {\n if (!date) return \"\";\n\n try {\n return new Intl.DateTimeFormat(\"en-US\", {\n month: opts.month ?? \"long\",\n day: opts.day ?? \"numeric\",\n year: opts.year ?? \"numeric\",\n ...opts,\n }).format(new Date(date));\n } catch {\n return \"\";\n }\n}\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "content": "import type { Cell, RowData } from \"@tanstack/react-table\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\n\nexport type Direction = \"ltr\" | \"rtl\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n label: string;\n value: string;\n icon?: React.ComponentType>;\n count?: number;\n}\n\nexport type CellOpts =\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 | {\n variant: \"url\";\n }\n | {\n variant: \"file\";\n maxFileSize?: number;\n maxFiles?: number;\n accept?: string;\n multiple?: boolean;\n };\n\nexport interface CellUpdate {\n rowIndex: number;\n columnId: string;\n value: unknown;\n}\n\nexport interface DataGridColumnMeta {\n label?: string;\n cell?: CellOpts;\n}\n\nexport interface DataGridTableMeta {\n dataGridRef?: React.RefObject;\n cellMapRef?: React.RefObject>;\n focusedCell?: CellPosition | null;\n editingCell?: CellPosition | null;\n selectionState?: SelectionState;\n searchOpen?: boolean;\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getVisualRowIndex?: (rowId: string) => number | undefined;\n scrollToCell?: (\n rowIndex: number,\n columnId: string,\n align?: \"auto\" | \"start\" | \"center\" | \"end\",\n ) => void;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (value: RowHeightValue) => void;\n onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;\n onDataUpdate?: (params: CellUpdate | Array) => void;\n onRowsDelete?: (rowIndices: number[]) => void | Promise;\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?: (rowIndex: number, columnId: string) => 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 onCellsCopy?: () => void;\n onCellsCut?: () => void;\n onCellsPaste?: (expand?: boolean) => void;\n onSelectionClear?: () => void;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise;\n contextMenu?: ContextMenuState;\n onContextMenuOpenChange?: (open: boolean) => void;\n pasteDialog?: PasteDialogState;\n onPasteDialogOpenChange?: (open: boolean) => void;\n readOnly?: boolean;\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 interface PasteDialogState {\n open: boolean;\n rowsNeeded: number;\n clipboardText: string;\n}\n\nexport type NavigationDirection =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"home\"\n | \"end\"\n | \"ctrl+up\"\n | \"ctrl+down\"\n | \"ctrl+home\"\n | \"ctrl+end\"\n | \"pageup\"\n | \"pagedown\"\n | \"pageleft\"\n | \"pageright\";\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\nexport interface DataGridCellProps {\n cell: Cell;\n tableMeta: DataGridTableMeta;\n rowIndex: number;\n columnId: string;\n rowHeight: RowHeightValue;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n isSearchMatch: boolean;\n isActiveSearchMatch: boolean;\n readOnly: boolean;\n}\n\nexport interface FileCellData {\n id: string;\n name: string;\n size: number;\n type: string;\n url?: string;\n}\n\nexport type TextFilterOperator =\n | \"contains\"\n | \"notContains\"\n | \"equals\"\n | \"notEquals\"\n | \"startsWith\"\n | \"endsWith\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type NumberFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"lessThan\"\n | \"lessThanOrEqual\"\n | \"greaterThan\"\n | \"greaterThanOrEqual\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type DateFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"before\"\n | \"after\"\n | \"onOrBefore\"\n | \"onOrAfter\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type SelectFilterOperator =\n | \"is\"\n | \"isNot\"\n | \"isAnyOf\"\n | \"isNoneOf\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type BooleanFilterOperator = \"isTrue\" | \"isFalse\";\n\nexport type FilterOperator =\n | TextFilterOperator\n | NumberFilterOperator\n | DateFilterOperator\n | SelectFilterOperator\n | BooleanFilterOperator;\n\nexport interface FilterValue {\n operator: FilterOperator;\n value?: string | number | string[];\n endValue?: string | number;\n}\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-features.ts", - "content": "import {\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n createFilteredRowModel,\n createSortedRowModel,\n metaHelper,\n rowSelectionFeature,\n rowSortingFeature,\n tableFeatures,\n} from \"@tanstack/react-table\";\n\nimport type {\n DataGridColumnMeta,\n DataGridTableMeta,\n} from \"@/lib/data-grid-types\";\n\nexport const dataGridFeatures = tableFeatures({\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n rowSelectionFeature,\n rowSortingFeature,\n filteredRowModel: createFilteredRowModel(),\n sortedRowModel: createSortedRowModel(),\n tableMeta: metaHelper(),\n columnMeta: metaHelper(),\n});\n\nexport type DataGridFeatures = typeof dataGridFeatures;\n", - "type": "registry:lib" - }, - { - "path": "src/hooks/use-callback-ref.ts", - "content": "import * as React from \"react\";\n\n/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx\n */\n\n/**\n * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a\n * prop or avoid re-executing effects when passed as a dependency\n */\nfunction useCallbackRef unknown>(\n callback: T | undefined,\n): T {\n const callbackRef = React.useRef(callback);\n\n React.useEffect(() => {\n callbackRef.current = callback;\n });\n\n // https://github.com/facebook/react/issues/19240\n return React.useMemo(\n () => ((...args) => callbackRef.current?.(...args)) as T,\n [],\n );\n}\n\nexport { useCallbackRef };\n", - "type": "registry:hook" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/data-grid-keyboard-shortcuts.json b/public/r/data-grid-keyboard-shortcuts.json deleted file mode 100644 index aef704328..000000000 --- a/public/r/data-grid-keyboard-shortcuts.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-grid-keyboard-shortcuts", - "title": "Data Grid Keyboard Shortcuts", - "description": "A keyboard shortcuts dialog component for the data grid showing all available shortcuts", - "dependencies": [ - "lucide-react" - ], - "registryDependencies": [ - "button", - "dialog", - "input", - "kbd", - "separator" - ], - "files": [ - { - "path": "src/components/data-grid/data-grid-keyboard-shortcuts.tsx", - "content": "\"use client\";\n\nimport { SearchIcon, XIcon } from \"lucide-react\";\nimport * as React from \"react\";\n\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 { useDirection } from \"@/components/ui/direction\";\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 enableUndoRedo?: boolean;\n enablePaste?: boolean;\n enableRowAdd?: boolean;\n enableRowsDelete?: boolean;\n}\n\nexport const DataGridKeyboardShortcuts = React.memo(\n DataGridKeyboardShortcutsImpl,\n (prev, next) => {\n return (\n prev.enableSearch === next.enableSearch &&\n prev.enableUndoRedo === next.enableUndoRedo &&\n prev.enablePaste === next.enablePaste &&\n prev.enableRowAdd === next.enableRowAdd &&\n prev.enableRowsDelete === next.enableRowsDelete\n );\n },\n);\n\nfunction DataGridKeyboardShortcutsImpl({\n enableSearch = false,\n enableUndoRedo = false,\n enablePaste = false,\n enableRowAdd = false,\n enableRowsDelete = false,\n}: DataGridKeyboardShortcutsProps) {\n const dir = useDirection();\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, \"↑\"],\n description: \"Move to first row (same column)\",\n },\n {\n keys: [modKey, \"↓\"],\n description: \"Move to last row (same column)\",\n },\n {\n keys: [modKey, \"←\"],\n description: \"Move to first column (same row)\",\n },\n {\n keys: [modKey, \"→\"],\n description: \"Move to last column (same row)\",\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 keys: [\"⌥\", \"↑\"],\n description: \"Scroll up one page\",\n },\n {\n keys: [\"⌥\", \"↓\"],\n description: \"Scroll down one page\",\n },\n {\n keys: [\"⌥\", \"PgUp\"],\n description: \"Scroll left one page of columns\",\n },\n {\n keys: [\"⌥\", \"PgDn\"],\n description: \"Scroll right one page of columns\",\n },\n ],\n },\n {\n title: \"Selection\",\n shortcuts: [\n {\n keys: [\"Shift\", \"↑↓←→\"],\n description: \"Extend selection\",\n },\n {\n keys: [modKey, \"Shift\", \"↑\"],\n description: \"Select to top of table\",\n },\n {\n keys: [modKey, \"Shift\", \"↓\"],\n description: \"Select to bottom of table\",\n },\n {\n keys: [modKey, \"Shift\", \"←\"],\n description: \"Select to first column\",\n },\n {\n keys: [modKey, \"Shift\", \"→\"],\n description: \"Select to last column\",\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: [\"F2\"],\n description: \"Start editing cell\",\n },\n {\n keys: [\"Double Click\"],\n description: \"Start editing cell\",\n },\n ...(enableRowAdd\n ? [\n {\n keys: [\"Shift\", \"Enter\"],\n description: \"Insert row below\",\n },\n ]\n : []),\n {\n keys: [modKey, \"C\"],\n description: \"Copy selected cells\",\n },\n {\n keys: [modKey, \"X\"],\n description: \"Cut selected cells\",\n },\n ...(enablePaste\n ? [\n {\n keys: [modKey, \"V\"],\n description: \"Paste cells\",\n },\n ]\n : []),\n {\n keys: [\"Delete\"],\n description: \"Clear selected cells\",\n },\n {\n keys: [\"Backspace\"],\n description: \"Clear selected cells\",\n },\n ...(enableRowsDelete\n ? [\n {\n keys: [modKey, \"Backspace\"],\n description: \"Delete selected rows\",\n },\n {\n keys: [modKey, \"Delete\"],\n description: \"Delete selected rows\",\n },\n ]\n : []),\n ...(enableUndoRedo\n ? [\n {\n keys: [modKey, \"Z\"],\n description: \"Undo last action\",\n },\n {\n keys: [modKey, \"Shift\", \"Z\"],\n description: \"Redo last action\",\n },\n ]\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: \"Filtering\",\n shortcuts: [\n {\n keys: [modKey, \"Shift\", \"F\"],\n description: \"Toggle the filter menu\",\n },\n {\n keys: [\"Backspace\"],\n description: \"Remove filter (when focused)\",\n },\n {\n keys: [\"Delete\"],\n description: \"Remove filter (when focused)\",\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 [\n modKey,\n enableSearch,\n enableUndoRedo,\n enablePaste,\n enableRowAdd,\n enableRowsDelete,\n ],\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/ui/direction.tsx", - "content": "\"use client\";\n\nimport type * as React from \"react\";\n\nimport { Direction } from \"radix-ui\";\n\nfunction DirectionProvider({\n dir,\n direction,\n children,\n}: React.ComponentProps & {\n direction?: React.ComponentProps[\"dir\"];\n}) {\n return (\n \n {children}\n \n );\n}\n\nconst useDirection = Direction.useDirection;\n\nexport { DirectionProvider, useDirection };\n", - "type": "registry:ui", - "target": "src/components/ui/direction.tsx" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/data-grid-row-height-menu.json b/public/r/data-grid-row-height-menu.json deleted file mode 100644 index b93394f38..000000000 --- a/public/r/data-grid-row-height-menu.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-grid-row-height-menu", - "title": "Data Grid Row Height Menu", - "description": "A row height menu component for the data grid to adjust row sizes", - "dependencies": [ - "@tanstack/react-table", - "lucide-react" - ], - "registryDependencies": [ - "select" - ], - "files": [ - { - "path": "src/components/data-grid/data-grid-row-height-menu.tsx", - "content": "\"use client\";\n\nimport type { RowData, Table } from \"@tanstack/react-table\";\n\nimport {\n AlignVerticalSpaceAroundIcon,\n ChevronsDownUpIcon,\n EqualIcon,\n MinusIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\n\nimport {\n Select,\n SelectContent,\n SelectGroup,\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 TData extends RowData,\n> extends React.ComponentProps {\n table: Table;\n disabled?: boolean;\n}\n\nexport function DataGridRowHeightMenu({\n table,\n disabled,\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 \n {selectedRowHeight.label}\n \n \n \n \n {rowHeights.map((option) => (\n \n \n {option.label}\n \n ))}\n \n \n \n );\n}\n", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-row-height-menu.tsx" - }, - { - "path": "src/lib/data-grid-features.ts", - "content": "import {\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n createFilteredRowModel,\n createSortedRowModel,\n metaHelper,\n rowSelectionFeature,\n rowSortingFeature,\n tableFeatures,\n} from \"@tanstack/react-table\";\n\nimport type {\n DataGridColumnMeta,\n DataGridTableMeta,\n} from \"@/lib/data-grid-types\";\n\nexport const dataGridFeatures = tableFeatures({\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n rowSelectionFeature,\n rowSortingFeature,\n filteredRowModel: createFilteredRowModel(),\n sortedRowModel: createSortedRowModel(),\n tableMeta: metaHelper(),\n columnMeta: metaHelper(),\n});\n\nexport type DataGridFeatures = typeof dataGridFeatures;\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "content": "import type { Cell, RowData } from \"@tanstack/react-table\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\n\nexport type Direction = \"ltr\" | \"rtl\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n label: string;\n value: string;\n icon?: React.ComponentType>;\n count?: number;\n}\n\nexport type CellOpts =\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 | {\n variant: \"url\";\n }\n | {\n variant: \"file\";\n maxFileSize?: number;\n maxFiles?: number;\n accept?: string;\n multiple?: boolean;\n };\n\nexport interface CellUpdate {\n rowIndex: number;\n columnId: string;\n value: unknown;\n}\n\nexport interface DataGridColumnMeta {\n label?: string;\n cell?: CellOpts;\n}\n\nexport interface DataGridTableMeta {\n dataGridRef?: React.RefObject;\n cellMapRef?: React.RefObject>;\n focusedCell?: CellPosition | null;\n editingCell?: CellPosition | null;\n selectionState?: SelectionState;\n searchOpen?: boolean;\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getVisualRowIndex?: (rowId: string) => number | undefined;\n scrollToCell?: (\n rowIndex: number,\n columnId: string,\n align?: \"auto\" | \"start\" | \"center\" | \"end\",\n ) => void;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (value: RowHeightValue) => void;\n onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;\n onDataUpdate?: (params: CellUpdate | Array) => void;\n onRowsDelete?: (rowIndices: number[]) => void | Promise;\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?: (rowIndex: number, columnId: string) => 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 onCellsCopy?: () => void;\n onCellsCut?: () => void;\n onCellsPaste?: (expand?: boolean) => void;\n onSelectionClear?: () => void;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise;\n contextMenu?: ContextMenuState;\n onContextMenuOpenChange?: (open: boolean) => void;\n pasteDialog?: PasteDialogState;\n onPasteDialogOpenChange?: (open: boolean) => void;\n readOnly?: boolean;\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 interface PasteDialogState {\n open: boolean;\n rowsNeeded: number;\n clipboardText: string;\n}\n\nexport type NavigationDirection =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"home\"\n | \"end\"\n | \"ctrl+up\"\n | \"ctrl+down\"\n | \"ctrl+home\"\n | \"ctrl+end\"\n | \"pageup\"\n | \"pagedown\"\n | \"pageleft\"\n | \"pageright\";\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\nexport interface DataGridCellProps {\n cell: Cell;\n tableMeta: DataGridTableMeta;\n rowIndex: number;\n columnId: string;\n rowHeight: RowHeightValue;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n isSearchMatch: boolean;\n isActiveSearchMatch: boolean;\n readOnly: boolean;\n}\n\nexport interface FileCellData {\n id: string;\n name: string;\n size: number;\n type: string;\n url?: string;\n}\n\nexport type TextFilterOperator =\n | \"contains\"\n | \"notContains\"\n | \"equals\"\n | \"notEquals\"\n | \"startsWith\"\n | \"endsWith\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type NumberFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"lessThan\"\n | \"lessThanOrEqual\"\n | \"greaterThan\"\n | \"greaterThanOrEqual\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type DateFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"before\"\n | \"after\"\n | \"onOrBefore\"\n | \"onOrAfter\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type SelectFilterOperator =\n | \"is\"\n | \"isNot\"\n | \"isAnyOf\"\n | \"isNoneOf\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type BooleanFilterOperator = \"isTrue\" | \"isFalse\";\n\nexport type FilterOperator =\n | TextFilterOperator\n | NumberFilterOperator\n | DateFilterOperator\n | SelectFilterOperator\n | BooleanFilterOperator;\n\nexport interface FilterValue {\n operator: FilterOperator;\n value?: string | number | string[];\n endValue?: string | number;\n}\n", - "type": "registry:lib" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/data-grid-select-column.json b/public/r/data-grid-select-column.json deleted file mode 100644 index e6ec76b93..000000000 --- a/public/r/data-grid-select-column.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-grid-select-column", - "title": "Data Grid Select Column", - "description": "A reusable select column definition for the data grid with checkbox selection and shift-click support", - "dependencies": [ - "@tanstack/react-table" - ], - "registryDependencies": [ - "checkbox" - ], - "files": [ - { - "path": "src/components/data-grid/data-grid-select-column.tsx", - "content": "\"use client\";\n\nimport type {\n CellContext,\n ColumnDef,\n HeaderContext,\n RowData,\n} from \"@tanstack/react-table\";\n\nimport * as React from \"react\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\n\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { cn } from \"@/lib/utils\";\n\ntype HitboxSize = \"default\" | \"sm\" | \"lg\";\n\ninterface DataGridSelectHitboxProps {\n htmlFor: string;\n children: React.ReactNode;\n size?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectHitbox({\n htmlFor,\n children,\n size,\n debug,\n}: DataGridSelectHitboxProps) {\n return (\n \n {children}\n \n
\n );\n}\n\ninterface DataGridSelectCheckboxProps extends Omit<\n React.ComponentProps,\n \"id\"\n> {\n rowNumber?: number;\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nfunction DataGridSelectCheckbox({\n rowNumber,\n hitboxSize,\n debug,\n checked,\n className,\n ...props\n}: DataGridSelectCheckboxProps) {\n const id = React.useId();\n\n if (rowNumber !== undefined) {\n return (\n \n \n {rowNumber}\n
\n \n \n );\n }\n\n return (\n \n \n \n );\n}\n\ninterface DataGridSelectHeaderProps extends Pick<\n HeaderContext,\n \"table\"\n> {\n hitboxSize?: HitboxSize;\n readOnly?: boolean;\n debug?: boolean;\n}\n\nfunction DataGridSelectHeader({\n table,\n hitboxSize,\n readOnly,\n debug,\n}: DataGridSelectHeaderProps) {\n const onCheckedChange = React.useCallback(\n (value: boolean) => table.toggleAllPageRowsSelected(value),\n [table],\n );\n\n if (readOnly) {\n return (\n
\n #\n
\n );\n }\n\n return (\n \n );\n}\n\ninterface DataGridSelectCellProps extends Pick<\n CellContext,\n \"row\" | \"table\"\n> {\n hitboxSize?: HitboxSize;\n enableRowMarkers?: boolean;\n readOnly?: boolean;\n debug?: boolean;\n}\n\nfunction DataGridSelectCell({\n row,\n table,\n hitboxSize,\n enableRowMarkers,\n readOnly,\n debug,\n}: DataGridSelectCellProps) {\n const meta = table.options.meta;\n const rowNumber = enableRowMarkers\n ? (meta?.getVisualRowIndex?.(row.id) ?? row.index + 1)\n : undefined;\n\n const onCheckedChange = React.useCallback(\n (value: boolean) => {\n if (meta?.onRowSelect) {\n meta.onRowSelect(row.id, value, false);\n } else {\n row.toggleSelected(value);\n }\n },\n [meta, row],\n );\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (event.shiftKey) {\n event.preventDefault();\n meta?.onRowSelect?.(row.id, !row.getIsSelected(), true);\n }\n },\n [meta, row],\n );\n\n if (readOnly) {\n return (\n
\n {rowNumber ?? row.index + 1}\n
\n );\n }\n\n return (\n \n );\n}\n\ninterface GetDataGridSelectColumnOptions extends Omit<\n Partial>,\n \"id\" | \"header\" | \"cell\"\n> {\n enableRowMarkers?: boolean;\n readOnly?: boolean;\n hitboxSize?: HitboxSize;\n debug?: boolean;\n}\n\nexport function getDataGridSelectColumn({\n size = 40,\n hitboxSize = \"default\",\n enableHiding = false,\n enableResizing = false,\n enableSorting = false,\n enableRowMarkers = false,\n readOnly = false,\n debug = false,\n ...props\n}: GetDataGridSelectColumnOptions = {}): ColumnDef<\n DataGridFeatures,\n TData\n> {\n return {\n id: \"select\",\n header: ({ table }) => (\n \n ),\n cell: ({ row, table }) => (\n \n ),\n size,\n enableHiding,\n enableResizing,\n enableSorting,\n ...props,\n };\n}\n", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-select-column.tsx" - }, - { - "path": "src/lib/data-grid-features.ts", - "content": "import {\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n createFilteredRowModel,\n createSortedRowModel,\n metaHelper,\n rowSelectionFeature,\n rowSortingFeature,\n tableFeatures,\n} from \"@tanstack/react-table\";\n\nimport type {\n DataGridColumnMeta,\n DataGridTableMeta,\n} from \"@/lib/data-grid-types\";\n\nexport const dataGridFeatures = tableFeatures({\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n rowSelectionFeature,\n rowSortingFeature,\n filteredRowModel: createFilteredRowModel(),\n sortedRowModel: createSortedRowModel(),\n tableMeta: metaHelper(),\n columnMeta: metaHelper(),\n});\n\nexport type DataGridFeatures = typeof dataGridFeatures;\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "content": "import type { Cell, RowData } from \"@tanstack/react-table\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\n\nexport type Direction = \"ltr\" | \"rtl\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n label: string;\n value: string;\n icon?: React.ComponentType>;\n count?: number;\n}\n\nexport type CellOpts =\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 | {\n variant: \"url\";\n }\n | {\n variant: \"file\";\n maxFileSize?: number;\n maxFiles?: number;\n accept?: string;\n multiple?: boolean;\n };\n\nexport interface CellUpdate {\n rowIndex: number;\n columnId: string;\n value: unknown;\n}\n\nexport interface DataGridColumnMeta {\n label?: string;\n cell?: CellOpts;\n}\n\nexport interface DataGridTableMeta {\n dataGridRef?: React.RefObject;\n cellMapRef?: React.RefObject>;\n focusedCell?: CellPosition | null;\n editingCell?: CellPosition | null;\n selectionState?: SelectionState;\n searchOpen?: boolean;\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getVisualRowIndex?: (rowId: string) => number | undefined;\n scrollToCell?: (\n rowIndex: number,\n columnId: string,\n align?: \"auto\" | \"start\" | \"center\" | \"end\",\n ) => void;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (value: RowHeightValue) => void;\n onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;\n onDataUpdate?: (params: CellUpdate | Array) => void;\n onRowsDelete?: (rowIndices: number[]) => void | Promise;\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?: (rowIndex: number, columnId: string) => 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 onCellsCopy?: () => void;\n onCellsCut?: () => void;\n onCellsPaste?: (expand?: boolean) => void;\n onSelectionClear?: () => void;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise;\n contextMenu?: ContextMenuState;\n onContextMenuOpenChange?: (open: boolean) => void;\n pasteDialog?: PasteDialogState;\n onPasteDialogOpenChange?: (open: boolean) => void;\n readOnly?: boolean;\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 interface PasteDialogState {\n open: boolean;\n rowsNeeded: number;\n clipboardText: string;\n}\n\nexport type NavigationDirection =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"home\"\n | \"end\"\n | \"ctrl+up\"\n | \"ctrl+down\"\n | \"ctrl+home\"\n | \"ctrl+end\"\n | \"pageup\"\n | \"pagedown\"\n | \"pageleft\"\n | \"pageright\";\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\nexport interface DataGridCellProps {\n cell: Cell;\n tableMeta: DataGridTableMeta;\n rowIndex: number;\n columnId: string;\n rowHeight: RowHeightValue;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n isSearchMatch: boolean;\n isActiveSearchMatch: boolean;\n readOnly: boolean;\n}\n\nexport interface FileCellData {\n id: string;\n name: string;\n size: number;\n type: string;\n url?: string;\n}\n\nexport type TextFilterOperator =\n | \"contains\"\n | \"notContains\"\n | \"equals\"\n | \"notEquals\"\n | \"startsWith\"\n | \"endsWith\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type NumberFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"lessThan\"\n | \"lessThanOrEqual\"\n | \"greaterThan\"\n | \"greaterThanOrEqual\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type DateFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"before\"\n | \"after\"\n | \"onOrBefore\"\n | \"onOrAfter\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type SelectFilterOperator =\n | \"is\"\n | \"isNot\"\n | \"isAnyOf\"\n | \"isNoneOf\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type BooleanFilterOperator = \"isTrue\" | \"isFalse\";\n\nexport type FilterOperator =\n | TextFilterOperator\n | NumberFilterOperator\n | DateFilterOperator\n | SelectFilterOperator\n | BooleanFilterOperator;\n\nexport interface FilterValue {\n operator: FilterOperator;\n value?: string | number | string[];\n endValue?: string | number;\n}\n", - "type": "registry:lib" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/data-grid-skeleton.json b/public/r/data-grid-skeleton.json deleted file mode 100644 index d24f18344..000000000 --- a/public/r/data-grid-skeleton.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-grid-skeleton", - "title": "Data Grid Skeleton", - "description": "A composable skeleton component for the data grid with toolbar and grid parts", - "dependencies": [], - "registryDependencies": [ - "skeleton" - ], - "files": [ - { - "path": "src/components/data-grid/data-grid-skeleton.tsx", - "content": "import type * as React from \"react\";\n\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {}\n\nfunction DataGridSkeleton({ className, ...props }: DivProps) {\n return (\n [data-slot=grid-skeleton-toolbar]]:h-[calc(100dvh-(--spacing(20)))]\",\n className,\n )}\n {...props}\n />\n );\n}\n\ninterface DataGridSkeletonToolbarProps extends DivProps {\n align?: \"start\" | \"center\" | \"end\";\n actionCount?: number;\n}\n\nfunction DataGridSkeletonToolbar({\n align = \"end\",\n actionCount = 4,\n className,\n ...props\n}: DataGridSkeletonToolbarProps) {\n return (\n \n {Array.from({ length: actionCount }).map((_, i) => (\n \n ))}\n
\n );\n}\n\nfunction DataGridSkeletonGrid({ className, ...props }: DivProps) {\n return (\n \n );\n}\n\nexport { DataGridSkeleton, DataGridSkeletonGrid, DataGridSkeletonToolbar };\n", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-skeleton.tsx" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/data-grid-sort-menu.json b/public/r/data-grid-sort-menu.json deleted file mode 100644 index 09d0b4b6f..000000000 --- a/public/r/data-grid-sort-menu.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-grid-sort-menu", - "title": "Data Grid Sort Menu", - "description": "A sort menu component for the data grid with drag-and-drop reordering", - "dependencies": [ - "@tanstack/react-table", - "lucide-react" - ], - "registryDependencies": [ - "badge", - "button", - "command", - "popover", - "select" - ], - "files": [ - { - "path": "src/components/data-grid/data-grid-sort-menu.tsx", - "content": "\"use client\";\n\nimport type {\n ColumnSort,\n RowData,\n SortDirection,\n Table,\n} from \"@tanstack/react-table\";\n\nimport {\n ArrowDownUp,\n ChevronsUpDown,\n GripVertical,\n Trash2,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\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 { useDirection } from \"@/components/ui/direction\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\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 = new Set([\"backspace\", \"delete\"]);\nconst SORT_ORDERS = [\n { label: \"Asc\", value: \"asc\" },\n { label: \"Desc\", value: \"desc\" },\n];\n\ninterface DataGridSortMenuProps<\n TData extends RowData,\n> extends React.ComponentProps {\n table: Table;\n disabled?: boolean;\n}\n\nexport function DataGridSortMenu({\n table,\n disabled,\n className,\n ...props\n}: DataGridSortMenuProps) {\n const dir = useDirection();\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.store.state.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.has(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 dir: \"ltr\" | \"rtl\";\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 dir,\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.has(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 \n {SORT_ORDERS.map((order) => (\n \n {order.label}\n \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/direction.tsx", - "content": "\"use client\";\n\nimport type * as React from \"react\";\n\nimport { Direction } from \"radix-ui\";\n\nfunction DirectionProvider({\n dir,\n direction,\n children,\n}: React.ComponentProps & {\n direction?: React.ComponentProps[\"dir\"];\n}) {\n return (\n \n {children}\n \n );\n}\n\nconst useDirection = Direction.useDirection;\n\nexport { DirectionProvider, useDirection };\n", - "type": "registry:ui", - "target": "src/components/ui/direction.tsx" - }, - { - "path": "src/components/ui/sortable.tsx", - "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 as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\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 SortableProps = 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 Sortable(props: SortableProps) {\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 ...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(\n \"`getItemValue` is required when using array of objects\",\n );\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 sortableProps.onDragStart?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(event.active.id);\n },\n [sortableProps.onDragStart],\n );\n\n const onDragEnd = React.useCallback(\n (event: DragEndEvent) => {\n sortableProps.onDragEnd?.(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, sortableProps.onDragEnd],\n );\n\n const onDragCancel = React.useCallback(\n (event: DragEndEvent) => {\n sortableProps.onDragCancel?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(null);\n },\n [sortableProps.onDragCancel],\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 ? SlotPrimitive.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 ? SlotPrimitive.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 ? SlotPrimitive.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 extends Omit<\n React.ComponentProps,\n \"children\"\n> {\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\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 Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n type SortableProps,\n};\n", - "type": "registry:ui", - "target": "src/components/ui/sortable.tsx" - }, - { - "path": "src/lib/compose-refs.ts", - "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef = React.Ref | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef(ref: PossibleRef, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n }\n\n if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs(...refs: PossibleRef[]): React.RefCallback {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup === \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup === \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs(...refs: PossibleRef[]): React.RefCallback {\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-features.ts", - "content": "import {\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n createFilteredRowModel,\n createSortedRowModel,\n metaHelper,\n rowSelectionFeature,\n rowSortingFeature,\n tableFeatures,\n} from \"@tanstack/react-table\";\n\nimport type {\n DataGridColumnMeta,\n DataGridTableMeta,\n} from \"@/lib/data-grid-types\";\n\nexport const dataGridFeatures = tableFeatures({\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n rowSelectionFeature,\n rowSortingFeature,\n filteredRowModel: createFilteredRowModel(),\n sortedRowModel: createSortedRowModel(),\n tableMeta: metaHelper(),\n columnMeta: metaHelper(),\n});\n\nexport type DataGridFeatures = typeof dataGridFeatures;\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "content": "import type { Cell, RowData } from \"@tanstack/react-table\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\n\nexport type Direction = \"ltr\" | \"rtl\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n label: string;\n value: string;\n icon?: React.ComponentType>;\n count?: number;\n}\n\nexport type CellOpts =\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 | {\n variant: \"url\";\n }\n | {\n variant: \"file\";\n maxFileSize?: number;\n maxFiles?: number;\n accept?: string;\n multiple?: boolean;\n };\n\nexport interface CellUpdate {\n rowIndex: number;\n columnId: string;\n value: unknown;\n}\n\nexport interface DataGridColumnMeta {\n label?: string;\n cell?: CellOpts;\n}\n\nexport interface DataGridTableMeta {\n dataGridRef?: React.RefObject;\n cellMapRef?: React.RefObject>;\n focusedCell?: CellPosition | null;\n editingCell?: CellPosition | null;\n selectionState?: SelectionState;\n searchOpen?: boolean;\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getVisualRowIndex?: (rowId: string) => number | undefined;\n scrollToCell?: (\n rowIndex: number,\n columnId: string,\n align?: \"auto\" | \"start\" | \"center\" | \"end\",\n ) => void;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (value: RowHeightValue) => void;\n onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;\n onDataUpdate?: (params: CellUpdate | Array) => void;\n onRowsDelete?: (rowIndices: number[]) => void | Promise;\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?: (rowIndex: number, columnId: string) => 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 onCellsCopy?: () => void;\n onCellsCut?: () => void;\n onCellsPaste?: (expand?: boolean) => void;\n onSelectionClear?: () => void;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise;\n contextMenu?: ContextMenuState;\n onContextMenuOpenChange?: (open: boolean) => void;\n pasteDialog?: PasteDialogState;\n onPasteDialogOpenChange?: (open: boolean) => void;\n readOnly?: boolean;\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 interface PasteDialogState {\n open: boolean;\n rowsNeeded: number;\n clipboardText: string;\n}\n\nexport type NavigationDirection =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"home\"\n | \"end\"\n | \"ctrl+up\"\n | \"ctrl+down\"\n | \"ctrl+home\"\n | \"ctrl+end\"\n | \"pageup\"\n | \"pagedown\"\n | \"pageleft\"\n | \"pageright\";\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\nexport interface DataGridCellProps {\n cell: Cell;\n tableMeta: DataGridTableMeta;\n rowIndex: number;\n columnId: string;\n rowHeight: RowHeightValue;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n isSearchMatch: boolean;\n isActiveSearchMatch: boolean;\n readOnly: boolean;\n}\n\nexport interface FileCellData {\n id: string;\n name: string;\n size: number;\n type: string;\n url?: string;\n}\n\nexport type TextFilterOperator =\n | \"contains\"\n | \"notContains\"\n | \"equals\"\n | \"notEquals\"\n | \"startsWith\"\n | \"endsWith\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type NumberFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"lessThan\"\n | \"lessThanOrEqual\"\n | \"greaterThan\"\n | \"greaterThanOrEqual\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type DateFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"before\"\n | \"after\"\n | \"onOrBefore\"\n | \"onOrAfter\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type SelectFilterOperator =\n | \"is\"\n | \"isNot\"\n | \"isAnyOf\"\n | \"isNoneOf\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type BooleanFilterOperator = \"isTrue\" | \"isFalse\";\n\nexport type FilterOperator =\n | TextFilterOperator\n | NumberFilterOperator\n | DateFilterOperator\n | SelectFilterOperator\n | BooleanFilterOperator;\n\nexport interface FilterValue {\n operator: FilterOperator;\n value?: string | number | string[];\n endValue?: string | number;\n}\n", - "type": "registry:lib" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/data-grid-view-menu.json b/public/r/data-grid-view-menu.json deleted file mode 100644 index bf1ee61ac..000000000 --- a/public/r/data-grid-view-menu.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-grid-view-menu", - "title": "Data Grid View Menu", - "description": "A view menu component for the data grid to toggle column visibility", - "dependencies": [ - "@tanstack/react-table", - "lucide-react" - ], - "registryDependencies": [ - "button", - "command", - "popover" - ], - "files": [ - { - "path": "src/components/data-grid/data-grid-view-menu.tsx", - "content": "\"use client\";\n\nimport type { RowData, Table } from \"@tanstack/react-table\";\n\nimport { Settings2 } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\n\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 { useDirection } from \"@/components/ui/direction\";\nimport {\n Popover,\n PopoverContent,\n PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataGridViewMenuProps<\n TData extends RowData,\n> extends React.ComponentProps {\n table: Table;\n disabled?: boolean;\n}\n\nexport function DataGridViewMenu({\n table,\n disabled,\n className,\n ...props\n}: DataGridViewMenuProps) {\n const dir = useDirection();\n\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", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-view-menu.tsx" - }, - { - "path": "src/components/ui/direction.tsx", - "content": "\"use client\";\n\nimport type * as React from \"react\";\n\nimport { Direction } from \"radix-ui\";\n\nfunction DirectionProvider({\n dir,\n direction,\n children,\n}: React.ComponentProps & {\n direction?: React.ComponentProps[\"dir\"];\n}) {\n return (\n \n {children}\n \n );\n}\n\nconst useDirection = Direction.useDirection;\n\nexport { DirectionProvider, useDirection };\n", - "type": "registry:ui", - "target": "src/components/ui/direction.tsx" - }, - { - "path": "src/lib/data-grid-features.ts", - "content": "import {\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n createFilteredRowModel,\n createSortedRowModel,\n metaHelper,\n rowSelectionFeature,\n rowSortingFeature,\n tableFeatures,\n} from \"@tanstack/react-table\";\n\nimport type {\n DataGridColumnMeta,\n DataGridTableMeta,\n} from \"@/lib/data-grid-types\";\n\nexport const dataGridFeatures = tableFeatures({\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n rowSelectionFeature,\n rowSortingFeature,\n filteredRowModel: createFilteredRowModel(),\n sortedRowModel: createSortedRowModel(),\n tableMeta: metaHelper(),\n columnMeta: metaHelper(),\n});\n\nexport type DataGridFeatures = typeof dataGridFeatures;\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "content": "import type { Cell, RowData } from \"@tanstack/react-table\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\n\nexport type Direction = \"ltr\" | \"rtl\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n label: string;\n value: string;\n icon?: React.ComponentType>;\n count?: number;\n}\n\nexport type CellOpts =\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 | {\n variant: \"url\";\n }\n | {\n variant: \"file\";\n maxFileSize?: number;\n maxFiles?: number;\n accept?: string;\n multiple?: boolean;\n };\n\nexport interface CellUpdate {\n rowIndex: number;\n columnId: string;\n value: unknown;\n}\n\nexport interface DataGridColumnMeta {\n label?: string;\n cell?: CellOpts;\n}\n\nexport interface DataGridTableMeta {\n dataGridRef?: React.RefObject;\n cellMapRef?: React.RefObject>;\n focusedCell?: CellPosition | null;\n editingCell?: CellPosition | null;\n selectionState?: SelectionState;\n searchOpen?: boolean;\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getVisualRowIndex?: (rowId: string) => number | undefined;\n scrollToCell?: (\n rowIndex: number,\n columnId: string,\n align?: \"auto\" | \"start\" | \"center\" | \"end\",\n ) => void;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (value: RowHeightValue) => void;\n onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;\n onDataUpdate?: (params: CellUpdate | Array) => void;\n onRowsDelete?: (rowIndices: number[]) => void | Promise;\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?: (rowIndex: number, columnId: string) => 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 onCellsCopy?: () => void;\n onCellsCut?: () => void;\n onCellsPaste?: (expand?: boolean) => void;\n onSelectionClear?: () => void;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise;\n contextMenu?: ContextMenuState;\n onContextMenuOpenChange?: (open: boolean) => void;\n pasteDialog?: PasteDialogState;\n onPasteDialogOpenChange?: (open: boolean) => void;\n readOnly?: boolean;\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 interface PasteDialogState {\n open: boolean;\n rowsNeeded: number;\n clipboardText: string;\n}\n\nexport type NavigationDirection =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"home\"\n | \"end\"\n | \"ctrl+up\"\n | \"ctrl+down\"\n | \"ctrl+home\"\n | \"ctrl+end\"\n | \"pageup\"\n | \"pagedown\"\n | \"pageleft\"\n | \"pageright\";\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\nexport interface DataGridCellProps {\n cell: Cell;\n tableMeta: DataGridTableMeta;\n rowIndex: number;\n columnId: string;\n rowHeight: RowHeightValue;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n isSearchMatch: boolean;\n isActiveSearchMatch: boolean;\n readOnly: boolean;\n}\n\nexport interface FileCellData {\n id: string;\n name: string;\n size: number;\n type: string;\n url?: string;\n}\n\nexport type TextFilterOperator =\n | \"contains\"\n | \"notContains\"\n | \"equals\"\n | \"notEquals\"\n | \"startsWith\"\n | \"endsWith\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type NumberFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"lessThan\"\n | \"lessThanOrEqual\"\n | \"greaterThan\"\n | \"greaterThanOrEqual\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type DateFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"before\"\n | \"after\"\n | \"onOrBefore\"\n | \"onOrAfter\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type SelectFilterOperator =\n | \"is\"\n | \"isNot\"\n | \"isAnyOf\"\n | \"isNoneOf\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type BooleanFilterOperator = \"isTrue\" | \"isFalse\";\n\nexport type FilterOperator =\n | TextFilterOperator\n | NumberFilterOperator\n | DateFilterOperator\n | SelectFilterOperator\n | BooleanFilterOperator;\n\nexport interface FilterValue {\n operator: FilterOperator;\n value?: string | number | string[];\n endValue?: string | number;\n}\n", - "type": "registry:lib" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/data-grid.json b/public/r/data-grid.json deleted file mode 100644 index ece81e4be..000000000 --- a/public/r/data-grid.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-grid", - "title": "Data Grid", - "description": "A high-performance editable data grid component with virtualization, keyboard navigation, and cell editing", - "dependencies": [ - "@tanstack/react-table", - "@tanstack/react-virtual", - "lucide-react", - "sonner" - ], - "registryDependencies": [ - "badge", - "button", - "calendar", - "checkbox", - "command", - "dialog", - "dropdown-menu", - "input", - "popover", - "select", - "separator", - "skeleton", - "textarea", - "tooltip" - ], - "files": [ - { - "path": "src/components/data-grid/data-grid.tsx", - "content": "\"use client\";\n\nimport type { RowData } from \"@tanstack/react-table\";\n\nimport { Plus } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { useDataGrid } from \"@/hooks/use-data-grid\";\nimport type { Direction } from \"@/lib/data-grid-types\";\n\nimport { DataGridColumnHeader } from \"@/components/data-grid/data-grid-column-header\";\nimport { DataGridContextMenu } from \"@/components/data-grid/data-grid-context-menu\";\nimport { DataGridPasteDialog } from \"@/components/data-grid/data-grid-paste-dialog\";\nimport { DataGridRow } from \"@/components/data-grid/data-grid-row\";\nimport { DataGridSearch } from \"@/components/data-grid/data-grid-search\";\nimport { useAsRef } from \"@/hooks/use-as-ref\";\nimport {\n flexRender,\n getColumnBorderVisibility,\n getColumnPinningStyle,\n} from \"@/lib/data-grid-utils\";\nimport { cn } from \"@/lib/utils\";\n\nconst EMPTY_CELL_SELECTION_SET = new Set();\n\ninterface DataGridProps\n extends\n Omit>, \"dir\">,\n Omit, \"contextMenu\"> {\n dir?: Direction;\n height?: number;\n stretchColumns?: boolean;\n}\n\nexport function DataGrid({\n dataGridRef,\n headerRef,\n rowMapRef,\n footerRef,\n dir = \"ltr\",\n table,\n tableMeta,\n virtualTotalSize,\n virtualItems,\n measureElement,\n columns,\n columnSizeVars,\n searchState,\n searchMatchesByRow,\n activeSearchMatch,\n cellSelectionMap,\n focusedCell,\n editingCell,\n rowHeight,\n contextMenu,\n pasteDialog,\n onRowAdd: onRowAddProp,\n height = 600,\n stretchColumns = false,\n adjustLayout,\n className,\n ...props\n}: DataGridProps) {\n const rows = table.getRowModel().rows;\n const readOnly = tableMeta?.readOnly ?? false;\n const columnVisibility = table.state.columnVisibility;\n const columnPinning = table.state.columnPinning;\n\n const onRowAddRef = useAsRef(onRowAddProp);\n\n const onRowAdd = React.useCallback(\n (event: React.MouseEvent) => {\n void onRowAddRef.current?.(event);\n },\n [onRowAddRef],\n );\n\n const onDataGridContextMenu = React.useCallback(\n (event: React.MouseEvent) => {\n event.preventDefault();\n },\n [],\n );\n\n const onFooterCellKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (!onRowAddRef.current) return;\n\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n void onRowAddRef.current();\n }\n },\n [onRowAddRef],\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.state.sorting;\n const currentSort = sorting.find(\n (sort) => sort.id === header.column.id,\n );\n const isSortable = header.column.getCanSort();\n\n const nextHeader = headerGroup.headers[colIndex + 1];\n const isLastColumn =\n colIndex === headerGroup.headers.length - 1;\n\n const { showEndBorder, showStartBorder } =\n getColumnBorderVisibility({\n column: header.column,\n nextColumn: nextHeader?.column,\n isLastColumn,\n });\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 {virtualItems.map((virtualItem) => {\n const row = rows[virtualItem.index];\n if (!row) return null;\n\n const cellSelectionKeys =\n cellSelectionMap?.get(virtualItem.index) ??\n EMPTY_CELL_SELECTION_SET;\n\n const searchMatchColumns =\n searchMatchesByRow?.get(virtualItem.index) ?? null;\n const isActiveSearchRow =\n activeSearchMatch?.rowIndex === virtualItem.index;\n\n return (\n \n );\n })}\n \n {!readOnly && 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-presence.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\n\ninterface DataGridCellPresence {\n color: string;\n name: string;\n}\n\nconst DataGridCellPresenceContext = React.createContext | null>(null);\n\ninterface DataGridPresenceProviderProps {\n value: Map;\n children: React.ReactNode;\n}\n\nfunction DataGridPresenceProvider({\n value,\n children,\n}: DataGridPresenceProviderProps) {\n return (\n \n {children}\n \n );\n}\n\nfunction useDataGridPresence(cellKey: string): DataGridCellPresence | null {\n const map = React.useContext(DataGridCellPresenceContext);\n return map?.get(cellKey) ?? null;\n}\n\nexport {\n type DataGridCellPresence,\n DataGridPresenceProvider,\n useDataGridPresence,\n};\n", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-presence.tsx" - }, - { - "path": "src/components/data-grid/data-grid-cell.tsx", - "content": "\"use client\";\n\nimport type { RowData } from \"@tanstack/react-table\";\n\nimport * as React from \"react\";\n\nimport type { DataGridCellProps } from \"@/lib/data-grid-types\";\n\nimport {\n CheckboxCell,\n DateCell,\n FileCell,\n LongTextCell,\n MultiSelectCell,\n NumberCell,\n SelectCell,\n ShortTextCell,\n UrlCell,\n} from \"@/components/data-grid/data-grid-cell-variants\";\n\nexport const DataGridCell = React.memo(DataGridCellImpl, (prev, next) => {\n // Fast path: check stable primitive props first\n if (prev.isFocused !== next.isFocused) return false;\n if (prev.isEditing !== next.isEditing) return false;\n if (prev.isSelected !== next.isSelected) return false;\n if (prev.isSearchMatch !== next.isSearchMatch) return false;\n if (prev.isActiveSearchMatch !== next.isActiveSearchMatch) return false;\n if (prev.readOnly !== next.readOnly) return false;\n if (prev.rowIndex !== next.rowIndex) return false;\n if (prev.columnId !== next.columnId) return false;\n if (prev.rowHeight !== next.rowHeight) return false;\n\n // Check cell value using row.original instead of getValue() for stability\n // getValue() is unstable and recreates on every render, breaking memoization\n const prevValue = (prev.cell.row.original as Record)[\n prev.columnId\n ];\n const nextValue = (next.cell.row.original as Record)[\n next.columnId\n ];\n if (prevValue !== nextValue) {\n return false;\n }\n\n // Check cell/row identity\n if (prev.cell.row.id !== next.cell.row.id) return false;\n\n return true;\n}) as typeof DataGridCellImpl;\n\nfunction DataGridCellImpl({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n rowHeight,\n}: DataGridCellProps) {\n const cellOpts = cell.column.columnDef.meta?.cell;\n const variant = cellOpts?.variant ?? \"text\";\n\n let Comp: React.ComponentType>;\n\n switch (variant) {\n case \"short-text\":\n Comp = ShortTextCell;\n break;\n case \"long-text\":\n Comp = LongTextCell;\n break;\n case \"number\":\n Comp = NumberCell;\n break;\n case \"url\":\n Comp = UrlCell;\n break;\n case \"checkbox\":\n Comp = CheckboxCell;\n break;\n case \"select\":\n Comp = SelectCell;\n break;\n case \"multi-select\":\n Comp = MultiSelectCell;\n break;\n case \"date\":\n Comp = DateCell;\n break;\n case \"file\":\n Comp = FileCell;\n break;\n\n default:\n Comp = ShortTextCell;\n break;\n }\n\n return (\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\";\n\nimport type { RowData } from \"@tanstack/react-table\";\n\nimport * as React from \"react\";\n\nimport type { DataGridCellProps } from \"@/lib/data-grid-types\";\n\nimport { useDataGridPresence } from \"@/components/data-grid/data-grid-presence\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { getCellKey } from \"@/lib/data-grid-utils\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataGridCellWrapperProps\n extends DataGridCellProps, React.ComponentProps<\"div\"> {}\n\nexport function DataGridCellWrapper({\n tableMeta,\n rowIndex,\n columnId,\n isEditing,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n rowHeight,\n className,\n onClick: onClickProp,\n onKeyDown: onKeyDownProp,\n ref,\n ...props\n}: DataGridCellWrapperProps) {\n const cellMapRef = tableMeta?.cellMapRef;\n const cellPresence = useDataGridPresence(getCellKey(rowIndex, columnId));\n\n const onCellChange = React.useCallback(\n (node: HTMLDivElement | null) => {\n if (!cellMapRef) return;\n\n const cellKey = getCellKey(rowIndex, columnId);\n\n if (node) {\n cellMapRef.current.set(cellKey, node);\n } else {\n cellMapRef.current.delete(cellKey);\n }\n },\n [rowIndex, columnId, cellMapRef],\n );\n\n const composedRef = useComposedRefs(ref, onCellChange);\n\n const onClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n event.preventDefault();\n onClickProp?.(event);\n if (isFocused && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n tableMeta?.onCellClick?.(rowIndex, columnId, event);\n }\n }\n },\n [\n tableMeta,\n rowIndex,\n columnId,\n isEditing,\n isFocused,\n readOnly,\n onClickProp,\n ],\n );\n\n const onContextMenu = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n tableMeta?.onCellContextMenu?.(rowIndex, columnId, event);\n }\n },\n [tableMeta, rowIndex, columnId, isEditing],\n );\n\n const onDoubleClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n event.preventDefault();\n tableMeta?.onCellDoubleClick?.(rowIndex, columnId);\n }\n },\n [tableMeta, 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 && !readOnly) {\n if (event.key === \"F2\" || event.key === \"Enter\") {\n event.preventDefault();\n event.stopPropagation();\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n return;\n }\n\n if (event.key === \" \") {\n event.preventDefault();\n event.stopPropagation();\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n return;\n }\n\n if (event.key.length === 1 && !event.ctrlKey && !event.metaKey) {\n event.preventDefault();\n event.stopPropagation();\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n }\n }\n },\n [\n onKeyDownProp,\n isFocused,\n isEditing,\n readOnly,\n tableMeta,\n rowIndex,\n columnId,\n ],\n );\n\n const onMouseDown = React.useCallback(\n (event: React.MouseEvent) => {\n if (!isEditing) {\n tableMeta?.onCellMouseDown?.(rowIndex, columnId, event);\n }\n },\n [tableMeta, rowIndex, columnId, isEditing],\n );\n\n const onMouseEnter = React.useCallback(() => {\n if (!isEditing) {\n tableMeta?.onCellMouseEnter?.(rowIndex, columnId);\n }\n }, [tableMeta, rowIndex, columnId, isEditing]);\n\n const onMouseUp = React.useCallback(() => {\n if (!isEditing) {\n tableMeta?.onCellMouseUp?.();\n }\n }, [tableMeta, isEditing]);\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\";\n\nimport type { RowData } from \"@tanstack/react-table\";\n\nimport { Check, Upload, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\n\nimport type { DataGridCellProps, FileCellData } from \"@/lib/data-grid-types\";\n\nimport { DataGridCellWrapper } from \"@/components/data-grid/data-grid-cell-wrapper\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\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 SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { useBadgeOverflow } from \"@/hooks/use-badge-overflow\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport {\n formatDateForDisplay,\n formatDateToString,\n formatFileSize,\n getCellKey,\n getFileIcon,\n getLineCount,\n getUrlHref,\n parseLocalDate,\n} from \"@/lib/data-grid-utils\";\nimport { cn } from \"@/lib/utils\";\n\nexport function ShortTextCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isEditing,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\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 prevIsEditingRef = React.useRef(false);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue);\n if (cellRef.current && !isEditing) {\n cellRef.current.textContent = initialValue;\n }\n }\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 (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: currentValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, readOnly]);\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 tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue,\n });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent ?? \"\";\n if (currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue,\n });\n }\n tableMeta?.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, tableMeta, rowIndex, columnId],\n );\n\n React.useEffect(() => {\n const wasEditing = prevIsEditingRef.current;\n prevIsEditingRef.current = isEditing;\n\n if (isEditing && !wasEditing && 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 }, [isEditing, value]);\n\n const displayValue = !isEditing ? (value ?? \"\") : \"\";\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n \n {displayValue}\n \n \n );\n}\n\nexport function LongTextCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const textareaRef = React.useRef(null);\n const containerRef = React.useRef(null);\n const pendingCharRef = React.useRef(null);\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 const debouncedSave = useDebouncedCallback((newValue: string) => {\n if (!readOnly) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\n }\n }, 300);\n\n const onSave = React.useCallback(() => {\n // Immediately save any pending changes and close the popover\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, value, initialValue, rowIndex, columnId, readOnly]);\n\n const onCancel = React.useCallback(() => {\n // Restore the original value\n setValue(initialValue ?? \"\");\n if (!readOnly) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: initialValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, initialValue, rowIndex, columnId, readOnly]);\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n // Immediately save any pending changes when closing\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, value, initialValue, rowIndex, columnId, readOnly],\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 // Insert pending character using execCommand so it's part of undo history\n // Use requestAnimationFrame to ensure focus has fully settled\n if (pendingCharRef.current) {\n const char = pendingCharRef.current;\n pendingCharRef.current = null;\n requestAnimationFrame(() => {\n if (\n textareaRef.current &&\n document.activeElement === textareaRef.current\n ) {\n document.execCommand(\"insertText\", false, char);\n textareaRef.current.scrollTop = textareaRef.current.scrollHeight;\n }\n });\n } else {\n textareaRef.current.scrollTop = textareaRef.current.scrollHeight;\n }\n }\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n isFocused &&\n !isEditing &&\n !readOnly &&\n event.key.length === 1 &&\n !event.ctrlKey &&\n !event.metaKey\n ) {\n // Store the character to be inserted after textarea focuses\n // This ensures it's part of the textarea's undo history\n pendingCharRef.current = event.key;\n }\n },\n [isFocused, isEditing, readOnly],\n );\n\n const onBlur = React.useCallback(() => {\n // Immediately save any pending changes on blur\n if (!readOnly && value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, value, initialValue, rowIndex, columnId, readOnly]);\n\n const onChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const newValue = event.target.value;\n setValue(newValue);\n debouncedSave(newValue);\n },\n [debouncedSave],\n );\n\n const onKeyDown = 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 } else if (event.key === \"Tab\") {\n event.preventDefault();\n // Save any pending changes\n if (value !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value });\n }\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n return;\n }\n // Stop propagation to prevent grid navigation\n event.stopPropagation();\n },\n [onSave, onCancel, value, initialValue, tableMeta, rowIndex, columnId],\n );\n\n return (\n \n \n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {value}\n \n \n \n \n \n \n );\n}\n\nexport function NumberCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\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\n const cellOpts = cell.column.columnDef.meta?.cell;\n const numberCellOpts = cellOpts?.variant === \"number\" ? cellOpts : null;\n const min = numberCellOpts?.min;\n const max = numberCellOpts?.max;\n const step = numberCellOpts?.step;\n\n const prevIsEditingRef = React.useRef(false);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(String(initialValue ?? \"\"));\n }\n\n const onBlur = React.useCallback(() => {\n const numValue = value === \"\" ? null : Number(value);\n if (!readOnly && numValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, value, readOnly]);\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 tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const numValue = value === \"\" ? null : Number(value);\n if (numValue !== initialValue) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: numValue });\n }\n tableMeta?.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, tableMeta, rowIndex, columnId, value],\n );\n\n React.useEffect(() => {\n const wasEditing = prevIsEditingRef.current;\n prevIsEditingRef.current = isEditing;\n\n // Only focus when we start editing (transition from false to true)\n if (isEditing && !wasEditing && inputRef.current) {\n inputRef.current.focus();\n }\n }, [isEditing]);\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n ) : (\n {value}\n )}\n \n );\n}\n\nexport function UrlCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isEditing,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\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 prevIsEditingRef = React.useRef(false);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n if (cellRef.current && !isEditing) {\n cellRef.current.textContent = initialValue ?? \"\";\n }\n }\n\n const onBlur = React.useCallback(() => {\n const currentValue = cellRef.current?.textContent?.trim() ?? \"\";\n\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.onCellEditingStop?.();\n }, [tableMeta, rowIndex, columnId, initialValue, readOnly]);\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?.trim() ?? \"\";\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.onCellEditingStop?.({ moveToNextRow: true });\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n const currentValue = cellRef.current?.textContent?.trim() ?? \"\";\n if (!readOnly && currentValue !== initialValue) {\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: currentValue || null,\n });\n }\n tableMeta?.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 !readOnly &&\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 [\n isEditing,\n isFocused,\n initialValue,\n tableMeta,\n rowIndex,\n columnId,\n readOnly,\n ],\n );\n\n const onLinkClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (isEditing) {\n event.preventDefault();\n return;\n }\n\n // Check if URL was rejected due to dangerous protocol\n const href = getUrlHref(value);\n if (!href) {\n event.preventDefault();\n toast.error(\"Invalid URL\", {\n description:\n \"URL contains a dangerous protocol (javascript:, data:, vbscript:, or file:)\",\n });\n return;\n }\n\n // Stop propagation to prevent grid from interfering with link navigation\n event.stopPropagation();\n },\n [isEditing, value],\n );\n\n React.useEffect(() => {\n const wasEditing = prevIsEditingRef.current;\n prevIsEditingRef.current = isEditing;\n\n if (isEditing && !wasEditing && 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 }, [isEditing, value]);\n\n const displayValue = !isEditing ? (value ?? \"\") : \"\";\n const urlHref = displayValue ? getUrlHref(displayValue) : \"\";\n const isDangerousUrl = displayValue && !urlHref;\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {!isEditing && displayValue ? (\n \n \n {displayValue}\n \n \n ) : (\n \n {displayValue}\n \n )}\n \n );\n}\n\nexport function CheckboxCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: Omit, \"isEditing\">) {\n const initialValue = cell.getValue() as boolean;\n const [value, setValue] = React.useState(initialValue);\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue);\n }\n\n const onCheckedChange = React.useCallback(\n (checked: boolean) => {\n if (readOnly) return;\n setValue(checked);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: checked });\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (\n isFocused &&\n !readOnly &&\n (event.key === \" \" || event.key === \"Enter\")\n ) {\n event.preventDefault();\n event.stopPropagation();\n onCheckedChange(!value);\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isFocused, value, onCheckedChange, tableMeta, readOnly],\n );\n\n const onWrapperClick = React.useCallback(\n (event: React.MouseEvent) => {\n if (isFocused && !readOnly) {\n event.preventDefault();\n event.stopPropagation();\n onCheckedChange(!value);\n }\n },\n [isFocused, value, onCheckedChange, readOnly],\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 ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={false}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n className=\"flex size-full justify-center\"\n onClick={onWrapperClick}\n onKeyDown={onWrapperKeyDown}\n >\n \n \n );\n}\n\nexport function SelectCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = (cell.getValue() ?? undefined) as string | undefined;\n\n const [value, setValue] = React.useState(initialValue);\n const containerRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const options = React.useMemo(\n () => (cellOpts?.variant === \"select\" ? cellOpts.options : []),\n [cellOpts],\n );\n const optionByValue = React.useMemo(\n () => new Map(options.map((option) => [option.value, option])),\n [options],\n );\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue);\n }\n\n const onValueChange = React.useCallback(\n (newValue: string) => {\n if (readOnly) return;\n setValue(newValue);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValue });\n tableMeta?.onCellEditingStop?.();\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, initialValue, tableMeta],\n );\n\n const displayLabel = value\n ? (optionByValue.get(value)?.label ?? value)\n : null;\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n \n {displayLabel ? (\n \n \n \n ) : (\n \n )}\n \n \n \n {options.map((option) => (\n \n {option.label}\n \n ))}\n \n \n \n ) : displayLabel ? (\n \n {displayLabel}\n \n ) : null}\n \n );\n}\n\nexport function MultiSelectCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const cellValue = React.useMemo(() => {\n const value = cell.getValue() as string[];\n return value ?? [];\n }, [cell]);\n\n const cellKey = getCellKey(rowIndex, columnId);\n const prevCellKeyRef = React.useRef(cellKey);\n\n const [selectedValues, setSelectedValues] =\n React.useState(cellValue);\n const [searchValue, setSearchValue] = React.useState(\"\");\n const containerRef = React.useRef(null);\n const inputRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const options = React.useMemo(\n () => (cellOpts?.variant === \"multi-select\" ? cellOpts.options : []),\n [cellOpts],\n );\n const optionByValue = React.useMemo(\n () => new Map(options.map((option) => [option.value, option])),\n [options],\n );\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const prevCellValueRef = React.useRef(cellValue);\n if (cellValue !== prevCellValueRef.current) {\n prevCellValueRef.current = cellValue;\n setSelectedValues(cellValue);\n }\n\n if (prevCellKeyRef.current !== cellKey) {\n prevCellKeyRef.current = cellKey;\n setSearchValue(\"\");\n }\n\n const onValueChange = React.useCallback(\n (value: string) => {\n if (readOnly) return;\n let newValues: string[] = [];\n setSelectedValues((curr) => {\n newValues = curr.includes(value)\n ? curr.filter((v) => v !== value)\n : [...curr, value];\n return newValues;\n });\n queueMicrotask(() => {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n inputRef.current?.focus();\n });\n setSearchValue(\"\");\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const removeValue = React.useCallback(\n (valueToRemove: string, event?: React.MouseEvent) => {\n if (readOnly) return;\n event?.stopPropagation();\n event?.preventDefault();\n let newValues: string[] = [];\n setSelectedValues((curr) => {\n newValues = curr.filter((v) => v !== valueToRemove);\n return newValues;\n });\n queueMicrotask(() => {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n inputRef.current?.focus();\n });\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const clearAll = React.useCallback(() => {\n if (readOnly) return;\n setSelectedValues([]);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: [] });\n queueMicrotask(() => inputRef.current?.focus());\n }, [tableMeta, rowIndex, columnId, readOnly]);\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\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 && event.key === \"Escape\") {\n event.preventDefault();\n setSelectedValues(cellValue);\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n setSearchValue(\"\");\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, cellValue, tableMeta],\n );\n\n const onInputKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"Backspace\" && searchValue === \"\") {\n event.preventDefault();\n let newValues: string[] | null = null;\n setSelectedValues((curr) => {\n if (curr.length === 0) return curr;\n newValues = curr.slice(0, -1);\n return newValues;\n });\n queueMicrotask(() => {\n if (newValues !== null) {\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValues });\n }\n inputRef.current?.focus();\n });\n }\n if (event.key === \"Escape\") {\n event.stopPropagation();\n }\n },\n [searchValue, tableMeta, rowIndex, columnId],\n );\n\n const displayLabels = selectedValues\n .map((val) => optionByValue.get(val)?.label ?? val)\n .filter(Boolean);\n\n const selectedValuesSet = React.useMemo(\n () => new Set(selectedValues),\n [selectedValues],\n );\n\n const lineCount = getLineCount(rowHeight);\n\n const { visibleItems: visibleLabels, hiddenCount: hiddenBadgeCount } =\n useBadgeOverflow({\n items: displayLabels,\n getLabel: (label) => label,\n containerRef,\n lineCount,\n });\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n \n
\n \n \n \n
\n {selectedValues.map((value) => {\n const label = optionByValue.get(value)?.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 = selectedValuesSet.has(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 DateCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const initialValue = cell.getValue() as string;\n const [value, setValue] = React.useState(initialValue ?? \"\");\n const containerRef = React.useRef(null);\n\n const prevInitialValueRef = React.useRef(initialValue);\n if (initialValue !== prevInitialValueRef.current) {\n prevInitialValueRef.current = initialValue;\n setValue(initialValue ?? \"\");\n }\n\n // Parse date as local time to avoid timezone shifts\n const selectedDate = value ? (parseLocalDate(value) ?? undefined) : undefined;\n\n const onDateSelect = React.useCallback(\n (date: Date | undefined) => {\n if (!date || readOnly) return;\n\n // Format using local date components to avoid timezone issues\n const formattedDate = formatDateToString(date);\n setValue(formattedDate);\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: formattedDate });\n tableMeta?.onCellEditingStop?.();\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing && event.key === \"Escape\") {\n event.preventDefault();\n setValue(initialValue);\n tableMeta?.onCellEditingStop?.();\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [isEditing, isFocused, initialValue, tableMeta],\n );\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n onKeyDown={onWrapperKeyDown}\n >\n \n \n \n {formatDateForDisplay(value)}\n \n \n {isEditing && (\n \n \n \n )}\n \n \n );\n}\n\nexport function FileCell({\n cell,\n tableMeta,\n rowIndex,\n columnId,\n rowHeight,\n isFocused,\n isEditing,\n isSelected,\n isSearchMatch,\n isActiveSearchMatch,\n readOnly,\n}: DataGridCellProps) {\n const cellValue = React.useMemo(\n () => (cell.getValue() as FileCellData[]) ?? [],\n [cell],\n );\n\n const cellKey = getCellKey(rowIndex, columnId);\n const prevCellKeyRef = React.useRef(cellKey);\n\n const labelId = React.useId();\n const descriptionId = React.useId();\n\n const [files, setFiles] = React.useState(cellValue);\n const [uploadingFiles, setUploadingFiles] = React.useState>(\n new Set(),\n );\n const [deletingFiles, setDeletingFiles] = React.useState>(\n new Set(),\n );\n const [isDraggingOver, setIsDraggingOver] = React.useState(false);\n const [isDragging, setIsDragging] = React.useState(false);\n const [error, setError] = React.useState(null);\n\n const isUploading = uploadingFiles.size > 0;\n const isDeleting = deletingFiles.size > 0;\n const isPending = isUploading || isDeleting;\n const containerRef = React.useRef(null);\n const fileInputRef = React.useRef(null);\n const dropzoneRef = React.useRef(null);\n const cellOpts = cell.column.columnDef.meta?.cell;\n const sideOffset = -(containerRef.current?.clientHeight ?? 0);\n\n const fileCellOpts = cellOpts?.variant === \"file\" ? cellOpts : null;\n const maxFileSize = fileCellOpts?.maxFileSize ?? 10 * 1024 * 1024;\n const maxFiles = fileCellOpts?.maxFiles ?? 10;\n const accept = fileCellOpts?.accept;\n const multiple = fileCellOpts?.multiple ?? false;\n\n const acceptedTypes = React.useMemo(\n () => (accept ? accept.split(\",\").map((t) => t.trim()) : null),\n [accept],\n );\n\n const prevCellValueRef = React.useRef(cellValue);\n if (cellValue !== prevCellValueRef.current) {\n prevCellValueRef.current = cellValue;\n for (const file of files) {\n if (file.url) {\n URL.revokeObjectURL(file.url);\n }\n }\n setFiles(cellValue);\n setError(null);\n }\n\n if (prevCellKeyRef.current !== cellKey) {\n prevCellKeyRef.current = cellKey;\n setError(null);\n }\n\n const validateFile = React.useCallback(\n (file: File): string | null => {\n if (maxFileSize && file.size > maxFileSize) {\n return `File size exceeds ${formatFileSize(maxFileSize)}`;\n }\n if (acceptedTypes) {\n const fileExtension = `.${file.name.split(\".\").pop()}`;\n const isAccepted = acceptedTypes.some((type) => {\n if (type.endsWith(\"/*\")) {\n const baseType = type.slice(0, -2);\n return file.type.startsWith(`${baseType}/`);\n }\n if (type.startsWith(\".\")) {\n return fileExtension.toLowerCase() === type.toLowerCase();\n }\n return file.type === type;\n });\n if (!isAccepted) {\n return \"File type not accepted\";\n }\n }\n return null;\n },\n [maxFileSize, acceptedTypes],\n );\n\n const addFiles = React.useCallback(\n async (newFiles: File[], skipUpload = false) => {\n if (readOnly || isPending) return;\n setError(null);\n\n if (maxFiles && files.length + newFiles.length > maxFiles) {\n const errorMessage = `Maximum ${maxFiles} files allowed`;\n setError(errorMessage);\n toast(errorMessage);\n setTimeout(() => {\n setError(null);\n }, 2000);\n return;\n }\n\n const rejectedFiles: Array<{ name: string; reason: string }> = [];\n const filesToValidate: File[] = [];\n\n for (const file of newFiles) {\n const validationError = validateFile(file);\n if (validationError) {\n rejectedFiles.push({ name: file.name, reason: validationError });\n continue;\n }\n filesToValidate.push(file);\n }\n\n if (rejectedFiles.length > 0) {\n const firstError = rejectedFiles[0];\n if (firstError) {\n setError(firstError.reason);\n\n const truncatedName =\n firstError.name.length > 20\n ? `${firstError.name.slice(0, 20)}...`\n : firstError.name;\n\n if (rejectedFiles.length === 1) {\n toast(firstError.reason, {\n description: `\"${truncatedName}\" has been rejected`,\n });\n } else {\n toast(firstError.reason, {\n description: `\"${truncatedName}\" and ${rejectedFiles.length - 1} more rejected`,\n });\n }\n\n setTimeout(() => {\n setError(null);\n }, 2000);\n }\n }\n\n if (filesToValidate.length > 0) {\n if (!skipUpload) {\n const tempFiles = filesToValidate.map((f) => ({\n id: crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: undefined,\n }));\n const filesWithTemp = [...files, ...tempFiles];\n setFiles(filesWithTemp);\n\n const uploadingIds = new Set(tempFiles.map((f) => f.id));\n setUploadingFiles(uploadingIds);\n\n let uploadedFiles: FileCellData[] = [];\n\n if (tableMeta?.onFilesUpload) {\n try {\n uploadedFiles = await tableMeta.onFilesUpload({\n files: filesToValidate,\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error\n ? error.message\n : `Failed to upload ${filesToValidate.length} file${filesToValidate.length !== 1 ? \"s\" : \"\"}`,\n );\n setFiles((prev) => prev.filter((f) => !uploadingIds.has(f.id)));\n setUploadingFiles(new Set());\n return;\n }\n } else {\n uploadedFiles = filesToValidate.map((f, i) => ({\n id: tempFiles[i]?.id ?? crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: URL.createObjectURL(f),\n }));\n }\n\n const finalFiles = filesWithTemp\n .map((f) => {\n if (uploadingIds.has(f.id)) {\n return uploadedFiles.find((uf) => uf.name === f.name) ?? f;\n }\n return f;\n })\n .filter((f) => f.url !== undefined);\n\n setFiles(finalFiles);\n setUploadingFiles(new Set());\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: finalFiles });\n } else {\n const newFilesData: FileCellData[] = filesToValidate.map((f) => ({\n id: crypto.randomUUID(),\n name: f.name,\n size: f.size,\n type: f.type,\n url: URL.createObjectURL(f),\n }));\n const updatedFiles = [...files, ...newFilesData];\n setFiles(updatedFiles);\n tableMeta?.onDataUpdate?.({\n rowIndex,\n columnId,\n value: updatedFiles,\n });\n }\n }\n },\n [\n files,\n maxFiles,\n validateFile,\n tableMeta,\n rowIndex,\n columnId,\n readOnly,\n isPending,\n ],\n );\n\n const removeFile = React.useCallback(\n async (fileId: string) => {\n if (readOnly || isPending) return;\n setError(null);\n\n const fileToRemove = files.find((f) => f.id === fileId);\n if (!fileToRemove) return;\n\n setDeletingFiles((prev) => new Set(prev).add(fileId));\n\n if (tableMeta?.onFilesDelete) {\n try {\n await tableMeta.onFilesDelete({\n fileIds: [fileId],\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error\n ? error.message\n : `Failed to delete ${fileToRemove.name}`,\n );\n setDeletingFiles((prev) => {\n const next = new Set(prev);\n next.delete(fileId);\n return next;\n });\n return;\n }\n }\n\n if (fileToRemove.url?.startsWith(\"blob:\")) {\n URL.revokeObjectURL(fileToRemove.url);\n }\n\n const updatedFiles = files.filter((f) => f.id !== fileId);\n setFiles(updatedFiles);\n setDeletingFiles((prev) => {\n const next = new Set(prev);\n next.delete(fileId);\n return next;\n });\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: updatedFiles });\n },\n [files, tableMeta, rowIndex, columnId, readOnly, isPending],\n );\n\n const clearAll = React.useCallback(async () => {\n if (readOnly || isPending) return;\n setError(null);\n\n const fileIds = files.map((f) => f.id);\n setDeletingFiles(new Set(fileIds));\n\n if (tableMeta?.onFilesDelete && files.length > 0) {\n try {\n await tableMeta.onFilesDelete({\n fileIds,\n rowIndex,\n columnId,\n });\n } catch (error) {\n toast.error(\n error instanceof Error ? error.message : \"Failed to delete files\",\n );\n setDeletingFiles(new Set());\n return;\n }\n }\n\n for (const file of files) {\n if (file.url?.startsWith(\"blob:\")) {\n URL.revokeObjectURL(file.url);\n }\n }\n setFiles([]);\n setDeletingFiles(new Set());\n tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: [] });\n }, [files, tableMeta, rowIndex, columnId, readOnly, isPending]);\n\n const onCellDragEnter = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n if (event.dataTransfer.types.includes(\"Files\")) {\n setIsDraggingOver(true);\n }\n }, []);\n\n const onCellDragLeave = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const rect = event.currentTarget.getBoundingClientRect();\n const x = event.clientX;\n const y = event.clientY;\n\n if (\n x <= rect.left ||\n x >= rect.right ||\n y <= rect.top ||\n y >= rect.bottom\n ) {\n setIsDraggingOver(false);\n }\n }, []);\n\n const onCellDragOver = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n }, []);\n\n const onCellDrop = React.useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDraggingOver(false);\n\n const droppedFiles = Array.from(event.dataTransfer.files);\n if (droppedFiles.length > 0) {\n void addFiles(droppedFiles, false);\n }\n },\n [addFiles],\n );\n\n const onDropzoneDragEnter = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDragging(true);\n }, []);\n\n const onDropzoneDragLeave = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n const rect = event.currentTarget.getBoundingClientRect();\n const x = event.clientX;\n const y = event.clientY;\n\n if (\n x <= rect.left ||\n x >= rect.right ||\n y <= rect.top ||\n y >= rect.bottom\n ) {\n setIsDragging(false);\n }\n }, []);\n\n const onDropzoneDragOver = React.useCallback((event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n }, []);\n\n const onDropzoneDrop = React.useCallback(\n (event: React.DragEvent) => {\n event.preventDefault();\n event.stopPropagation();\n setIsDragging(false);\n\n const droppedFiles = Array.from(event.dataTransfer.files);\n void addFiles(droppedFiles, false);\n },\n [addFiles],\n );\n\n const onDropzoneClick = React.useCallback(() => {\n fileInputRef.current?.click();\n }, []);\n\n const onDropzoneKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (event.key === \"Enter\" || event.key === \" \") {\n event.preventDefault();\n onDropzoneClick();\n }\n },\n [onDropzoneClick],\n );\n\n const onFileInputChange = React.useCallback(\n (event: React.ChangeEvent) => {\n const selectedFiles = Array.from(event.target.files ?? []);\n void addFiles(selectedFiles, false);\n event.target.value = \"\";\n },\n [addFiles],\n );\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n if (open && !readOnly) {\n setError(null);\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else {\n setError(null);\n tableMeta?.onCellEditingStop?.();\n }\n },\n [tableMeta, rowIndex, columnId, readOnly],\n );\n\n const onEscapeKeyDown: NonNullable<\n React.ComponentProps[\"onEscapeKeyDown\"]\n > = React.useCallback((event) => {\n // Prevent the escape key from propagating to the data grid's keyboard handler\n // which would call blurCell() and remove focus from the cell\n event.stopPropagation();\n }, []);\n\n const onOpenAutoFocus: NonNullable<\n React.ComponentProps[\"onOpenAutoFocus\"]\n > = React.useCallback((event) => {\n event.preventDefault();\n queueMicrotask(() => {\n dropzoneRef.current?.focus();\n });\n }, []);\n\n const onWrapperKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if (isEditing) {\n if (event.key === \"Escape\") {\n event.preventDefault();\n setFiles(cellValue);\n setError(null);\n tableMeta?.onCellEditingStop?.();\n } else if (event.key === \" \") {\n event.preventDefault();\n onDropzoneClick();\n } else if (event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n } else if (isFocused && event.key === \"Enter\") {\n event.preventDefault();\n tableMeta?.onCellEditingStart?.(rowIndex, columnId);\n } else if (isFocused && event.key === \"Tab\") {\n event.preventDefault();\n tableMeta?.onCellEditingStop?.({\n direction: event.shiftKey ? \"left\" : \"right\",\n });\n }\n },\n [\n isEditing,\n isFocused,\n cellValue,\n tableMeta,\n onDropzoneClick,\n rowIndex,\n columnId,\n ],\n );\n\n React.useEffect(() => {\n return () => {\n for (const file of files) {\n if (file.url) {\n URL.revokeObjectURL(file.url);\n }\n }\n };\n }, [files]);\n\n const lineCount = getLineCount(rowHeight);\n\n const { visibleItems: visibleFiles, hiddenCount: hiddenFileCount } =\n useBadgeOverflow({\n items: files,\n getLabel: (file) => file.name,\n containerRef,\n lineCount,\n cacheKeyPrefix: \"file\",\n iconSize: 12,\n maxWidth: 100,\n });\n\n return (\n \n ref={containerRef}\n cell={cell}\n tableMeta={tableMeta}\n rowIndex={rowIndex}\n columnId={columnId}\n rowHeight={rowHeight}\n isEditing={isEditing}\n isFocused={isFocused}\n isSelected={isSelected}\n isSearchMatch={isSearchMatch}\n isActiveSearchMatch={isActiveSearchMatch}\n readOnly={readOnly}\n className={cn({\n \"ring-1 ring-primary/80 ring-inset\": isDraggingOver,\n })}\n onDragEnter={onCellDragEnter}\n onDragLeave={onCellDragLeave}\n onDragOver={onCellDragOver}\n onDrop={onCellDrop}\n onKeyDown={onWrapperKeyDown}\n >\n {isEditing ? (\n \n \n
\n \n \n
\n \n File upload\n \n \n \n
\n

\n {isDragging ? \"Drop files here\" : \"Drag files here\"}\n

\n

\n or click to browse\n

\n
\n

\n {maxFileSize\n ? `Max size: ${formatFileSize(maxFileSize)}${maxFiles ? ` • Max ${maxFiles} files` : \"\"}`\n : maxFiles\n ? `Max ${maxFiles} files`\n : \"Select files to upload\"}\n

\n
\n \n {files.length > 0 && (\n
\n
\n

\n {files.length} {files.length === 1 ? \"file\" : \"files\"}\n

\n \n Clear all\n \n
\n
\n {files.map((file) => {\n const FileIcon = getFileIcon(file.type);\n const isFileUploading = uploadingFiles.has(file.id);\n const isFileDeleting = deletingFiles.has(file.id);\n const isFilePending = isFileUploading || isFileDeleting;\n\n return (\n \n {FileIcon && (\n \n )}\n
\n

{file.name}

\n

\n {isFileUploading\n ? \"Uploading...\"\n : isFileDeleting\n ? \"Deleting...\"\n : formatFileSize(file.size)}\n

\n
\n removeFile(file.id)}\n disabled={isPending}\n >\n \n \n
\n );\n })}\n
\n
\n )}\n \n \n
\n ) : null}\n {isDraggingOver ? (\n
\n \n Drop files here\n
\n ) : files.length > 0 ? (\n
\n {visibleFiles.map((file) => {\n const isUploading = uploadingFiles.has(file.id);\n\n if (isUploading) {\n return (\n \n );\n }\n\n const FileIcon = getFileIcon(file.type);\n\n return (\n \n {FileIcon && }\n {file.name}\n \n );\n })}\n {hiddenFileCount > 0 && (\n \n +{hiddenFileCount}\n \n )}\n
\n ) : null}\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\";\n\nimport type {\n ColumnSort,\n Header,\n RowData,\n SortDirection,\n SortingState,\n Table,\n} from \"@tanstack/react-table\";\n\nimport {\n ChevronDownIcon,\n ChevronUpIcon,\n EyeOffIcon,\n PinIcon,\n PinOffIcon,\n XIcon,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\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 { getColumnVariant } from \"@/lib/data-grid-utils\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataGridColumnHeaderProps<\n TData extends RowData,\n TValue,\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 = table.store.state.columnResizing.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 === \"start\";\n const isPinnedRight = pinnedPosition === \"end\";\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(\"start\");\n }, [column]);\n\n const onRightPin = React.useCallback(() => {\n column.pin(\"end\");\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]:ltr:right-2 [&>span:first-child]:ltr:left-auto [&>span:first-child]:rtl:right-auto [&>span:first-child]:rtl:left-2\"\n checked={column.getIsSorted() === \"asc\"}\n onSelect={() => onSortingChange(\"asc\")}\n >\n \n Sort asc\n \n span:first-child]:ltr:right-2 [&>span:first-child]:ltr:left-auto [&>span:first-child]:rtl:right-auto [&>span:first-child]:rtl:left-2\"\n checked={column.getIsSorted() === \"desc\"}\n onSelect={() => 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 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 TData extends RowData,\n TValue,\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\";\n\nimport type { ColumnDef, RowData } from \"@tanstack/react-table\";\n\nimport { CopyIcon, EraserIcon, ScissorsIcon, Trash2Icon } from \"lucide-react\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\nimport type {\n CellUpdate,\n ContextMenuState,\n DataGridTableMeta,\n} from \"@/lib/data-grid-types\";\n\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { useAsRef } from \"@/hooks/use-as-ref\";\nimport { getEmptyCellValue, parseCellKey } from \"@/lib/data-grid-utils\";\n\ninterface DataGridContextMenuProps {\n tableMeta: DataGridTableMeta;\n columns: ReadonlyArray>;\n contextMenu: ContextMenuState;\n}\n\nexport function DataGridContextMenu({\n tableMeta,\n columns,\n contextMenu,\n}: DataGridContextMenuProps) {\n const onContextMenuOpenChange = tableMeta?.onContextMenuOpenChange;\n const selectionState = tableMeta?.selectionState;\n const dataGridRef = tableMeta?.dataGridRef;\n const onDataUpdate = tableMeta?.onDataUpdate;\n const onRowsDelete = tableMeta?.onRowsDelete;\n const onCellsCopy = tableMeta?.onCellsCopy;\n const onCellsCut = tableMeta?.onCellsCut;\n\n if (!contextMenu.open) return null;\n\n return (\n \n );\n}\n\ninterface ContextMenuProps\n extends\n Pick<\n DataGridTableMeta,\n | \"dataGridRef\"\n | \"onContextMenuOpenChange\"\n | \"selectionState\"\n | \"onDataUpdate\"\n | \"onRowsDelete\"\n | \"onCellsCopy\"\n | \"onCellsCut\"\n | \"readOnly\"\n >,\n Required> {\n tableMeta: DataGridTableMeta;\n columns: ReadonlyArray>;\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 tableMeta,\n columns,\n dataGridRef,\n contextMenu,\n onContextMenuOpenChange,\n selectionState,\n onDataUpdate,\n onRowsDelete,\n onCellsCopy,\n onCellsCut,\n}: ContextMenuProps) {\n const propsRef = useAsRef({\n dataGridRef,\n selectionState,\n onDataUpdate,\n onRowsDelete,\n onCellsCopy,\n onCellsCut,\n columns,\n });\n\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 propsRef.current.dataGridRef?.current?.focus();\n },\n [propsRef],\n );\n\n const onCopy = React.useCallback(() => {\n propsRef.current.onCellsCopy?.();\n }, [propsRef]);\n\n const onCut = React.useCallback(() => {\n propsRef.current.onCellsCut?.();\n }, [propsRef]);\n\n const onClear = React.useCallback(() => {\n const { selectionState, columns, onDataUpdate } = propsRef.current;\n\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\n // Get column from columns array\n const column = columns.find((col) => {\n if (col.id) return col.id === columnId;\n if (\"accessorKey\" in col) return col.accessorKey === columnId;\n return false;\n });\n const cellVariant = column?.meta?.cell?.variant;\n\n const emptyValue = getEmptyCellValue(cellVariant);\n\n updates.push({ rowIndex, columnId, value: emptyValue });\n }\n\n onDataUpdate?.(updates);\n\n toast.success(\n `${updates.length} cell${updates.length !== 1 ? \"s\" : \"\"} cleared`,\n );\n }, [propsRef]);\n\n const onDelete = React.useCallback(async () => {\n const { selectionState, onRowsDelete } = propsRef.current;\n\n if (\n !selectionState?.selectedCells ||\n selectionState.selectedCells.size === 0\n )\n return;\n\n const rowIndices = new Set();\n for (const cellKey of selectionState.selectedCells) {\n const { rowIndex } = parseCellKey(cellKey);\n rowIndices.add(rowIndex);\n }\n\n const rowIndicesArray = Array.from(rowIndices).sort((a, b) => a - b);\n const rowCount = rowIndicesArray.length;\n\n await onRowsDelete?.(rowIndicesArray);\n\n toast.success(`${rowCount} row${rowCount !== 1 ? \"s\" : \"\"} deleted`);\n }, [propsRef]);\n\n return (\n \n \n \n \n \n Copy\n \n \n \n Cut\n \n \n \n Clear\n \n {onRowsDelete && (\n <>\n \n \n \n Delete rows\n \n \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-paste-dialog.tsx", - "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport type {\n DataGridTableMeta,\n PasteDialogState,\n} from \"@/lib/data-grid-types\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { useAsRef } from \"@/hooks/use-as-ref\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataGridPasteDialogProps {\n tableMeta: DataGridTableMeta;\n pasteDialog: PasteDialogState;\n}\n\nexport function DataGridPasteDialog({\n tableMeta,\n pasteDialog,\n}: DataGridPasteDialogProps) {\n const onPasteDialogOpenChange = tableMeta?.onPasteDialogOpenChange;\n const onCellsPaste = tableMeta?.onCellsPaste;\n\n if (!pasteDialog.open) return null;\n\n return (\n \n );\n}\n\ninterface PasteDialogProps\n extends\n Pick,\n Required> {}\n\nconst PasteDialog = React.memo(PasteDialogImpl, (prev, next) => {\n if (prev.pasteDialog.open !== next.pasteDialog.open) return false;\n if (!next.pasteDialog.open) return true;\n if (prev.pasteDialog.rowsNeeded !== next.pasteDialog.rowsNeeded) return false;\n\n return true;\n});\n\nfunction PasteDialogImpl({\n pasteDialog,\n onPasteDialogOpenChange,\n onCellsPaste,\n}: PasteDialogProps) {\n const propsRef = useAsRef({\n onPasteDialogOpenChange,\n onCellsPaste,\n });\n\n const expandRadioRef = React.useRef(null);\n\n const onOpenChange = React.useCallback(\n (open: boolean) => {\n propsRef.current.onPasteDialogOpenChange?.(open);\n },\n [propsRef],\n );\n\n const onCancel = React.useCallback(() => {\n propsRef.current.onPasteDialogOpenChange?.(false);\n }, [propsRef]);\n\n const onContinue = React.useCallback(() => {\n propsRef.current.onCellsPaste?.(expandRadioRef.current?.checked ?? false);\n }, [propsRef]);\n\n return (\n \n \n \n Do you want to add more rows?\n \n We need {pasteDialog.rowsNeeded} additional row\n {pasteDialog.rowsNeeded !== 1 ? \"s\" : \"\"} to paste everything from\n your clipboard.\n \n \n
\n \n \n
\n \n \n \n \n
\n
\n );\n}\n\nfunction RadioItem({ className, ...props }: React.ComponentProps<\"input\">) {\n return (\n \n );\n}\n", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-paste-dialog.tsx" - }, - { - "path": "src/components/data-grid/data-grid-row.tsx", - "content": "\"use client\";\n\nimport type {\n ColumnPinningState,\n ColumnVisibilityState,\n Row,\n RowData,\n} from \"@tanstack/react-table\";\nimport type { VirtualItem } from \"@tanstack/react-virtual\";\n\nimport * as React from \"react\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\nimport type {\n CellPosition,\n DataGridTableMeta,\n Direction,\n RowHeightValue,\n} from \"@/lib/data-grid-types\";\n\nimport { DataGridCell } from \"@/components/data-grid/data-grid-cell\";\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport {\n flexRender,\n getCellKey,\n getColumnBorderVisibility,\n getColumnPinningStyle,\n getRowHeightValue,\n} from \"@/lib/data-grid-utils\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataGridRowProps<\n TData extends RowData,\n> extends React.ComponentProps<\"div\"> {\n row: Row;\n tableMeta: DataGridTableMeta;\n virtualItem: VirtualItem;\n measureElement: (node: Element | null) => void;\n rowMapRef: React.RefObject>;\n rowHeight: RowHeightValue;\n columnVisibility: ColumnVisibilityState;\n columnPinning: ColumnPinningState;\n focusedCell: CellPosition | null;\n editingCell: CellPosition | null;\n cellSelectionKeys: Set;\n searchMatchColumns: Set | null;\n activeSearchMatch: CellPosition | null;\n dir: Direction;\n readOnly: boolean;\n stretchColumns: boolean;\n adjustLayout: boolean;\n}\n\nexport const DataGridRow = React.memo(DataGridRowImpl, (prev, next) => {\n const prevRowIndex = prev.virtualItem.index;\n const nextRowIndex = next.virtualItem.index;\n\n // Re-render if row identity changed\n if (prev.row.id !== next.row.id) {\n return false;\n }\n\n // Re-render if row data (original) reference changed\n if (prev.row.original !== next.row.original) {\n return false;\n }\n\n // Re-render if virtual position changed (handles transform updates)\n if (prev.virtualItem.start !== next.virtualItem.start) {\n return false;\n }\n\n // Re-render if focus state changed for this row\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 // Re-render if focused column changed within this row\n if (nextHasFocus && prevHasFocus) {\n if (prev.focusedCell?.columnId !== next.focusedCell?.columnId) {\n return false;\n }\n }\n\n // Re-render if editing state changed for this row\n const prevHasEditing = prev.editingCell?.rowIndex === prevRowIndex;\n const nextHasEditing = next.editingCell?.rowIndex === nextRowIndex;\n\n if (prevHasEditing !== nextHasEditing) {\n return false;\n }\n\n // Re-render if editing column changed within this row\n if (nextHasEditing && prevHasEditing) {\n if (prev.editingCell?.columnId !== next.editingCell?.columnId) {\n return false;\n }\n }\n\n // Re-render if this row's selected cells changed\n // Using stable Set reference that only includes this row's cells\n if (prev.cellSelectionKeys !== next.cellSelectionKeys) {\n return false;\n }\n\n // Re-render if column visibility changed\n if (prev.columnVisibility !== next.columnVisibility) {\n return false;\n }\n\n // Re-render if row height changed\n if (prev.rowHeight !== next.rowHeight) {\n return false;\n }\n\n // Re-render if column pinning state changed\n if (prev.columnPinning !== next.columnPinning) {\n return false;\n }\n\n // Re-render if readOnly changed\n if (prev.readOnly !== next.readOnly) {\n return false;\n }\n\n // Re-render if search match columns changed for this row\n if (prev.searchMatchColumns !== next.searchMatchColumns) {\n return false;\n }\n\n // Re-render if active search match changed for this row\n if (prev.activeSearchMatch?.columnId !== next.activeSearchMatch?.columnId) {\n return false;\n }\n\n // Re-render if direction changed\n if (prev.dir !== next.dir) {\n return false;\n }\n\n // Re-render if adjustLayout state changed\n if (prev.adjustLayout !== next.adjustLayout) {\n return false;\n }\n\n // Re-render if stretchColumns changed\n if (prev.stretchColumns !== next.stretchColumns) {\n return false;\n }\n\n // Skip re-render - props are equal\n return true;\n}) as typeof DataGridRowImpl;\n\nfunction DataGridRowImpl({\n row,\n tableMeta,\n virtualItem,\n measureElement,\n rowMapRef,\n rowHeight,\n columnVisibility,\n columnPinning,\n focusedCell,\n editingCell,\n cellSelectionKeys,\n searchMatchColumns,\n activeSearchMatch,\n dir,\n readOnly,\n stretchColumns,\n adjustLayout,\n className,\n style,\n ref,\n ...props\n}: DataGridRowProps) {\n const virtualRowIndex = virtualItem.index;\n\n const onRowChange = React.useCallback(\n (node: HTMLDivElement | null) => {\n if (typeof virtualRowIndex === \"undefined\") return;\n\n if (node) {\n measureElement(node);\n rowMapRef.current?.set(virtualRowIndex, node);\n } else {\n rowMapRef.current?.delete(virtualRowIndex);\n }\n },\n [virtualRowIndex, measureElement, rowMapRef],\n );\n\n const rowRef = useComposedRefs(ref, onRowChange);\n\n const isRowSelected = row.getIsSelected();\n\n // Memoize visible cells to avoid recreating cell array on every render\n // Though TanStack returns new Cell wrappers, memoizing the array helps React's reconciliation\n const visibleCells = React.useMemo(\n () => row.getVisibleCells(),\n [row, columnVisibility, columnPinning],\n );\n\n return (\n \n {visibleCells.map((cell, colIndex) => {\n const columnId = cell.column.id;\n\n const isCellFocused =\n focusedCell?.rowIndex === virtualRowIndex &&\n focusedCell?.columnId === columnId;\n const isCellEditing =\n editingCell?.rowIndex === virtualRowIndex &&\n editingCell?.columnId === columnId;\n const isCellSelected =\n cellSelectionKeys?.has(getCellKey(virtualRowIndex, columnId)) ??\n false;\n\n const isSearchMatch = searchMatchColumns?.has(columnId) ?? false;\n const isActiveSearchMatch = activeSearchMatch?.columnId === columnId;\n\n const nextCell = visibleCells[colIndex + 1];\n const isLastColumn = colIndex === visibleCells.length - 1;\n const { showEndBorder, showStartBorder } = getColumnBorderVisibility({\n column: cell.column,\n nextColumn: nextCell?.column,\n isLastColumn,\n });\n\n return (\n \n {typeof cell.column.columnDef.header === \"function\" ? (\n \n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n \n ) : (\n \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\";\n\nimport { ChevronDown, ChevronUp, X } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { SearchState } from \"@/lib/data-grid-types\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { useAsRef } from \"@/hooks/use-as-ref\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\n\nfunction onTriggerPointerDown(event: React.PointerEvent) {\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 // Prevent the trigger from stealing focus away from the input\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\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 // Exclude searchQuery because the input is uncontrolled, and hasQuery state handles the status text\n if (prev.matchIndex !== next.matchIndex) return false;\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 propsRef = useAsRef({\n onSearchOpenChange,\n onSearchQueryChange,\n onSearch,\n onNavigateToNextMatch,\n onNavigateToPrevMatch,\n });\n\n const inputRef = React.useRef(null);\n const isComposingRef = React.useRef(false);\n const [hasQuery, setHasQuery] = React.useState(searchQuery.length > 0);\n\n React.useEffect(() => {\n if (searchOpen) {\n requestAnimationFrame(() => {\n inputRef.current?.focus();\n });\n return;\n }\n\n isComposingRef.current = false;\n setHasQuery(false);\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 propsRef.current.onSearchOpenChange(false);\n }\n }\n\n document.addEventListener(\"keydown\", onEscape);\n return () => document.removeEventListener(\"keydown\", onEscape);\n }, [searchOpen, propsRef]);\n\n const debouncedSearch = useDebouncedCallback((query: string) => {\n propsRef.current.onSearch(query);\n }, 150);\n\n function onCompositionStart() {\n isComposingRef.current = true;\n }\n\n function onCompositionEnd(event: React.CompositionEvent) {\n isComposingRef.current = false;\n const value = event.currentTarget.value;\n setHasQuery(value.length > 0);\n propsRef.current.onSearchQueryChange(value);\n debouncedSearch(value);\n }\n\n function onKeyDown(event: React.KeyboardEvent) {\n event.stopPropagation();\n\n if (event.key === \"Enter\") {\n if (event.nativeEvent.isComposing) return;\n event.preventDefault();\n if (event.shiftKey) {\n propsRef.current.onNavigateToPrevMatch();\n } else {\n propsRef.current.onNavigateToNextMatch();\n }\n }\n }\n\n function onChange(event: React.ChangeEvent) {\n if (isComposingRef.current) return;\n const value = event.target.value;\n setHasQuery(value.length > 0);\n propsRef.current.onSearchQueryChange(value);\n debouncedSearch(value);\n }\n\n function onClose() {\n propsRef.current.onSearchOpenChange(false);\n }\n\n function onPrevMatch() {\n propsRef.current.onNavigateToPrevMatch();\n }\n\n function onNextMatch() {\n propsRef.current.onNavigateToNextMatch();\n }\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 ) : hasQuery ? (\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" - }, - { - "path": "src/components/ui/direction.tsx", - "content": "\"use client\";\n\nimport type * as React from \"react\";\n\nimport { Direction } from \"radix-ui\";\n\nfunction DirectionProvider({\n dir,\n direction,\n children,\n}: React.ComponentProps & {\n direction?: React.ComponentProps[\"dir\"];\n}) {\n return (\n \n {children}\n \n );\n}\n\nconst useDirection = Direction.useDirection;\n\nexport { DirectionProvider, useDirection };\n", - "type": "registry:ui", - "target": "src/components/ui/direction.tsx" - }, - { - "path": "src/hooks/use-as-ref.ts", - "content": "import * as React from \"react\";\n\nimport { useIsomorphicLayoutEffect } from \"@/hooks/use-isomorphic-layout-effect\";\n\nfunction useAsRef(props: T) {\n const ref = React.useRef(props);\n\n useIsomorphicLayoutEffect(() => {\n ref.current = props;\n });\n\n return ref;\n}\n\nexport { useAsRef };\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-badge-overflow.ts", - "content": "import * as React from \"react\";\n\nconst badgeWidthCache = new Map();\n\nconst DEFAULT_CONTAINER_PADDING = 16; // px-2 = 8px * 2\nconst DEFAULT_BADGE_GAP = 4; // gap-1 = 4px\nconst DEFAULT_OVERFLOW_BADGE_WIDTH = 40; // Approximate width of \"+N\" badge\n\ninterface MeasureBadgeWidthProps {\n label: string;\n cacheKey: string;\n iconSize?: number;\n maxWidth?: number;\n className?: string;\n}\n\nfunction measureBadgeWidth({\n label,\n cacheKey,\n iconSize,\n maxWidth,\n className,\n}: MeasureBadgeWidthProps): number {\n const cached = badgeWidthCache.get(cacheKey);\n if (cached !== undefined) {\n return cached;\n }\n\n const measureEl = document.createElement(\"div\");\n measureEl.className = `inline-flex items-center rounded-md border px-1.5 text-xs font-semibold h-5 gap-1 shrink-0 absolute invisible pointer-events-none ${\n className ?? \"\"\n }`;\n measureEl.style.whiteSpace = \"nowrap\";\n\n if (iconSize) {\n const icon = document.createElement(\"span\");\n icon.className = \"shrink-0\";\n icon.style.width = `${iconSize}px`;\n icon.style.height = `${iconSize}px`;\n measureEl.appendChild(icon);\n }\n\n if (maxWidth) {\n const text = document.createElement(\"span\");\n text.className = \"truncate\";\n text.style.maxWidth = `${maxWidth}px`;\n text.textContent = label;\n measureEl.appendChild(text);\n } else {\n measureEl.textContent = label;\n }\n\n document.body.appendChild(measureEl);\n const width = measureEl.offsetWidth;\n document.body.removeChild(measureEl);\n\n badgeWidthCache.set(cacheKey, width);\n return width;\n}\n\ninterface UseBadgeOverflowProps {\n items: T[];\n getLabel: (item: T) => string;\n containerRef: React.RefObject;\n lineCount: number;\n cacheKeyPrefix?: string;\n iconSize?: number;\n maxWidth?: number;\n className?: string;\n containerPadding?: number;\n badgeGap?: number;\n overflowBadgeWidth?: number;\n}\n\ninterface UseBadgeOverflowReturn {\n visibleItems: T[];\n hiddenCount: number;\n containerWidth: number;\n}\n\nexport function useBadgeOverflow({\n items,\n getLabel,\n containerRef,\n lineCount,\n cacheKeyPrefix = \"\",\n containerPadding = DEFAULT_CONTAINER_PADDING,\n badgeGap = DEFAULT_BADGE_GAP,\n overflowBadgeWidth = DEFAULT_OVERFLOW_BADGE_WIDTH,\n iconSize,\n maxWidth,\n className,\n}: UseBadgeOverflowProps): UseBadgeOverflowReturn {\n const [containerWidth, setContainerWidth] = React.useState(0);\n\n React.useEffect(() => {\n if (!containerRef.current) return;\n\n function measureWidth() {\n if (containerRef.current) {\n const width = containerRef.current.clientWidth - containerPadding;\n setContainerWidth(width);\n }\n }\n\n measureWidth();\n\n const resizeObserver = new ResizeObserver(measureWidth);\n resizeObserver.observe(containerRef.current);\n\n return () => {\n resizeObserver.disconnect();\n };\n }, [containerRef, containerPadding]);\n\n const result = React.useMemo(() => {\n if (!containerWidth || items.length === 0) {\n return { visibleItems: items, hiddenCount: 0, containerWidth };\n }\n\n let currentLineWidth = 0;\n let currentLine = 1;\n const visible: T[] = [];\n\n for (const item of items) {\n const label = getLabel(item);\n const cacheKey = cacheKeyPrefix ? `${cacheKeyPrefix}:${label}` : label;\n const badgeWidth = measureBadgeWidth({\n label,\n cacheKey,\n iconSize,\n maxWidth,\n className,\n });\n const widthWithGap = badgeWidth + badgeGap;\n\n if (currentLineWidth + widthWithGap <= containerWidth) {\n currentLineWidth += widthWithGap;\n visible.push(item);\n } else if (currentLine < lineCount) {\n currentLine++;\n currentLineWidth = widthWithGap;\n visible.push(item);\n } else {\n if (\n currentLineWidth + overflowBadgeWidth > containerWidth &&\n visible.length > 0\n ) {\n visible.pop();\n }\n\n break;\n }\n }\n\n return {\n visibleItems: visible,\n hiddenCount: Math.max(0, items.length - visible.length),\n containerWidth,\n };\n }, [\n items,\n getLabel,\n containerWidth,\n lineCount,\n cacheKeyPrefix,\n iconSize,\n maxWidth,\n className,\n badgeGap,\n overflowBadgeWidth,\n ]);\n\n return result;\n}\n\nexport function clearBadgeWidthCache(): void {\n badgeWidthCache.clear();\n}\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-callback-ref.ts", - "content": "import * as React from \"react\";\n\n/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx\n */\n\n/**\n * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a\n * prop or avoid re-executing effects when passed as a dependency\n */\nfunction useCallbackRef unknown>(\n callback: T | undefined,\n): T {\n const callbackRef = React.useRef(callback);\n\n React.useEffect(() => {\n callbackRef.current = callback;\n });\n\n // https://github.com/facebook/react/issues/19240\n return React.useMemo(\n () => ((...args) => callbackRef.current?.(...args)) as T,\n [],\n );\n}\n\nexport { useCallbackRef };\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-data-grid.ts", - "content": "import {\n type ColumnDef,\n type ColumnFiltersState,\n type Row,\n type RowData,\n type RowSelectionState,\n type SortingState,\n type TableOptions,\n type TableState,\n type Updater,\n useTable,\n} from \"@tanstack/react-table\";\nimport { useVirtualizer, type Virtualizer } from \"@tanstack/react-virtual\";\nimport * as React from \"react\";\nimport { toast } from \"sonner\";\n\nimport type {\n CellPosition,\n CellUpdate,\n ContextMenuState,\n DataGridTableMeta,\n Direction,\n FileCellData,\n NavigationDirection,\n PasteDialogState,\n RowHeightValue,\n SearchState,\n SelectionState,\n} from \"@/lib/data-grid-types\";\n\nimport { useDirection } from \"@/components/ui/direction\";\nimport { useAsRef } from \"@/hooks/use-as-ref\";\nimport { useIsomorphicLayoutEffect } from \"@/hooks/use-isomorphic-layout-effect\";\nimport { useLazyRef } from \"@/hooks/use-lazy-ref\";\nimport {\n type DataGridFeatures,\n dataGridFeatures,\n} from \"@/lib/data-grid-features\";\nimport {\n getCellKey,\n getEmptyCellValue,\n getIsFileCellData,\n getIsInPopover,\n getRowHeightValue,\n getScrollDirection,\n matchSelectOption,\n parseCellKey,\n parseTsv,\n scrollCellIntoView,\n stringifyUnknown,\n} from \"@/lib/data-grid-utils\";\n\nconst DEFAULT_ROW_HEIGHT = \"short\";\nconst OVERSCAN = 6;\nconst VIEWPORT_OFFSET = 1;\nconst HORIZONTAL_PAGE_SIZE = 5;\nconst SCROLL_SYNC_RETRY_COUNT = 16;\nconst MIN_COLUMN_SIZE = 60;\nconst MAX_COLUMN_SIZE = 800;\nconst SEARCH_SHORTCUT_KEY = \"f\";\nconst NON_NAVIGABLE_COLUMN_IDS = new Set([\"select\", \"actions\"]);\nconst AUTO_SCROLL_EDGE_ZONE = 50;\nconst AUTO_SCROLL_SPEED_RAMP_ZONE = AUTO_SCROLL_EDGE_ZONE * 3;\nconst AUTO_SCROLL_MIN_SPEED = 8;\nconst AUTO_SCROLL_MAX_SPEED = 40;\nconst AUTO_SCROLL_SELECTION_THROTTLE_MS = 32;\n\nconst DOMAIN_REGEX = /^[\\w.-]+\\.[a-z]{2,}(\\/\\S*)?$/i;\nconst ISO_DATE_REGEX = /^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}.*)?$/;\nconst TRUTHY_BOOLEANS = new Set([\"true\", \"1\", \"yes\", \"checked\"]);\nconst VALID_BOOLEANS = new Set([\n \"true\",\n \"false\",\n \"1\",\n \"0\",\n \"yes\",\n \"no\",\n \"checked\",\n \"unchecked\",\n]);\n\nfunction setRowSelected(\n rowSelection: RowSelectionState,\n rowId: string,\n selected: boolean,\n) {\n if (selected) {\n rowSelection[rowId] = true;\n } else {\n delete rowSelection[rowId];\n }\n}\n\ninterface DataGridState {\n sorting: SortingState;\n columnFilters: ColumnFiltersState;\n rowHeight: RowHeightValue;\n rowSelection: RowSelectionState;\n selectionState: SelectionState;\n focusedCell: CellPosition | null;\n editingCell: CellPosition | null;\n cutCells: Set;\n contextMenu: ContextMenuState;\n searchQuery: string;\n searchMatches: CellPosition[];\n matchIndex: number;\n searchOpen: boolean;\n lastClickedRowId: string | null;\n pasteDialog: PasteDialogState;\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 extends Omit<\n TableOptions,\n \"features\"\n> {\n onDataChange?: (data: TData[]) => void;\n onRowAdd?: (\n event?: React.MouseEvent,\n ) => Partial | Promise | null> | null;\n onRowsAdd?: (count: number) => void | Promise;\n onRowsDelete?: (rows: TData[], rowIndices: number[]) => void | Promise;\n onPaste?: (updates: Array) => void | Promise;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (rowHeight: RowHeightValue) => void;\n overscan?: number;\n dir?: Direction;\n autoFocus?: boolean | Partial;\n enableSingleCellSelection?: boolean;\n enableColumnSelection?: boolean;\n enableSearch?: boolean;\n enablePaste?: boolean;\n readOnly?: boolean;\n}\n\nfunction useDataGrid({\n data,\n columns,\n rowHeight: rowHeightProp = DEFAULT_ROW_HEIGHT,\n overscan = OVERSCAN,\n dir: dirProp,\n initialState,\n ...props\n}: UseDataGridProps) {\n const dir = useDirection(dirProp);\n const dataGridRef = React.useRef(null);\n const tableRef =\n 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 cellMapRef = React.useRef>(new Map());\n const footerRef = React.useRef(null);\n const focusGuardRef = React.useRef(false);\n\n const propsRef = useAsRef({\n ...props,\n data,\n columns,\n initialState,\n });\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n\n const stateRef = useLazyRef(() => {\n return {\n sorting: initialState?.sorting ?? [],\n columnFilters: initialState?.columnFilters ?? [],\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 cutCells: new Set(),\n contextMenu: {\n open: false,\n x: 0,\n y: 0,\n },\n searchQuery: \"\",\n searchMatches: [],\n matchIndex: -1,\n searchOpen: false,\n lastClickedRowId: null,\n pasteDialog: {\n open: false,\n rowsNeeded: 0,\n clipboardText: \"\",\n },\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 columnFilters = useStore(store, (state) => state.columnFilters);\n const rowSelection = useStore(store, (state) => state.rowSelection);\n const rowHeight = useStore(store, (state) => state.rowHeight);\n const contextMenu = useStore(store, (state) => state.contextMenu);\n const pasteDialog = useStore(store, (state) => state.pasteDialog);\n\n const rowHeightValue = getRowHeightValue(rowHeight);\n\n const prevCellSelectionMapRef = useLazyRef(\n () => new Map>(),\n );\n\n // Memoize per-row selection sets to prevent unnecessary row re-renders\n // Each row gets a stable Set reference that only changes when its cells' selection changes\n const cellSelectionMap = React.useMemo(() => {\n const selectedCells = selectionState.selectedCells;\n\n if (selectedCells.size === 0) {\n prevCellSelectionMapRef.current.clear();\n return null;\n }\n\n const newRowCells = new Map>();\n for (const cellKey of selectedCells) {\n const { rowIndex } = parseCellKey(cellKey);\n let rowSet = newRowCells.get(rowIndex);\n if (!rowSet) {\n rowSet = new Set();\n newRowCells.set(rowIndex, rowSet);\n }\n rowSet.add(cellKey);\n }\n\n const stableMap = new Map>();\n for (const [rowIndex, newSet] of newRowCells) {\n const prevSet = prevCellSelectionMapRef.current.get(rowIndex);\n if (\n prevSet &&\n prevSet.size === newSet.size &&\n [...newSet].every((key) => prevSet.has(key))\n ) {\n stableMap.set(rowIndex, prevSet);\n } else {\n stableMap.set(rowIndex, newSet);\n }\n }\n\n prevCellSelectionMapRef.current = stableMap;\n return stableMap;\n }, [selectionState.selectedCells, prevCellSelectionMapRef]);\n\n const visualRowIndexCacheRef = React.useRef<{\n rows: Row[] | null;\n map: Map;\n } | null>(null);\n\n // Pre-compute visual row index map for O(1) lookups (used by select column)\n // Cache is invalidated when row model identity changes (sorting/filtering)\n const getVisualRowIndex = React.useCallback(\n (rowId: string): number | undefined => {\n const rows = tableRef.current?.getRowModel().rows;\n if (!rows) return undefined;\n\n if (visualRowIndexCacheRef.current?.rows !== rows) {\n const map = new Map();\n for (const [i, row] of rows.entries()) {\n map.set(row.id, i + 1);\n }\n visualRowIndexCacheRef.current = { rows, map };\n }\n\n return visualRowIndexCacheRef.current.map.get(rowId);\n },\n [],\n );\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.has(c));\n }, [columnIds]);\n\n const onDataUpdate = React.useCallback(\n (updates: CellUpdate | Array) => {\n if (propsRef.current.readOnly) return;\n\n const updateArray = Array.isArray(updates) ? updates : [updates];\n\n if (updateArray.length === 0) return;\n\n const currentTable = tableRef.current;\n const currentData = propsRef.current.data;\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 = currentData.indexOf(originalData);\n\n const targetIndex =\n originalRowIndex !== -1 ? originalRowIndex : update.rowIndex;\n\n const existingUpdates = rowUpdatesMap.get(targetIndex) ?? [];\n existingUpdates.push({\n columnId: update.columnId,\n value: update.value,\n });\n rowUpdatesMap.set(targetIndex, existingUpdates);\n }\n }\n\n const maxUpdateIndex =\n rowUpdatesMap.size > 0\n ? Math.max(...Array.from(rowUpdatesMap.keys()))\n : -1;\n const dataLength = Math.max(currentData.length, maxUpdateIndex + 1);\n\n const newData: TData[] = Array.from({ length: dataLength });\n\n for (let i = 0; i < dataLength; i++) {\n const updates = rowUpdatesMap.get(i);\n // Fall back to the table's row data for rows not yet in currentData\n const existingRow = currentData[i] ?? rows?.[i]?.original;\n\n if (existingRow == null) continue;\n\n if (updates) {\n const updatedRow = Object.assign(\n {},\n existingRow as Record,\n );\n for (const { columnId, value } of updates) {\n updatedRow[columnId] = value;\n }\n newData[i] = updatedRow as TData;\n } else {\n newData[i] = existingRow;\n }\n }\n\n propsRef.current.onDataChange?.(newData);\n },\n [propsRef],\n );\n\n const getIsCellSelected = React.useCallback(\n (rowIndex: number, columnId: string) => {\n const currentSelectionState = store.getState().selectionState;\n return currentSelectionState.selectedCells.has(\n getCellKey(rowIndex, columnId),\n );\n },\n [store],\n );\n\n const onSelectionClear = 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 ?? propsRef.current.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, propsRef, 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 ?? propsRef.current.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 [propsRef, 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 dragDepsRef = useAsRef({\n selectRange,\n dir,\n rowHeightValue,\n columnIds,\n });\n\n const serializeCellsToTsv = React.useCallback(() => {\n const currentState = store.getState();\n\n let selectedCellsArray: string[];\n if (!currentState.selectionState.selectedCells.size) {\n if (!currentState.focusedCell) return null;\n const focusedCellKey = getCellKey(\n currentState.focusedCell.rowIndex,\n currentState.focusedCell.columnId,\n );\n selectedCellsArray = [focusedCellKey];\n } else {\n selectedCellsArray = Array.from(\n currentState.selectionState.selectedCells,\n );\n }\n\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows;\n if (!rows) return null;\n\n const selectedColumnIds: string[] = [];\n const seenColumnIds = new Set();\n const cellData = new Map();\n const rowIndices = new Set();\n const rowCellMaps = new Map<\n number,\n Map<\n string,\n ReturnType[\"getVisibleCells\"]>[number]\n >\n >();\n const navigableCells: string[] = [];\n\n for (const cellKey of selectedCellsArray) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n\n if (columnId && NON_NAVIGABLE_COLUMN_IDS.has(columnId)) {\n continue;\n }\n\n navigableCells.push(cellKey);\n\n if (columnId && !seenColumnIds.has(columnId)) {\n seenColumnIds.add(columnId);\n selectedColumnIds.push(columnId);\n }\n\n rowIndices.add(rowIndex);\n\n const row = rows[rowIndex];\n if (row) {\n let cellMap = rowCellMaps.get(rowIndex);\n if (!cellMap) {\n cellMap = new Map(row.getVisibleCells().map((c) => [c.column.id, c]));\n rowCellMaps.set(rowIndex, cellMap);\n }\n const cell = cellMap.get(columnId);\n if (cell) {\n const value = cell.getValue();\n const cellVariant = cell.column.columnDef?.meta?.cell?.variant;\n\n let serializedValue = \"\";\n if (cellVariant === \"file\" || cellVariant === \"multi-select\") {\n serializedValue = value ? JSON.stringify(value) : \"\";\n } else if (value instanceof Date) {\n serializedValue = value.toISOString();\n } else {\n serializedValue = stringifyUnknown(value);\n }\n\n cellData.set(cellKey, serializedValue);\n }\n }\n }\n\n const colIndices = new Set();\n for (const cellKey of navigableCells) {\n const { columnId } = parseCellKey(cellKey);\n const colIndex = selectedColumnIds.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) => selectedColumnIds[i]);\n\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 return { tsvData, selectedCellsArray: navigableCells };\n }, [store]);\n\n const onCellsCopy = React.useCallback(async () => {\n const result = serializeCellsToTsv();\n if (!result) return;\n\n const { tsvData, selectedCellsArray } = result;\n\n try {\n await navigator.clipboard.writeText(tsvData);\n\n const currentState = store.getState();\n if (currentState.cutCells.size > 0) {\n store.setState(\"cutCells\", new Set());\n }\n\n toast.success(\n `${selectedCellsArray.length} cell${\n selectedCellsArray.length !== 1 ? \"s\" : \"\"\n } copied`,\n );\n } catch (error) {\n toast.error(\n error instanceof Error ? error.message : \"Failed to copy to clipboard\",\n );\n }\n }, [store, serializeCellsToTsv]);\n\n const onCellsCut = React.useCallback(async () => {\n if (propsRef.current.readOnly) return;\n\n const result = serializeCellsToTsv();\n if (!result) return;\n\n const { tsvData, selectedCellsArray } = result;\n\n try {\n await navigator.clipboard.writeText(tsvData);\n\n store.setState(\"cutCells\", new Set(selectedCellsArray));\n\n toast.success(\n `${selectedCellsArray.length} cell${\n selectedCellsArray.length !== 1 ? \"s\" : \"\"\n } cut`,\n );\n } catch (error) {\n toast.error(\n error instanceof Error ? error.message : \"Failed to cut to clipboard\",\n );\n }\n }, [store, propsRef, serializeCellsToTsv]);\n\n const restoreFocus = React.useCallback((element: HTMLDivElement | null) => {\n if (element && document.activeElement !== element) {\n requestAnimationFrame(() => {\n element.focus();\n });\n }\n }, []);\n\n const onCellsPaste = React.useCallback(\n async (expandRows = false) => {\n if (propsRef.current.readOnly) return;\n\n const currentState = store.getState();\n if (!currentState.focusedCell) return;\n\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows;\n if (!rows) return;\n\n try {\n let clipboardText = currentState.pasteDialog.clipboardText;\n\n if (!clipboardText) {\n clipboardText = await navigator.clipboard.readText();\n if (!clipboardText) return;\n }\n\n const rawPastedData = parseTsv(\n clipboardText,\n navigableColumnIds.length,\n );\n\n // Fill entire selection when clipboard has a single value and multiple cells are selected\n const selectionCells = currentState.selectionState.selectedCells;\n const isSingleCellClipboard =\n rawPastedData.length === 1 && (rawPastedData[0]?.length ?? 0) === 1;\n\n let pastedData = rawPastedData;\n let startRowIndex = currentState.focusedCell.rowIndex;\n let startColIndex = navigableColumnIds.indexOf(\n currentState.focusedCell.columnId,\n );\n\n if (isSingleCellClipboard && selectionCells.size > 1) {\n const singleValue = rawPastedData[0]?.[0] ?? \"\";\n let minRow = Infinity;\n let maxRow = -Infinity;\n let minColIdx = Infinity;\n let maxColIdx = -Infinity;\n\n for (const cellKey of selectionCells) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n const colIdx = navigableColumnIds.indexOf(columnId);\n if (colIdx === -1) continue;\n minRow = Math.min(minRow, rowIndex);\n maxRow = Math.max(maxRow, rowIndex);\n minColIdx = Math.min(minColIdx, colIdx);\n maxColIdx = Math.max(maxColIdx, colIdx);\n }\n\n if (minRow !== Infinity) {\n startRowIndex = minRow;\n startColIndex = minColIdx;\n const numRows = maxRow - minRow + 1;\n const numCols = maxColIdx - minColIdx + 1;\n pastedData = Array.from({ length: numRows }, () =>\n Array.from({ length: numCols }, () => singleValue),\n );\n }\n }\n\n if (startColIndex === -1) return;\n\n const rowCount = rows.length ?? propsRef.current.data.length;\n const rowsNeeded = startRowIndex + pastedData.length - rowCount;\n\n if (\n rowsNeeded > 0 &&\n !expandRows &&\n propsRef.current.onRowAdd &&\n !currentState.pasteDialog.clipboardText\n ) {\n store.setState(\"pasteDialog\", {\n open: true,\n rowsNeeded,\n clipboardText,\n });\n return;\n }\n\n if (expandRows && rowsNeeded > 0) {\n const expectedRowCount = rowCount + rowsNeeded;\n\n if (propsRef.current.onRowsAdd) {\n await propsRef.current.onRowsAdd(rowsNeeded);\n } else if (propsRef.current.onRowAdd) {\n for (let i = 0; i < rowsNeeded; i++) {\n await propsRef.current.onRowAdd();\n }\n }\n\n let attempts = 0;\n const maxAttempts = 50;\n let currentTableRowCount =\n tableRef.current?.getRowModel().rows.length ?? 0;\n\n while (\n currentTableRowCount < expectedRowCount &&\n attempts < maxAttempts\n ) {\n await new Promise((resolve) => setTimeout(resolve, 100));\n currentTableRowCount =\n tableRef.current?.getRowModel().rows.length ?? 0;\n attempts++;\n }\n }\n\n const updates: Array = [];\n const tableColumns = currentTable?.getAllColumns() ?? [];\n let cellsUpdated = 0;\n let endRowIndex = startRowIndex;\n let endColIndex = startColIndex;\n\n const updatedTable = tableRef.current;\n const updatedRows = updatedTable?.getRowModel().rows;\n const currentRowCount = updatedRows?.length ?? 0;\n\n let cellsSkipped = 0;\n\n const columnMap = new Map(tableColumns.map((c) => [c.id, c]));\n\n for (\n let pasteRowIdx = 0;\n pasteRowIdx < pastedData.length;\n pasteRowIdx++\n ) {\n const pasteRow = pastedData[pasteRowIdx];\n if (!pasteRow) continue;\n\n const targetRowIndex = startRowIndex + pasteRowIdx;\n if (targetRowIndex >= currentRowCount) break;\n\n for (\n let pasteColIdx = 0;\n pasteColIdx < pasteRow.length;\n pasteColIdx++\n ) {\n const targetColIndex = startColIndex + pasteColIdx;\n if (targetColIndex >= navigableColumnIds.length) break;\n\n const targetColumnId = navigableColumnIds[targetColIndex];\n if (!targetColumnId) continue;\n\n const pastedValue = pasteRow[pasteColIdx] ?? \"\";\n const column = columnMap.get(targetColumnId);\n const cellOpts = column?.columnDef?.meta?.cell;\n const cellVariant = cellOpts?.variant;\n\n let processedValue: unknown = pastedValue;\n let shouldSkip = false;\n\n switch (cellVariant) {\n case \"number\": {\n if (!pastedValue) {\n processedValue = null;\n } else {\n const num = Number.parseFloat(pastedValue);\n if (Number.isNaN(num)) shouldSkip = true;\n else processedValue = num;\n }\n break;\n }\n\n case \"checkbox\": {\n if (!pastedValue) {\n processedValue = false;\n } else {\n const lower = pastedValue.toLowerCase();\n if (VALID_BOOLEANS.has(lower)) {\n processedValue = TRUTHY_BOOLEANS.has(lower);\n } else {\n shouldSkip = true;\n }\n }\n break;\n }\n\n case \"date\": {\n if (!pastedValue) {\n processedValue = null;\n } else {\n const date = new Date(pastedValue);\n if (Number.isNaN(date.getTime())) shouldSkip = true;\n else processedValue = date;\n }\n break;\n }\n\n case \"select\": {\n const options = cellOpts?.options ?? [];\n if (!pastedValue) {\n processedValue = null;\n } else {\n const matched = matchSelectOption(pastedValue, options);\n if (matched) processedValue = matched;\n else shouldSkip = true;\n }\n break;\n }\n\n case \"multi-select\": {\n const options = cellOpts?.options ?? [];\n let values: string[] = [];\n try {\n const parsed = JSON.parse(pastedValue);\n if (Array.isArray(parsed)) {\n values = parsed.filter(\n (v): v is string => typeof v === \"string\",\n );\n }\n } catch {\n values = pastedValue\n ? pastedValue.split(\",\").map((v) => v.trim())\n : [];\n }\n\n const validated = values\n .map((v) => matchSelectOption(v, options))\n .filter(Boolean) as string[];\n\n if (values.length > 0 && validated.length === 0) {\n shouldSkip = true;\n } else {\n processedValue = validated;\n }\n break;\n }\n\n case \"file\": {\n if (!pastedValue) {\n processedValue = [];\n } else {\n try {\n const parsed = JSON.parse(pastedValue);\n if (!Array.isArray(parsed)) {\n shouldSkip = true;\n } else {\n const validFiles = parsed.filter(getIsFileCellData);\n if (parsed.length > 0 && validFiles.length === 0) {\n shouldSkip = true;\n } else {\n processedValue = validFiles;\n }\n }\n } catch {\n shouldSkip = true;\n }\n }\n break;\n }\n\n case \"url\": {\n if (!pastedValue) {\n processedValue = \"\";\n } else {\n const firstChar = pastedValue[0];\n if (firstChar === \"[\" || firstChar === \"{\") {\n shouldSkip = true;\n } else {\n try {\n new URL(pastedValue);\n processedValue = pastedValue;\n } catch {\n if (DOMAIN_REGEX.test(pastedValue)) {\n processedValue = pastedValue;\n } else {\n shouldSkip = true;\n }\n }\n }\n }\n break;\n }\n\n default: {\n if (!pastedValue) {\n processedValue = \"\";\n break;\n }\n\n if (ISO_DATE_REGEX.test(pastedValue)) {\n const date = new Date(pastedValue);\n if (!Number.isNaN(date.getTime())) {\n processedValue = date.toLocaleDateString();\n break;\n }\n }\n\n const firstChar = pastedValue[0];\n if (\n firstChar === \"[\" ||\n firstChar === \"{\" ||\n firstChar === \"t\" ||\n firstChar === \"f\"\n ) {\n try {\n const parsed = JSON.parse(pastedValue);\n\n if (Array.isArray(parsed)) {\n if (\n parsed.length > 0 &&\n parsed.every(getIsFileCellData)\n ) {\n processedValue = parsed.map((f) => f.name).join(\", \");\n } else if (parsed.every((v) => typeof v === \"string\")) {\n processedValue = (parsed as string[]).join(\", \");\n }\n } else if (typeof parsed === \"boolean\") {\n processedValue = parsed ? \"Checked\" : \"Unchecked\";\n }\n } catch {\n const lower = pastedValue.toLowerCase();\n if (lower === \"true\" || lower === \"false\") {\n processedValue =\n lower === \"true\" ? \"Checked\" : \"Unchecked\";\n }\n }\n }\n }\n }\n\n if (shouldSkip) {\n cellsSkipped++;\n endRowIndex = Math.max(endRowIndex, targetRowIndex);\n endColIndex = Math.max(endColIndex, targetColIndex);\n continue;\n }\n\n updates.push({\n rowIndex: targetRowIndex,\n columnId: targetColumnId,\n value: processedValue,\n });\n cellsUpdated++;\n\n endRowIndex = Math.max(endRowIndex, targetRowIndex);\n endColIndex = Math.max(endColIndex, targetColIndex);\n }\n }\n\n if (updates.length > 0) {\n if (propsRef.current.onPaste) {\n await propsRef.current.onPaste(updates);\n }\n\n const allUpdates = [...updates];\n\n if (currentState.cutCells.size > 0) {\n const columnById = new Map(tableColumns.map((c) => [c.id, c]));\n\n for (const cellKey of currentState.cutCells) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n const column = columnById.get(columnId);\n const cellVariant = column?.columnDef?.meta?.cell?.variant;\n const emptyValue = getEmptyCellValue(cellVariant);\n allUpdates.push({ rowIndex, columnId, value: emptyValue });\n }\n\n store.setState(\"cutCells\", new Set());\n }\n\n onDataUpdate(allUpdates);\n\n if (cellsSkipped > 0) {\n toast.success(\n `${cellsUpdated} cell${\n cellsUpdated !== 1 ? \"s\" : \"\"\n } pasted, ${cellsSkipped} skipped`,\n );\n } else {\n toast.success(\n `${cellsUpdated} cell${cellsUpdated !== 1 ? \"s\" : \"\"} pasted`,\n );\n }\n\n const endColumnId = navigableColumnIds[endColIndex];\n if (endColumnId) {\n selectRange(\n {\n rowIndex: startRowIndex,\n columnId: currentState.focusedCell.columnId,\n },\n { rowIndex: endRowIndex, columnId: endColumnId },\n );\n }\n\n restoreFocus(dataGridRef.current);\n } else if (cellsSkipped > 0) {\n toast.error(\n `${cellsSkipped} cell${\n cellsSkipped !== 1 ? \"s\" : \"\"\n } skipped pasting for invalid data`,\n );\n }\n\n if (currentState.pasteDialog.open) {\n store.setState(\"pasteDialog\", {\n open: false,\n rowsNeeded: 0,\n clipboardText: \"\",\n });\n }\n } catch (error) {\n toast.error(\n error instanceof Error\n ? error.message\n : \"Failed to paste. Please try again.\",\n );\n }\n },\n [\n store,\n navigableColumnIds,\n propsRef,\n onDataUpdate,\n selectRange,\n restoreFocus,\n ],\n );\n\n // Release focus guard after delay to allow async data re-renders to settle.\n // 300ms accounts for db sync and virtualized cell mounting\n const releaseFocusGuard = React.useCallback((immediate = false) => {\n if (immediate) {\n focusGuardRef.current = false;\n return;\n }\n\n setTimeout(() => {\n focusGuardRef.current = false;\n }, 300);\n }, []);\n\n const focusCellWrapper = React.useCallback(\n (rowIndex: number, columnId: string) => {\n focusGuardRef.current = true;\n\n requestAnimationFrame(() => {\n const cellKey = getCellKey(rowIndex, columnId);\n const cellWrapperElement = cellMapRef.current.get(cellKey);\n\n if (!cellWrapperElement) {\n const container = dataGridRef.current;\n if (container) {\n container.focus();\n }\n releaseFocusGuard();\n return;\n }\n\n cellWrapperElement.focus();\n releaseFocusGuard();\n });\n },\n [releaseFocusGuard],\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 focusCellWrapper(rowIndex, columnId);\n },\n [store, focusCellWrapper],\n );\n\n const onRowsDelete = React.useCallback(\n async (rowIndices: number[]) => {\n if (\n propsRef.current.readOnly ||\n !propsRef.current.onRowsDelete ||\n rowIndices.length === 0\n )\n return;\n\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows;\n\n if (!rows || rows.length === 0) return;\n\n const currentState = store.getState();\n const currentFocusedColumn =\n currentState.focusedCell?.columnId ?? navigableColumnIds[0];\n\n const minDeletedRowIndex = Math.min(...rowIndices);\n\n const rowsToDelete: TData[] = [];\n for (const rowIndex of rowIndices) {\n const row = rows[rowIndex];\n if (row) {\n rowsToDelete.push(row.original);\n }\n }\n\n await propsRef.current.onRowsDelete(rowsToDelete, rowIndices);\n\n store.batch(() => {\n store.setState(\"selectionState\", {\n selectedCells: new Set(),\n selectionRange: null,\n isSelecting: false,\n });\n store.setState(\"rowSelection\", {});\n store.setState(\"editingCell\", null);\n });\n\n requestAnimationFrame(() => {\n const currentTable = tableRef.current;\n const currentRows = currentTable?.getRowModel().rows ?? [];\n const newRowCount = currentRows.length ?? propsRef.current.data.length;\n\n if (newRowCount > 0 && currentFocusedColumn) {\n const targetRowIndex = Math.min(minDeletedRowIndex, newRowCount - 1);\n focusCell(targetRowIndex, currentFocusedColumn);\n }\n });\n },\n [propsRef, store, navigableColumnIds, focusCell],\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 ?? propsRef.current.data.length;\n\n let newRowIndex = rowIndex;\n let newColumnId = columnId;\n\n const isRtl = dir === \"rtl\";\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 (isRtl) {\n if (currentColIndex < navigableColumnIds.length - 1) {\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\n if (nextColumnId) newColumnId = nextColumnId;\n }\n } else {\n if (currentColIndex > 0) {\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\n if (prevColumnId) newColumnId = prevColumnId;\n }\n }\n break;\n case \"right\":\n if (isRtl) {\n if (currentColIndex > 0) {\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\n if (prevColumnId) newColumnId = prevColumnId;\n }\n } else {\n if (currentColIndex < navigableColumnIds.length - 1) {\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\n if (nextColumnId) newColumnId = nextColumnId;\n }\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 \"ctrl+up\":\n newRowIndex = 0;\n break;\n case \"ctrl+down\":\n newRowIndex = Math.max(0, rowCount - 1);\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 case \"pageleft\":\n if (currentColIndex > 0) {\n const targetIndex = Math.max(\n 0,\n currentColIndex - HORIZONTAL_PAGE_SIZE,\n );\n const targetColumnId = navigableColumnIds[targetIndex];\n if (targetColumnId) newColumnId = targetColumnId;\n }\n break;\n case \"pageright\":\n if (currentColIndex < navigableColumnIds.length - 1) {\n const targetIndex = Math.min(\n navigableColumnIds.length - 1,\n currentColIndex + HORIZONTAL_PAGE_SIZE,\n );\n const targetColumnId = navigableColumnIds[targetIndex];\n if (targetColumnId) newColumnId = targetColumnId;\n }\n break;\n }\n\n if (newRowIndex !== rowIndex || newColumnId !== columnId) {\n focusCell(newRowIndex, newColumnId);\n\n // Calculate and apply scrolls synchronously to avoid flashing\n const container = dataGridRef.current;\n if (!container) return;\n\n const targetRow = rowMapRef.current.get(newRowIndex);\n const cellKey = getCellKey(newRowIndex, newColumnId);\n const targetCell = cellMapRef.current.get(cellKey);\n\n // If target row is not rendered, scroll it into view first\n if (!targetRow) {\n if (rowVirtualizer) {\n const align =\n direction === \"up\" ||\n direction === \"pageup\" ||\n direction === \"ctrl+up\" ||\n direction === \"ctrl+home\"\n ? \"start\"\n : direction === \"down\" ||\n direction === \"pagedown\" ||\n direction === \"ctrl+down\" ||\n direction === \"ctrl+end\"\n ? \"end\"\n : \"center\";\n\n rowVirtualizer.scrollToIndex(newRowIndex, { align });\n\n // Wait for row to render before horizontal scroll\n if (newColumnId !== columnId) {\n requestAnimationFrame(() => {\n const cellKeyRetry = getCellKey(newRowIndex, newColumnId);\n const targetCellRetry = cellMapRef.current.get(cellKeyRetry);\n\n if (targetCellRetry) {\n const scrollDirection = getScrollDirection(direction);\n\n scrollCellIntoView({\n container,\n targetCell: targetCellRetry,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n direction: scrollDirection,\n isRtl: dir === \"rtl\",\n });\n }\n });\n }\n } else {\n // Use direct scroll calculation when virtualizer is not available\n const rowHeightValue = getRowHeightValue(rowHeight);\n const estimatedScrollTop = newRowIndex * rowHeightValue;\n container.scrollTop = estimatedScrollTop;\n }\n\n return;\n }\n\n // Vertical scrolling for rendered rows that changed\n if (newRowIndex !== rowIndex && targetRow) {\n requestAnimationFrame(() => {\n const containerRect = container.getBoundingClientRect();\n const headerHeight =\n headerRef.current?.getBoundingClientRect().height ?? 0;\n const footerHeight =\n footerRef.current?.getBoundingClientRect().height ?? 0;\n const viewportTop =\n containerRect.top + headerHeight + VIEWPORT_OFFSET;\n const viewportBottom =\n containerRect.bottom - footerHeight - VIEWPORT_OFFSET;\n\n const rowRect = targetRow.getBoundingClientRect();\n const isFullyVisible =\n rowRect.top >= viewportTop && rowRect.bottom <= viewportBottom;\n\n if (!isFullyVisible) {\n // Only apply vertical scroll for vertical navigation\n const isVerticalNavigation =\n direction === \"up\" ||\n direction === \"down\" ||\n direction === \"pageup\" ||\n direction === \"pagedown\" ||\n direction === \"ctrl+up\" ||\n direction === \"ctrl+down\" ||\n direction === \"ctrl+home\" ||\n direction === \"ctrl+end\";\n\n if (isVerticalNavigation) {\n if (\n direction === \"down\" ||\n direction === \"pagedown\" ||\n direction === \"ctrl+down\" ||\n direction === \"ctrl+end\"\n ) {\n container.scrollTop += rowRect.bottom - viewportBottom;\n } else {\n container.scrollTop -= viewportTop - rowRect.top;\n }\n }\n }\n });\n }\n\n // Horizontal scrolling for rendered cells\n if (newColumnId !== columnId && targetCell) {\n requestAnimationFrame(() => {\n const scrollDirection = getScrollDirection(direction);\n\n scrollCellIntoView({\n container,\n targetCell,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n direction: scrollDirection,\n isRtl: dir === \"rtl\",\n });\n });\n }\n }\n },\n [dir, store, navigableColumnIds, focusCell, propsRef, rowHeight],\n );\n\n const onCellEditingStart = React.useCallback(\n (rowIndex: number, columnId: string) => {\n if (propsRef.current.readOnly) return;\n\n store.batch(() => {\n store.setState(\"focusedCell\", { rowIndex, columnId });\n store.setState(\"editingCell\", { rowIndex, columnId });\n });\n },\n [store, propsRef],\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 ?? propsRef.current.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 const { rowIndex, columnId } = currentEditing;\n focusCell(rowIndex, columnId);\n requestAnimationFrame(() => {\n navigateCell(opts.direction ?? \"right\");\n });\n } else if (currentEditing) {\n const { rowIndex, columnId } = currentEditing;\n focusCellWrapper(rowIndex, columnId);\n }\n },\n [store, propsRef, focusCell, navigateCell, focusCellWrapper],\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 const cellById = new Map(\n row.getVisibleCells().map((c) => [c.column.id, c]),\n );\n\n for (const columnId of columnIds) {\n const cell = cellById.get(columnId);\n if (!cell) continue;\n\n const value = cell.getValue();\n const stringValue = stringifyUnknown(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 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 searchMatchSet = React.useMemo(() => {\n return new Set(\n searchMatches.map((m) => getCellKey(m.rowIndex, m.columnId)),\n );\n }, [searchMatches]);\n\n const getIsSearchMatch = React.useCallback(\n (rowIndex: number, columnId: string) => {\n return searchMatchSet.has(getCellKey(rowIndex, columnId));\n },\n [searchMatchSet],\n );\n\n const getIsActiveSearchMatch = React.useCallback(\n (rowIndex: number, columnId: string) => {\n const currentState = store.getState();\n if (currentState.matchIndex < 0) return false;\n const currentMatch = currentState.searchMatches[currentState.matchIndex];\n return (\n currentMatch?.rowIndex === rowIndex &&\n currentMatch?.columnId === columnId\n );\n },\n [store],\n );\n\n // Compute search match data for targeted row re-renders\n const searchMatchesByRow = React.useMemo(() => {\n if (searchMatches.length === 0) return null;\n const rowMap = new Map>();\n for (const match of searchMatches) {\n let columnSet = rowMap.get(match.rowIndex);\n if (!columnSet) {\n columnSet = new Set();\n rowMap.set(match.rowIndex, columnSet);\n }\n columnSet.add(match.columnId);\n }\n return rowMap;\n }, [searchMatches]);\n\n const activeSearchMatch = React.useMemo(() => {\n if (matchIndex < 0 || searchMatches.length === 0) return null;\n return searchMatches[matchIndex] ?? null;\n }, [searchMatches, matchIndex]);\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 scrollToCell = React.useCallback(\n (rowIndex: number, columnId: string) => {\n requestAnimationFrame(() => {\n const container = dataGridRef.current;\n const cellKey = getCellKey(rowIndex, columnId);\n const targetCell = cellMapRef.current.get(cellKey);\n\n if (container && targetCell) {\n scrollCellIntoView({\n container,\n targetCell,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n isRtl: dir === \"rtl\",\n });\n }\n });\n },\n [dir],\n );\n\n const onCellClick = React.useCallback(\n (rowIndex: number, columnId: string, event?: React.MouseEvent) => {\n if (event?.button === 2) return;\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 scrollToCell(rowIndex, columnId);\n return;\n }\n\n if (event.shiftKey && currentState.focusedCell) {\n event.preventDefault();\n selectRange(currentState.focusedCell, { rowIndex, columnId });\n scrollToCell(rowIndex, columnId);\n return;\n }\n }\n\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 const cellKey = getCellKey(rowIndex, columnId);\n const isClickingSelectedCell =\n currentState.selectionState.selectedCells.has(cellKey);\n\n if (!isClickingSelectedCell) {\n onSelectionClear();\n } else {\n focusCell(rowIndex, columnId);\n scrollToCell(rowIndex, columnId);\n return;\n }\n } else if (hasSelectedRows && columnId !== \"select\") {\n onSelectionClear();\n }\n\n if (\n currentFocused?.rowIndex === rowIndex &&\n currentFocused?.columnId === columnId\n ) {\n onCellEditingStart(rowIndex, columnId);\n } else {\n focusCell(rowIndex, columnId);\n scrollToCell(rowIndex, columnId);\n }\n },\n [\n store,\n focusCell,\n scrollToCell,\n onCellEditingStart,\n selectRange,\n onSelectionClear,\n ],\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 if (event.button === 2) {\n return;\n }\n\n event.preventDefault();\n\n if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {\n const cellKey = getCellKey(rowIndex, columnId);\n store.batch(() => {\n store.setState(\"selectionState\", {\n selectedCells: propsRef.current.enableSingleCellSelection\n ? new Set([cellKey])\n : 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, propsRef],\n );\n\n const onCellMouseEnter = React.useCallback(\n (rowIndex: number, columnId: string) => {\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 (!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 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 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 propsRef.current.onSortingChange?.(newSorting);\n },\n [store, propsRef],\n );\n\n const onColumnFiltersChange = React.useCallback(\n (updater: Updater) => {\n const currentState = store.getState();\n const newColumnFilters =\n typeof updater === \"function\"\n ? updater(currentState.columnFilters)\n : updater;\n store.setState(\"columnFilters\", newColumnFilters);\n\n propsRef.current.onColumnFiltersChange?.(newColumnFilters);\n },\n [store, propsRef],\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 propsRef.current.onRowSelectionChange?.(updater);\n },\n [store, columnIds, propsRef],\n );\n\n const onRowSelect = React.useCallback(\n (rowId: string, selected: boolean, shiftKey: boolean) => {\n const currentState = store.getState();\n const rows = tableRef.current?.getRowModel().rows ?? [];\n const currentRowIndex = rows.findIndex((r) => r.id === rowId);\n const currentRow = currentRowIndex >= 0 ? rows[currentRowIndex] : null;\n if (!currentRow) return;\n\n if (shiftKey && currentState.lastClickedRowId !== null) {\n const lastClickedRowIndex = rows.findIndex(\n (r) => r.id === currentState.lastClickedRowId,\n );\n if (lastClickedRowIndex >= 0) {\n const startIndex = Math.min(lastClickedRowIndex, currentRowIndex);\n const endIndex = Math.max(lastClickedRowIndex, currentRowIndex);\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 setRowSelected(newRowSelection, row.id, selected);\n }\n }\n\n onRowSelectionChange(newRowSelection);\n } else {\n const newRowSelection: RowSelectionState = {\n ...currentState.rowSelection,\n };\n setRowSelected(newRowSelection, currentRow.id, selected);\n onRowSelectionChange(newRowSelection);\n }\n } else {\n const newRowSelection: RowSelectionState = {\n ...currentState.rowSelection,\n };\n setRowSelected(newRowSelection, currentRow.id, selected);\n onRowSelectionChange(newRowSelection);\n }\n\n store.setState(\"lastClickedRowId\", rowId);\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 propsRef.current.onRowHeightChange?.(newRowHeight);\n },\n [store, propsRef],\n );\n\n const onColumnClick = React.useCallback(\n (columnId: string) => {\n if (!propsRef.current.enableColumnSelection) {\n onSelectionClear();\n return;\n }\n\n selectColumn(columnId);\n },\n [propsRef, selectColumn, onSelectionClear],\n );\n\n const onPasteDialogOpenChange = React.useCallback(\n (open: boolean) => {\n if (!open) {\n store.setState(\"pasteDialog\", {\n open: false,\n rowsNeeded: 0,\n clipboardText: \"\",\n });\n }\n },\n [store],\n );\n\n const defaultColumn: Partial> =\n React.useMemo(\n () => ({\n // Cell is rendered directly in DataGridRow to bypass flexRender's\n // unstable cell.getContext() (https://github.com/TanStack/table/issues/4794)\n minSize: MIN_COLUMN_SIZE,\n maxSize: MAX_COLUMN_SIZE,\n }),\n [],\n );\n\n const tableMeta = React.useMemo(() => {\n return {\n ...propsRef.current.meta,\n dataGridRef,\n cellMapRef,\n get focusedCell() {\n return store.getState().focusedCell;\n },\n get editingCell() {\n return store.getState().editingCell;\n },\n get selectionState() {\n return store.getState().selectionState;\n },\n get searchOpen() {\n return store.getState().searchOpen;\n },\n get contextMenu() {\n return store.getState().contextMenu;\n },\n get pasteDialog() {\n return store.getState().pasteDialog;\n },\n get rowHeight() {\n return store.getState().rowHeight;\n },\n get readOnly() {\n return propsRef.current.readOnly;\n },\n getIsCellSelected,\n getIsSearchMatch,\n getIsActiveSearchMatch,\n getVisualRowIndex,\n scrollToCell: (rowIndex, columnId, align = \"auto\") => {\n const container = dataGridRef.current;\n if (!container) return;\n\n rowVirtualizerRef.current?.scrollToIndex(rowIndex, { align });\n\n const scrollRowIntoView = (retries = 1) => {\n requestAnimationFrame(() => {\n const targetRow = rowMapRef.current.get(rowIndex);\n if (!targetRow) {\n if (retries > 0) scrollRowIntoView(retries - 1);\n return;\n }\n\n const headerBottom =\n headerRef.current?.getBoundingClientRect().bottom ??\n container.getBoundingClientRect().top;\n\n const viewportTop = headerBottom + VIEWPORT_OFFSET;\n\n const rowRect = targetRow.getBoundingClientRect();\n\n if (rowRect.top < viewportTop) {\n container.scrollTop -= viewportTop - rowRect.top;\n }\n\n const cellKey = getCellKey(rowIndex, columnId);\n const targetCell = cellMapRef.current.get(cellKey);\n if (targetCell) {\n scrollCellIntoView({\n container,\n targetCell,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n isRtl: dir === \"rtl\",\n });\n }\n });\n };\n\n scrollRowIntoView();\n },\n onRowHeightChange,\n onRowSelect,\n onDataUpdate,\n onRowsDelete: propsRef.current.onRowsDelete ? onRowsDelete : undefined,\n onColumnClick,\n onCellClick,\n onCellDoubleClick,\n onCellMouseDown,\n onCellMouseEnter,\n onCellMouseUp,\n onCellContextMenu,\n onCellEditingStart,\n onCellEditingStop,\n onCellsCopy,\n onCellsCut,\n onCellsPaste,\n onSelectionClear,\n onFilesUpload: propsRef.current.onFilesUpload\n ? propsRef.current.onFilesUpload\n : undefined,\n onFilesDelete: propsRef.current.onFilesDelete\n ? propsRef.current.onFilesDelete\n : undefined,\n onContextMenuOpenChange,\n onPasteDialogOpenChange,\n };\n }, [\n propsRef,\n store,\n dir,\n getIsCellSelected,\n getIsSearchMatch,\n getIsActiveSearchMatch,\n getVisualRowIndex,\n onRowHeightChange,\n onRowSelect,\n onDataUpdate,\n onRowsDelete,\n onColumnClick,\n onCellClick,\n onCellDoubleClick,\n onCellMouseDown,\n onCellMouseEnter,\n onCellMouseUp,\n onCellContextMenu,\n onCellEditingStart,\n onCellEditingStop,\n onCellsCopy,\n onCellsCut,\n onCellsPaste,\n onSelectionClear,\n onContextMenuOpenChange,\n onPasteDialogOpenChange,\n ]);\n\n // Memoize state object to reduce shallow equality checks\n const tableState = React.useMemo>>(\n () => ({\n ...propsRef.current.state,\n sorting,\n columnFilters,\n rowSelection,\n }),\n [propsRef, sorting, columnFilters, rowSelection],\n );\n\n const tableOptions = React.useMemo<\n TableOptions\n >(() => {\n return {\n ...propsRef.current,\n features: dataGridFeatures,\n data,\n columns,\n defaultColumn,\n initialState: propsRef.current.initialState,\n state: tableState,\n onRowSelectionChange,\n onSortingChange,\n onColumnFiltersChange,\n columnResizeMode: \"onChange\",\n columnResizeDirection: dir,\n meta: tableMeta,\n };\n }, [\n propsRef,\n data,\n columns,\n defaultColumn,\n tableState,\n dir,\n onRowSelectionChange,\n onSortingChange,\n onColumnFiltersChange,\n tableMeta,\n ]);\n\n const table = useTable(tableOptions);\n\n if (!tableRef.current) {\n tableRef.current = table;\n }\n\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.state.columnResizing, table.state.columnSizing]);\n\n const isFirefox = React.useSyncExternalStore(\n React.useCallback(() => () => {}, []),\n React.useCallback(() => {\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") {\n return false;\n }\n return navigator.userAgent.indexOf(\"Firefox\") !== -1;\n }, []),\n React.useCallback(() => false, []),\n );\n\n const adjustLayout = React.useMemo(() => {\n const columnPinning = table.state.columnPinning;\n return (\n isFirefox &&\n ((columnPinning.start?.length ?? 0) > 0 ||\n (columnPinning.end?.length ?? 0) > 0)\n );\n }, [isFirefox, table.state.columnPinning]);\n\n const rowVirtualizer = useVirtualizer({\n count: table.getRowModel().rows.length,\n getScrollElement: () => dataGridRef.current,\n estimateSize: () => rowHeightValue,\n overscan,\n measureElement: !isFirefox\n ? (element) => element?.getBoundingClientRect().height\n : undefined,\n scrollPaddingStart:\n (headerRef.current?.getBoundingClientRect().bottom ?? 0) -\n (dataGridRef.current?.getBoundingClientRect().top ?? 0) +\n VIEWPORT_OFFSET,\n // Add extra row buffer to absorb virtual position drift after render measurements\n scrollPaddingEnd:\n (dataGridRef.current?.getBoundingClientRect().bottom ?? 0) -\n (footerRef.current?.getBoundingClientRect().top ??\n dataGridRef.current?.getBoundingClientRect().bottom ??\n 0) +\n rowHeightValue +\n VIEWPORT_OFFSET,\n });\n\n if (!rowVirtualizerRef.current) {\n rowVirtualizerRef.current = rowVirtualizer;\n }\n\n const onScrollToRow = React.useCallback(\n async (opts: Partial) => {\n const rowIndex = opts?.rowIndex ?? 0;\n const columnId = opts?.columnId;\n\n focusGuardRef.current = true;\n\n const navigableIds = propsRef.current.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 .filter((c) => !NON_NAVIGABLE_COLUMN_IDS.has(c));\n\n const targetColumnId = columnId ?? navigableIds[0];\n\n if (!targetColumnId) {\n releaseFocusGuard(true);\n return;\n }\n\n async function onScrollAndFocus(retryCount: number) {\n if (!targetColumnId) return;\n const currentRowCount = propsRef.current.data.length;\n\n // If the requested row doesn't exist yet, wait for data to update\n if (rowIndex >= currentRowCount && retryCount > 0) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n await onScrollAndFocus(retryCount - 1);\n return;\n }\n\n const safeRowIndex = Math.min(\n rowIndex,\n Math.max(0, currentRowCount - 1),\n );\n\n const isBottomHalf = safeRowIndex > currentRowCount / 2;\n rowVirtualizer.scrollToIndex(safeRowIndex, {\n align: isBottomHalf ? \"end\" : \"start\",\n });\n\n await new Promise((resolve) => requestAnimationFrame(resolve));\n\n // Adjust scroll position to account for sticky header/footer\n const container = dataGridRef.current;\n const targetRow = rowMapRef.current.get(safeRowIndex);\n\n if (container && targetRow) {\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 const rowRect = targetRow.getBoundingClientRect();\n const isFullyVisible =\n rowRect.top >= viewportTop && rowRect.bottom <= viewportBottom;\n\n if (!isFullyVisible) {\n if (rowRect.top < viewportTop) {\n // Scroll up as row is partially hidden by header\n container.scrollTop -= viewportTop - rowRect.top;\n } else if (rowRect.bottom > viewportBottom) {\n // Scroll down as row is partially hidden by footer\n container.scrollTop += rowRect.bottom - viewportBottom;\n }\n }\n }\n\n store.batch(() => {\n store.setState(\"focusedCell\", {\n rowIndex: safeRowIndex,\n columnId: targetColumnId,\n });\n store.setState(\"editingCell\", null);\n });\n\n const cellKey = getCellKey(safeRowIndex, targetColumnId);\n const cellElement = cellMapRef.current.get(cellKey);\n\n if (cellElement) {\n cellElement.focus();\n releaseFocusGuard();\n } else if (retryCount > 0) {\n await new Promise((resolve) => requestAnimationFrame(resolve));\n await onScrollAndFocus(retryCount - 1);\n } else {\n dataGridRef.current?.focus();\n releaseFocusGuard();\n }\n }\n\n await onScrollAndFocus(SCROLL_SYNC_RETRY_COUNT);\n },\n [rowVirtualizer, propsRef, store, releaseFocusGuard],\n );\n\n const onRowAdd = React.useCallback(\n async (event?: React.MouseEvent) => {\n if (propsRef.current.readOnly || !propsRef.current.onRowAdd) return;\n\n const initialRowCount = propsRef.current.data.length;\n\n let result: Partial | null;\n try {\n result = await propsRef.current.onRowAdd(event);\n } catch {\n // Callback threw an error, don't proceed with scroll/focus\n return;\n }\n\n if (result === null || event?.defaultPrevented) return;\n\n onSelectionClear();\n\n // onScrollToRow will handle retries if the row isn't rendered yet\n const targetRowIndex = result.rowIndex ?? initialRowCount;\n const targetColumnId = result.columnId;\n\n void onScrollToRow({\n rowIndex: targetRowIndex,\n columnId: targetColumnId,\n });\n },\n [propsRef, onScrollToRow, onSelectionClear],\n );\n\n const onDataGridKeyDown = React.useCallback(\n (event: KeyboardEvent) => {\n const currentState = store.getState();\n const { key, ctrlKey, metaKey, shiftKey, altKey } = event;\n const isCtrlPressed = ctrlKey || metaKey;\n\n if (\n propsRef.current.enableSearch &&\n isCtrlPressed &&\n !shiftKey &&\n key === SEARCH_SHORTCUT_KEY\n ) {\n event.preventDefault();\n onSearchOpenChange(true);\n return;\n }\n\n if (\n propsRef.current.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 return;\n }\n\n // Cell editing keyboard events (Enter, Tab, Escape) are handled by the cell variants\n // to ensure proper value commitment before navigation\n if (currentState.editingCell) return;\n\n if (\n isCtrlPressed &&\n (key === \"Backspace\" || key === \"Delete\") &&\n !propsRef.current.readOnly &&\n propsRef.current.onRowsDelete\n ) {\n const rowIndices = new Set();\n\n const selectedRowIds = Object.keys(currentState.rowSelection);\n if (selectedRowIds.length > 0) {\n const currentTable = tableRef.current;\n const rows = currentTable?.getRowModel().rows ?? [];\n for (const row of rows) {\n if (currentState.rowSelection[row.id]) {\n rowIndices.add(row.index);\n }\n }\n } else if (currentState.selectionState.selectedCells.size > 0) {\n for (const cellKey of currentState.selectionState.selectedCells) {\n const { rowIndex } = parseCellKey(cellKey);\n rowIndices.add(rowIndex);\n }\n } else if (currentState.focusedCell) {\n rowIndices.add(currentState.focusedCell.rowIndex);\n }\n\n if (rowIndices.size > 0) {\n event.preventDefault();\n void onRowsDelete(Array.from(rowIndices));\n }\n return;\n }\n\n if (!currentState.focusedCell) return;\n\n let direction: NavigationDirection | null = null;\n\n if (isCtrlPressed && !shiftKey && key === \"a\") {\n event.preventDefault();\n selectAll();\n return;\n }\n\n if (isCtrlPressed && !shiftKey && key === \"c\") {\n event.preventDefault();\n void onCellsCopy();\n return;\n }\n\n if (\n isCtrlPressed &&\n !shiftKey &&\n key === \"x\" &&\n !propsRef.current.readOnly\n ) {\n event.preventDefault();\n void onCellsCut();\n return;\n }\n\n if (\n propsRef.current.enablePaste &&\n isCtrlPressed &&\n !shiftKey &&\n key === \"v\" &&\n !propsRef.current.readOnly\n ) {\n event.preventDefault();\n void onCellsPaste();\n return;\n }\n\n if (\n (key === \"Delete\" || key === \"Backspace\") &&\n !isCtrlPressed &&\n !propsRef.current.readOnly\n ) {\n const cellsToClear =\n currentState.selectionState.selectedCells.size > 0\n ? Array.from(currentState.selectionState.selectedCells)\n : currentState.focusedCell\n ? [\n getCellKey(\n currentState.focusedCell.rowIndex,\n currentState.focusedCell.columnId,\n ),\n ]\n : [];\n\n if (cellsToClear.length > 0) {\n event.preventDefault();\n\n const updates: Array<{\n rowIndex: number;\n columnId: string;\n value: unknown;\n }> = [];\n\n const currentTable = tableRef.current;\n const tableColumns = currentTable?.getAllColumns() ?? [];\n const columnById = new Map(tableColumns.map((c) => [c.id, c]));\n\n for (const cellKey of cellsToClear) {\n const { rowIndex, columnId } = parseCellKey(cellKey);\n const column = columnById.get(columnId);\n const cellVariant = column?.columnDef?.meta?.cell?.variant;\n const emptyValue = getEmptyCellValue(cellVariant);\n updates.push({ rowIndex, columnId, value: emptyValue });\n }\n\n onDataUpdate(updates);\n\n if (currentState.selectionState.selectedCells.size > 0) {\n onSelectionClear();\n }\n\n if (currentState.cutCells.size > 0) {\n store.setState(\"cutCells\", new Set());\n }\n }\n return;\n }\n\n if (\n key === \"Enter\" &&\n shiftKey &&\n !propsRef.current.readOnly &&\n propsRef.current.onRowAdd\n ) {\n event.preventDefault();\n const initialRowCount = propsRef.current.data.length;\n const currentColumnId = currentState.focusedCell.columnId;\n\n Promise.resolve(propsRef.current.onRowAdd())\n .then(async (result) => {\n if (result === null) return;\n\n onSelectionClear();\n\n const targetRowIndex = result.rowIndex ?? initialRowCount;\n const targetColumnId = result.columnId ?? currentColumnId;\n\n void onScrollToRow({\n rowIndex: targetRowIndex,\n columnId: targetColumnId,\n });\n })\n .catch(() => {\n // Callback threw an error, don't proceed with scroll/focus\n });\n return;\n }\n\n switch (key) {\n case \"ArrowUp\":\n if (altKey && !isCtrlPressed && !shiftKey) {\n direction = \"pageup\";\n } else if (isCtrlPressed && shiftKey) {\n const selectionEdge =\n currentState.selectionState.selectionRange?.end ||\n currentState.focusedCell;\n const currentColIndex = navigableColumnIds.indexOf(\n selectionEdge.columnId,\n );\n const selectionStart =\n currentState.selectionState.selectionRange?.start ||\n currentState.focusedCell;\n\n selectRange(selectionStart, {\n rowIndex: 0,\n columnId:\n navigableColumnIds[currentColIndex] ?? selectionEdge.columnId,\n });\n\n const rowVirtualizer = rowVirtualizerRef.current;\n if (rowVirtualizer) {\n rowVirtualizer.scrollToIndex(0, { align: \"start\" });\n }\n\n restoreFocus(dataGridRef.current);\n\n event.preventDefault();\n return;\n } else if (isCtrlPressed && !shiftKey) {\n direction = \"ctrl+up\";\n } else {\n direction = \"up\";\n }\n break;\n case \"ArrowDown\":\n if (altKey && !isCtrlPressed && !shiftKey) {\n direction = \"pagedown\";\n } else if (isCtrlPressed && shiftKey) {\n const rowCount =\n tableRef.current?.getRowModel().rows.length ||\n propsRef.current.data.length;\n const selectionEdge =\n currentState.selectionState.selectionRange?.end ||\n currentState.focusedCell;\n const currentColIndex = navigableColumnIds.indexOf(\n selectionEdge.columnId,\n );\n const selectionStart =\n currentState.selectionState.selectionRange?.start ||\n currentState.focusedCell;\n\n selectRange(selectionStart, {\n rowIndex: Math.max(0, rowCount - 1),\n columnId:\n navigableColumnIds[currentColIndex] ?? selectionEdge.columnId,\n });\n\n const rowVirtualizer = rowVirtualizerRef.current;\n if (rowVirtualizer) {\n rowVirtualizer.scrollToIndex(Math.max(0, rowCount - 1), {\n align: \"end\",\n });\n }\n\n restoreFocus(dataGridRef.current);\n\n event.preventDefault();\n return;\n } else if (isCtrlPressed && !shiftKey) {\n direction = \"ctrl+down\";\n } else {\n direction = \"down\";\n }\n break;\n case \"ArrowLeft\":\n if (isCtrlPressed && shiftKey) {\n const selectionEdge =\n currentState.selectionState.selectionRange?.end ||\n currentState.focusedCell;\n const selectionStart =\n currentState.selectionState.selectionRange?.start ||\n currentState.focusedCell;\n const targetColumnId =\n dir === \"rtl\"\n ? navigableColumnIds[navigableColumnIds.length - 1]\n : navigableColumnIds[0];\n\n if (targetColumnId) {\n selectRange(selectionStart, {\n rowIndex: selectionEdge.rowIndex,\n columnId: targetColumnId,\n });\n\n const container = dataGridRef.current;\n const cellKey = getCellKey(\n selectionEdge.rowIndex,\n targetColumnId,\n );\n const targetCell = cellMapRef.current.get(cellKey);\n if (container && targetCell) {\n scrollCellIntoView({\n container,\n targetCell,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n direction: \"home\",\n isRtl: dir === \"rtl\",\n });\n }\n\n restoreFocus(container);\n }\n event.preventDefault();\n return;\n } else if (isCtrlPressed && !shiftKey) {\n direction = \"home\";\n } else {\n direction = \"left\";\n }\n break;\n case \"ArrowRight\":\n if (isCtrlPressed && shiftKey) {\n const selectionEdge =\n currentState.selectionState.selectionRange?.end ||\n currentState.focusedCell;\n const selectionStart =\n currentState.selectionState.selectionRange?.start ||\n currentState.focusedCell;\n const targetColumnId =\n dir === \"rtl\"\n ? navigableColumnIds[0]\n : navigableColumnIds[navigableColumnIds.length - 1];\n\n if (targetColumnId) {\n selectRange(selectionStart, {\n rowIndex: selectionEdge.rowIndex,\n columnId: targetColumnId,\n });\n\n const container = dataGridRef.current;\n const cellKey = getCellKey(\n selectionEdge.rowIndex,\n targetColumnId,\n );\n const targetCell = cellMapRef.current.get(cellKey);\n if (container && targetCell) {\n scrollCellIntoView({\n container,\n targetCell,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n direction: \"end\",\n isRtl: dir === \"rtl\",\n });\n }\n\n restoreFocus(container);\n }\n event.preventDefault();\n return;\n } else if (isCtrlPressed && !shiftKey) {\n direction = \"end\";\n } else {\n direction = \"right\";\n }\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 = altKey ? \"pageleft\" : \"pageup\";\n break;\n case \"PageDown\":\n direction = altKey ? \"pageright\" : \"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 onSelectionClear();\n } else {\n blurCell();\n }\n return;\n case \"Tab\":\n event.preventDefault();\n if (dir === \"rtl\") {\n direction = event.shiftKey ? \"right\" : \"left\";\n } else {\n direction = event.shiftKey ? \"left\" : \"right\";\n }\n break;\n }\n\n if (direction) {\n event.preventDefault();\n\n if (shiftKey && key !== \"Tab\" && currentState.focusedCell) {\n const selectionEdge =\n currentState.selectionState.selectionRange?.end ||\n currentState.focusedCell;\n\n const currentColIndex = navigableColumnIds.indexOf(\n selectionEdge.columnId,\n );\n let newRowIndex = selectionEdge.rowIndex;\n let newColumnId = selectionEdge.columnId;\n\n const isRtl = dir === \"rtl\";\n\n const rowCount =\n tableRef.current?.getRowModel().rows.length ||\n propsRef.current.data.length;\n\n switch (direction) {\n case \"up\":\n newRowIndex = Math.max(0, selectionEdge.rowIndex - 1);\n break;\n case \"down\":\n newRowIndex = Math.min(rowCount - 1, selectionEdge.rowIndex + 1);\n break;\n case \"left\":\n if (isRtl) {\n if (currentColIndex < navigableColumnIds.length - 1) {\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\n if (nextColumnId) newColumnId = nextColumnId;\n }\n } else {\n if (currentColIndex > 0) {\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\n if (prevColumnId) newColumnId = prevColumnId;\n }\n }\n break;\n case \"right\":\n if (isRtl) {\n if (currentColIndex > 0) {\n const prevColumnId = navigableColumnIds[currentColIndex - 1];\n if (prevColumnId) newColumnId = prevColumnId;\n }\n } else {\n if (currentColIndex < navigableColumnIds.length - 1) {\n const nextColumnId = navigableColumnIds[currentColIndex + 1];\n if (nextColumnId) newColumnId = nextColumnId;\n }\n }\n break;\n case \"home\":\n if (navigableColumnIds.length > 0) {\n newColumnId = navigableColumnIds[0] ?? newColumnId;\n }\n break;\n case \"end\":\n if (navigableColumnIds.length > 0) {\n newColumnId =\n navigableColumnIds[navigableColumnIds.length - 1] ??\n newColumnId;\n }\n break;\n }\n\n const selectionStart =\n currentState.selectionState.selectionRange?.start ||\n currentState.focusedCell;\n\n selectRange(selectionStart, {\n rowIndex: newRowIndex,\n columnId: newColumnId,\n });\n\n const container = dataGridRef.current;\n const targetRow = rowMapRef.current.get(newRowIndex);\n const cellKey = getCellKey(newRowIndex, newColumnId);\n const targetCell = cellMapRef.current.get(cellKey);\n\n if (\n newRowIndex !== selectionEdge.rowIndex &&\n (direction === \"up\" || direction === \"down\")\n ) {\n if (container && targetRow) {\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 const rowRect = targetRow.getBoundingClientRect();\n const isFullyVisible =\n rowRect.top >= viewportTop && rowRect.bottom <= viewportBottom;\n\n if (!isFullyVisible) {\n const scrollNeeded =\n direction === \"down\"\n ? rowRect.bottom - viewportBottom\n : viewportTop - rowRect.top;\n\n if (direction === \"down\") {\n container.scrollTop += scrollNeeded;\n } else {\n container.scrollTop -= scrollNeeded;\n }\n\n restoreFocus(container);\n }\n } else {\n const rowVirtualizer = rowVirtualizerRef.current;\n if (rowVirtualizer) {\n const align = direction === \"up\" ? \"start\" : \"end\";\n rowVirtualizer.scrollToIndex(newRowIndex, { align });\n\n restoreFocus(container);\n }\n }\n }\n\n if (\n newColumnId !== selectionEdge.columnId &&\n (direction === \"left\" ||\n direction === \"right\" ||\n direction === \"home\" ||\n direction === \"end\")\n ) {\n if (container && targetCell) {\n scrollCellIntoView({\n container,\n targetCell,\n tableRef,\n viewportOffset: VIEWPORT_OFFSET,\n direction,\n isRtl,\n });\n }\n }\n } else {\n if (currentState.selectionState.selectedCells.size > 0) {\n onSelectionClear();\n }\n navigateCell(direction);\n }\n }\n },\n [\n dir,\n store,\n propsRef,\n blurCell,\n navigateCell,\n selectAll,\n onCellsCopy,\n onCellsCut,\n onCellsPaste,\n onDataUpdate,\n onSelectionClear,\n navigableColumnIds,\n selectRange,\n onSearchOpenChange,\n onNavigateToNextMatch,\n onNavigateToPrevMatch,\n onRowsDelete,\n restoreFocus,\n onScrollToRow,\n ],\n );\n\n const searchState = React.useMemo(() => {\n if (!propsRef.current.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 propsRef,\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, shiftKey } = event;\n const isCommandPressed = ctrlKey || metaKey;\n\n if (\n propsRef.current.enableSearch &&\n isCommandPressed &&\n !shiftKey &&\n key === SEARCH_SHORTCUT_KEY\n ) {\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\n const nextSearchOpen = !store.getState().searchOpen;\n onSearchOpenChange(nextSearchOpen);\n\n if (nextSearchOpen && !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 onSelectionClear();\n }\n }\n }\n\n window.addEventListener(\"keydown\", onGlobalKeyDown, true);\n return () => {\n window.removeEventListener(\"keydown\", onGlobalKeyDown, true);\n };\n }, [propsRef, onSearchOpenChange, store, onSelectionClear]);\n\n React.useEffect(() => {\n const currentState = store.getState();\n const autoFocus = propsRef.current.autoFocus;\n\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 }, [store, propsRef, data, columns, navigableColumnIds, focusCell]);\n\n // Restore focus to container when virtualized cells are unmounted\n React.useEffect(() => {\n const container = dataGridRef.current;\n if (!container) return;\n\n function onFocusOut(event: FocusEvent) {\n if (focusGuardRef.current) return;\n\n const currentContainer = dataGridRef.current;\n if (!currentContainer) return;\n\n const currentState = store.getState();\n\n if (!currentState.focusedCell || currentState.editingCell) return;\n\n const relatedTarget = event.relatedTarget;\n\n const isFocusMovingOutsideGrid =\n !relatedTarget || !currentContainer.contains(relatedTarget as Node);\n\n const isFocusMovingToPopover = getIsInPopover(relatedTarget);\n\n if (isFocusMovingOutsideGrid && !isFocusMovingToPopover) {\n const { rowIndex, columnId } = currentState.focusedCell;\n const cellKey = getCellKey(rowIndex, columnId);\n const cellElement = cellMapRef.current.get(cellKey);\n\n requestAnimationFrame(() => {\n if (focusGuardRef.current) return;\n\n if (cellElement && document.body.contains(cellElement)) {\n cellElement.focus();\n } else {\n currentContainer.focus();\n }\n });\n }\n }\n\n container.addEventListener(\"focusout\", onFocusOut);\n\n return () => {\n container.removeEventListener(\"focusout\", onFocusOut);\n };\n }, [store]);\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 elements = document.elementsFromPoint(\n event.clientX,\n event.clientY,\n );\n\n // Compensate for event.target bubbling up\n const isInsidePopover = elements.some((element) =>\n getIsInPopover(element),\n );\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 onSelectionClear();\n }\n }\n }\n }\n\n document.addEventListener(\"mousedown\", onOutsideClick);\n return () => {\n document.removeEventListener(\"mousedown\", onOutsideClick);\n };\n }, [store, blurCell, onSelectionClear]);\n\n React.useEffect(() => {\n function onSelectStart(event: Event) {\n event.preventDefault();\n }\n\n function onContextMenu(event: Event) {\n event.preventDefault();\n }\n\n function onCleanup() {\n document.removeEventListener(\"selectstart\", onSelectStart);\n document.removeEventListener(\"contextmenu\", onContextMenu);\n document.body.style.userSelect = \"\";\n }\n\n const onUnsubscribe = store.subscribe(() => {\n const currentState = store.getState();\n if (currentState.selectionState.isSelecting) {\n document.addEventListener(\"selectstart\", onSelectStart);\n document.addEventListener(\"contextmenu\", onContextMenu);\n document.body.style.userSelect = \"none\";\n } else {\n onCleanup();\n }\n });\n\n return () => {\n onCleanup();\n onUnsubscribe();\n };\n }, [store]);\n\n React.useEffect(() => {\n let rafId: number | null = null;\n let mouseX = 0;\n let mouseY = 0;\n let mouseReady = false;\n let active = false;\n let lastSelectionTime = 0;\n let resizeObserver: ResizeObserver | null = null;\n\n let cachedRect: DOMRect | null = null;\n let cachedHdrH = 0;\n let cachedFtrH = 0;\n let cachedLpw = 0;\n let cachedRpw = 0;\n\n function getAutoScrollSpeed(dist: number): number {\n const t = Math.min(dist / AUTO_SCROLL_SPEED_RAMP_ZONE, 1);\n return Math.round(\n AUTO_SCROLL_MIN_SPEED +\n (AUTO_SCROLL_MAX_SPEED - AUTO_SCROLL_MIN_SPEED) * t,\n );\n }\n\n function cacheLayout(container: HTMLDivElement) {\n cachedRect = container.getBoundingClientRect();\n cachedHdrH = headerRef.current?.getBoundingClientRect().height ?? 0;\n cachedFtrH = footerRef.current?.getBoundingClientRect().height ?? 0;\n const tbl = tableRef.current;\n if (tbl) {\n cachedLpw = tbl\n .getStartVisibleLeafColumns()\n .reduce((s, c) => s + c.getSize(), 0);\n cachedRpw = tbl\n .getEndVisibleLeafColumns()\n .reduce((s, c) => s + c.getSize(), 0);\n }\n }\n\n function tick() {\n if (!active) return;\n const container = dataGridRef.current;\n const tbl = tableRef.current;\n\n if (!container || !tbl) {\n onAutoScrollStop();\n return;\n }\n\n if (!mouseReady || !cachedRect) {\n rafId = requestAnimationFrame(tick);\n return;\n }\n\n const rect = cachedRect;\n const { dir } = dragDepsRef.current;\n const hasNegativeScroll = container.scrollLeft < 0;\n const isActuallyRtl = dir === \"rtl\" || hasNegativeScroll;\n\n const dataTop = rect.top + cachedHdrH;\n const dataBottom = rect.bottom - cachedFtrH;\n\n const scrollAreaLeft = isActuallyRtl\n ? rect.left + cachedRpw\n : rect.left + cachedLpw;\n const scrollAreaRight = isActuallyRtl\n ? rect.right - cachedLpw\n : rect.right - cachedRpw;\n\n let dy = 0;\n let dx = 0;\n\n if (mouseY < dataTop) dy = -getAutoScrollSpeed(dataTop - mouseY);\n else if (mouseY > dataBottom)\n dy = getAutoScrollSpeed(mouseY - dataBottom);\n\n if (mouseX < scrollAreaLeft)\n dx = -getAutoScrollSpeed(scrollAreaLeft - mouseX);\n else if (mouseX > scrollAreaRight)\n dx = getAutoScrollSpeed(mouseX - scrollAreaRight);\n\n if (dx === 0 && dy === 0) {\n rafId = requestAnimationFrame(tick);\n return;\n }\n\n container.scrollTop += dy;\n container.scrollLeft += dx;\n\n const now = performance.now();\n if (now - lastSelectionTime < AUTO_SCROLL_SELECTION_THROTTLE_MS) {\n rafId = requestAnimationFrame(tick);\n return;\n }\n\n const { rowHeightValue: rh, columnIds } = dragDepsRef.current;\n if (columnIds.length === 0) {\n rafId = requestAnimationFrame(tick);\n return;\n }\n\n const totalRows = tbl.getRowModel().rows.length;\n const clampedY = Math.max(dataTop, Math.min(mouseY, dataBottom));\n const absY = container.scrollTop + (clampedY - dataTop);\n const rowIndex = Math.max(\n 0,\n Math.min(Math.floor(absY / rh), totalRows - 1),\n );\n\n const st = store.getState();\n const range = st.selectionState.selectionRange;\n\n let columnId: string | undefined;\n\n if (dx !== 0) {\n const clampedX = Math.max(rect.left, Math.min(mouseX, rect.right));\n const relX = clampedX - rect.left;\n\n const leftZoneWidth = isActuallyRtl ? cachedRpw : cachedLpw;\n const rightZoneWidth = isActuallyRtl ? cachedLpw : cachedRpw;\n\n if (relX < leftZoneWidth) {\n const columns = isActuallyRtl\n ? tbl.getEndVisibleLeafColumns()\n : tbl.getStartVisibleLeafColumns();\n columnId = columns[0]?.id ?? columnIds[0] ?? \"\";\n let cx = 0;\n for (const col of columns) {\n if (relX < cx + col.getSize()) {\n columnId = col.id;\n break;\n }\n cx += col.getSize();\n }\n } else if (relX > rect.width - rightZoneWidth) {\n const columns = isActuallyRtl\n ? tbl.getStartVisibleLeafColumns()\n : tbl.getEndVisibleLeafColumns();\n columnId = columns[0]?.id ?? columnIds[columnIds.length - 1] ?? \"\";\n let cx = rect.width - rightZoneWidth;\n for (const col of columns) {\n if (relX < cx + col.getSize()) {\n columnId = col.id;\n break;\n }\n cx += col.getSize();\n }\n } else {\n const center = tbl.getCenterVisibleLeafColumns();\n const centerZoneWidth = rect.width - leftZoneWidth - rightZoneWidth;\n const distFromVisualLeft = relX - leftZoneWidth;\n\n let absX: number;\n if (isActuallyRtl) {\n const scrollFromRight = hasNegativeScroll\n ? -container.scrollLeft\n : container.scrollWidth -\n container.clientWidth -\n container.scrollLeft;\n absX = scrollFromRight + (centerZoneWidth - distFromVisualLeft);\n } else {\n absX = container.scrollLeft + distFromVisualLeft;\n }\n\n columnId =\n center[center.length - 1]?.id ??\n columnIds[columnIds.length - 1] ??\n \"\";\n let cw = 0;\n for (const col of center) {\n cw += col.getSize();\n if (absX < cw) {\n columnId = col.id;\n break;\n }\n }\n }\n }\n\n if (!columnId) {\n columnId = range?.end.columnId ?? columnIds[0] ?? \"\";\n }\n\n if (\n range &&\n (rowIndex !== range.end.rowIndex || columnId !== range.end.columnId)\n ) {\n dragDepsRef.current.selectRange(\n range.start,\n { rowIndex, columnId },\n true,\n );\n lastSelectionTime = now;\n }\n\n rafId = requestAnimationFrame(tick);\n }\n\n function onMove(event: MouseEvent) {\n mouseX = event.clientX;\n mouseY = event.clientY;\n mouseReady = true;\n }\n\n function onUp() {\n onAutoScrollStop();\n const st = store.getState();\n if (st.selectionState.isSelecting) {\n store.setState(\"selectionState\", {\n ...st.selectionState,\n isSelecting: false,\n });\n }\n }\n\n function onAutoScrollStart() {\n if (active) return;\n\n const container = dataGridRef.current;\n if (!container) return;\n\n active = true;\n mouseReady = false;\n cachedRect = null;\n lastSelectionTime = 0;\n document.addEventListener(\"mousemove\", onMove);\n document.addEventListener(\"mouseup\", onUp);\n resizeObserver = new ResizeObserver(() => {\n const currentContainer = dataGridRef.current;\n if (currentContainer) cacheLayout(currentContainer);\n });\n resizeObserver.observe(container);\n rafId = requestAnimationFrame(() => {\n const currentContainer = dataGridRef.current;\n if (currentContainer) cacheLayout(currentContainer);\n rafId = requestAnimationFrame(tick);\n });\n }\n\n function onAutoScrollStop() {\n if (!active) return;\n active = false;\n cachedRect = null;\n document.removeEventListener(\"mousemove\", onMove);\n document.removeEventListener(\"mouseup\", onUp);\n resizeObserver?.disconnect();\n resizeObserver = null;\n if (rafId !== null) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n }\n\n if (store.getState().selectionState.isSelecting) onAutoScrollStart();\n\n const onUnsubscribe = store.subscribe(() => {\n const st = store.getState();\n if (st.selectionState.isSelecting && !active) onAutoScrollStart();\n else if (!st.selectionState.isSelecting && active) onAutoScrollStop();\n });\n\n return () => {\n onAutoScrollStop();\n onUnsubscribe();\n };\n }, [store, dragDepsRef]);\n\n useIsomorphicLayoutEffect(() => {\n const rafId = requestAnimationFrame(() => {\n rowVirtualizer.measure();\n });\n return () => cancelAnimationFrame(rafId);\n }, [\n rowHeight,\n table.state.columnFilters,\n table.state.columnPinning,\n table.state.columnSizing,\n table.state.columnVisibility,\n table.state.rowSelection,\n table.state.sorting,\n ]);\n\n const virtualTotalSize = rowVirtualizer.getTotalSize();\n const virtualItems = rowVirtualizer.getVirtualItems();\n const measureElement = rowVirtualizer.measureElement;\n\n return React.useMemo(\n () => ({\n dataGridRef,\n headerRef,\n rowMapRef,\n footerRef,\n dir,\n table,\n tableMeta,\n virtualTotalSize,\n virtualItems,\n measureElement,\n columns,\n columnSizeVars,\n searchState,\n searchMatchesByRow,\n activeSearchMatch,\n cellSelectionMap,\n focusedCell,\n editingCell,\n rowHeight,\n contextMenu,\n pasteDialog,\n onRowAdd: propsRef.current.onRowAdd ? onRowAdd : undefined,\n adjustLayout,\n }),\n [\n propsRef,\n dir,\n table,\n tableMeta,\n virtualTotalSize,\n virtualItems,\n measureElement,\n columns,\n columnSizeVars,\n searchState,\n searchMatchesByRow,\n activeSearchMatch,\n cellSelectionMap,\n focusedCell,\n editingCell,\n rowHeight,\n contextMenu,\n pasteDialog,\n onRowAdd,\n adjustLayout,\n ],\n );\n}\n\nexport { type UseDataGridProps, useDataGrid };\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-debounced-callback.ts", - "content": "/**\n * @see https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts\n */\n\nimport * as React from \"react\";\n\nimport { useCallbackRef } from \"@/hooks/use-callback-ref\";\n\nexport function useDebouncedCallback unknown>(\n callback: T,\n delay: number,\n) {\n const handleCallback = useCallbackRef(callback);\n const debounceTimerRef = React.useRef(0);\n React.useEffect(\n () => () => window.clearTimeout(debounceTimerRef.current),\n [],\n );\n\n const setValue = React.useCallback(\n (...args: Parameters) => {\n window.clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = window.setTimeout(\n () => handleCallback(...args),\n delay,\n );\n },\n [handleCallback, delay],\n );\n\n return setValue;\n}\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-isomorphic-layout-effect.ts", - "content": "import * as React from \"react\";\n\nconst useIsomorphicLayoutEffect =\n typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\nexport { useIsomorphicLayoutEffect };\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-lazy-ref.ts", - "content": "import * as React from \"react\";\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\nexport { useLazyRef };\n", - "type": "registry:hook" - }, - { - "path": "src/lib/compose-refs.ts", - "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef = React.Ref | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef(ref: PossibleRef, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n }\n\n if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs(...refs: PossibleRef[]): React.RefCallback {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup === \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup === \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs(...refs: PossibleRef[]): React.RefCallback {\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-utils.ts", - "content": "import type { Column, RowData, Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport {\n BaselineIcon,\n CalendarIcon,\n CheckSquareIcon,\n File,\n FileArchive,\n FileAudio,\n FileIcon,\n FileImage,\n FileSpreadsheet,\n FileText,\n FileVideo,\n HashIcon,\n LinkIcon,\n ListChecksIcon,\n ListIcon,\n Presentation,\n TextInitialIcon,\n} from \"lucide-react\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\nimport type {\n CellOpts,\n CellPosition,\n Direction,\n FileCellData,\n RowHeightValue,\n} from \"@/lib/data-grid-types\";\n\nexport function stringifyUnknown(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n if (value instanceof Date) return value.toISOString();\n try {\n return JSON.stringify(value) ?? \"\";\n } catch {\n return \"\";\n }\n}\n\nexport function flexRender(\n Comp: ((props: TProps) => React.ReactNode) | string | undefined,\n props: TProps,\n): React.ReactNode {\n if (typeof Comp === \"string\") {\n return Comp;\n }\n return Comp?.(props);\n}\n\nexport function getIsFileCellData(item: unknown): item is FileCellData {\n return (\n !!item &&\n typeof item === \"object\" &&\n \"id\" in item &&\n \"name\" in item &&\n \"size\" in item &&\n \"type\" in item\n );\n}\n\nexport function matchSelectOption(\n value: string,\n options: { value: string; label: string }[],\n): string | undefined {\n return options.find(\n (o) =>\n o.value === value ||\n o.value.toLowerCase() === value.toLowerCase() ||\n o.label.toLowerCase() === value.toLowerCase(),\n )?.value;\n}\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\nexport function getColumnBorderVisibility(params: {\n column: Column;\n nextColumn?: Column;\n isLastColumn: boolean;\n}): {\n showEndBorder: boolean;\n showStartBorder: boolean;\n} {\n const { column, nextColumn, isLastColumn } = params;\n\n const isPinned = column.getIsPinned();\n const isFirstRightPinnedColumn =\n isPinned === \"end\" && column.getIsFirstColumn(\"end\");\n const isLastRightPinnedColumn =\n isPinned === \"end\" && column.getIsLastColumn(\"end\");\n\n const nextIsPinned = nextColumn?.getIsPinned();\n const isBeforeRightPinned =\n nextIsPinned === \"end\" && nextColumn?.getIsFirstColumn(\"end\");\n\n const showEndBorder =\n !isBeforeRightPinned && (isLastColumn || !isLastRightPinnedColumn);\n\n const showStartBorder = isFirstRightPinnedColumn;\n\n return {\n showEndBorder,\n showStartBorder,\n };\n}\n\nexport function getColumnPinningStyle(params: {\n column: Column;\n withBorder?: boolean;\n dir?: Direction;\n}): React.CSSProperties {\n const { column, dir = \"ltr\", withBorder = false } = params;\n\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn =\n isPinned === \"start\" && column.getIsLastColumn(\"start\");\n const isFirstRightPinnedColumn =\n isPinned === \"end\" && column.getIsFirstColumn(\"end\");\n\n const isRtl = dir === \"rtl\";\n\n const leftPosition =\n isPinned === \"start\" ? `${column.getStart(\"start\")}px` : undefined;\n const rightPosition =\n isPinned === \"end\" ? `${column.getAfter(\"end\")}px` : undefined;\n\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? isRtl\n ? \"4px 0 4px -4px var(--border) inset\"\n : \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? isRtl\n ? \"-4px 0 4px -4px var(--border) inset\"\n : \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isRtl ? rightPosition : leftPosition,\n right: isRtl ? leftPosition : rightPosition,\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 getScrollDirection(\n direction: string,\n): \"left\" | \"right\" | \"home\" | \"end\" | undefined {\n if (\n direction === \"left\" ||\n direction === \"right\" ||\n direction === \"home\" ||\n direction === \"end\"\n ) {\n return direction as \"left\" | \"right\" | \"home\" | \"end\";\n }\n if (direction === \"pageleft\") return \"left\";\n if (direction === \"pageright\") return \"right\";\n return undefined;\n}\n\nexport function scrollCellIntoView(params: {\n container: HTMLDivElement;\n targetCell: HTMLDivElement;\n tableRef: React.RefObject | null>;\n viewportOffset: number;\n direction?: \"left\" | \"right\" | \"home\" | \"end\";\n isRtl: boolean;\n}): void {\n const { container, targetCell, tableRef, direction, viewportOffset, isRtl } =\n params;\n\n const containerRect = container.getBoundingClientRect();\n const cellRect = targetCell.getBoundingClientRect();\n\n const hasNegativeScroll = container.scrollLeft < 0;\n const isActuallyRtl = isRtl || hasNegativeScroll;\n\n const currentTable = tableRef.current;\n const leftPinnedColumns = currentTable?.getStartVisibleLeafColumns() ?? [];\n const rightPinnedColumns = currentTable?.getEndVisibleLeafColumns() ?? [];\n\n const leftPinnedWidth = leftPinnedColumns.reduce(\n (sum, c) => sum + c.getSize(),\n 0,\n );\n const rightPinnedWidth = rightPinnedColumns.reduce(\n (sum, c) => sum + c.getSize(),\n 0,\n );\n\n const viewportLeft = isActuallyRtl\n ? containerRect.left + rightPinnedWidth + viewportOffset\n : containerRect.left + leftPinnedWidth + viewportOffset;\n const viewportRight = isActuallyRtl\n ? containerRect.right - leftPinnedWidth - viewportOffset\n : containerRect.right - rightPinnedWidth - viewportOffset;\n\n const isFullyVisible =\n cellRect.left >= viewportLeft && cellRect.right <= viewportRight;\n\n if (isFullyVisible) return;\n\n const isClippedLeft = cellRect.left < viewportLeft;\n const isClippedRight = cellRect.right > viewportRight;\n\n let scrollDelta = 0;\n\n if (!direction) {\n if (isClippedRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else if (isClippedLeft) {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n } else {\n const shouldScrollRight = isActuallyRtl\n ? direction === \"right\" || direction === \"home\"\n : direction === \"right\" || direction === \"end\";\n\n if (shouldScrollRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n }\n\n container.scrollLeft += scrollDelta;\n}\n\nfunction countTabs(s: string): number {\n let n = 0;\n for (let i = 0; i < s.length; i++) if (s[i] === \"\\t\") n++;\n return n;\n}\n\nexport function parseTsv(\n text: string,\n fallbackColumnCount: number,\n): string[][] {\n if (text.startsWith('\"') || text.includes('\\t\"')) {\n const rows: string[][] = [];\n let currentRow: string[] = [];\n let currentField = \"\";\n let inQuotes = false;\n let i = 0;\n\n while (i < text.length) {\n const char = text[i];\n const nextChar = text[i + 1];\n\n if (inQuotes) {\n if (char === '\"' && nextChar === '\"') {\n currentField += '\"';\n i += 2;\n } else if (char === '\"') {\n inQuotes = false;\n i++;\n } else {\n currentField += char;\n i++;\n }\n } else {\n if (char === '\"' && currentField === \"\") {\n inQuotes = true;\n i++;\n } else if (char === \"\\t\") {\n currentRow.push(currentField);\n currentField = \"\";\n i++;\n } else if (char === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i++;\n } else if (char === \"\\r\" && nextChar === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i += 2;\n } else {\n currentField += char;\n i++;\n }\n }\n }\n\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n\n return rows;\n }\n\n const lines = text.split(\"\\n\").map((l) => l.replace(/\\r$/, \"\"));\n let maxTabCount = 0;\n for (const line of lines) {\n const n = countTabs(line);\n if (n > maxTabCount) maxTabCount = n;\n }\n const columnCount = maxTabCount > 0 ? maxTabCount + 1 : fallbackColumnCount;\n if (columnCount <= 0) return [];\n\n const expectedTabCount = columnCount - 1;\n const rows: string[][] = [];\n let buf = \"\";\n let bufTabCount = 0;\n\n for (const line of lines) {\n const tc = countTabs(line);\n\n if (tc === expectedTabCount) {\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n rows.push(line.split(\"\\t\"));\n } else {\n buf = buf ? `${buf}\\n${line}` : line;\n bufTabCount += tc;\n if (bufTabCount === expectedTabCount) {\n rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n }\n }\n }\n\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n\n return rows.length > 0\n ? rows\n : lines.filter((l) => l.length > 0).map((l) => l.split(\"\\t\"));\n}\n\nexport function getIsInPopover(element: unknown): boolean {\n if (!(element instanceof Element)) return false;\n\n return (\n element.closest(\"[data-grid-cell-editor]\") !== null ||\n element.closest(\"[data-grid-popover]\") !== null ||\n element.closest(\"[data-slot='dropdown-menu-content']\") !== null ||\n element.closest(\"[data-slot='popover-content']\") !== null\n );\n}\n\nexport function getColumnVariant(variant?: CellOpts[\"variant\"]): {\n icon: React.ComponentType>;\n label: string;\n} | null {\n switch (variant) {\n case \"short-text\":\n return { label: \"Short text\", icon: BaselineIcon };\n case \"long-text\":\n return { label: \"Long text\", icon: TextInitialIcon };\n case \"number\":\n return { label: \"Number\", icon: HashIcon };\n case \"url\":\n return { label: \"URL\", icon: LinkIcon };\n case \"checkbox\":\n return { label: \"Checkbox\", icon: CheckSquareIcon };\n case \"select\":\n return { label: \"Select\", icon: ListIcon };\n case \"multi-select\":\n return { label: \"Multi-select\", icon: ListChecksIcon };\n case \"date\":\n return { label: \"Date\", icon: CalendarIcon };\n case \"file\":\n return { label: \"File\", icon: FileIcon };\n default:\n return null;\n }\n}\n\nexport function getEmptyCellValue(\n variant: CellOpts[\"variant\"] | undefined,\n): unknown {\n if (variant === \"multi-select\" || variant === \"file\") return [];\n if (variant === \"number\" || variant === \"date\" || variant === \"select\")\n return null;\n if (variant === \"checkbox\") return false;\n return \"\";\n}\n\nexport function getUrlHref(urlString: string): string {\n if (!urlString || urlString.trim() === \"\") return \"\";\n\n const trimmed = urlString.trim();\n\n // Reject dangerous protocols (extra safety, though our http:// prefix would neutralize them)\n if (/^(javascript|data|vbscript|file):/i.test(trimmed)) {\n return \"\";\n }\n\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed;\n }\n\n return `http://${trimmed}`;\n}\n\nexport function parseLocalDate(dateStr: unknown): Date | null {\n if (!dateStr) return null;\n if (dateStr instanceof Date) return dateStr;\n if (typeof dateStr !== \"string\") return null;\n const [year, month, day] = dateStr.split(\"-\").map(Number);\n if (!year || !month || !day) return null;\n const date = new Date(year, month - 1, day);\n // Verify date wasn't auto-corrected (e.g. Feb 30 -> Mar 1)\n if (\n date.getFullYear() !== year ||\n date.getMonth() !== month - 1 ||\n date.getDate() !== day\n ) {\n return null;\n }\n return date;\n}\n\nexport function formatDateToString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\nexport function formatDateForDisplay(dateStr: unknown): string {\n if (!dateStr) return \"\";\n const date = parseLocalDate(dateStr);\n if (!date) return typeof dateStr === \"string\" ? dateStr : \"\";\n return date.toLocaleDateString();\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(\n sizes.length - 1,\n Math.floor(Math.log(bytes) / Math.log(k)),\n );\n return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\nexport function getFileIcon(\n type: string,\n): React.ComponentType> {\n if (type.startsWith(\"image/\")) return FileImage;\n if (type.startsWith(\"video/\")) return FileVideo;\n if (type.startsWith(\"audio/\")) return FileAudio;\n if (type.includes(\"pdf\")) return FileText;\n if (type.includes(\"zip\") || type.includes(\"rar\")) return FileArchive;\n if (\n type.includes(\"word\") ||\n type.includes(\"document\") ||\n type.includes(\"doc\")\n )\n return FileText;\n if (type.includes(\"sheet\") || type.includes(\"excel\") || type.includes(\"xls\"))\n return FileSpreadsheet;\n if (\n type.includes(\"presentation\") ||\n type.includes(\"powerpoint\") ||\n type.includes(\"ppt\")\n )\n return Presentation;\n return File;\n}\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "content": "import type { Cell, RowData } from \"@tanstack/react-table\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\n\nexport type Direction = \"ltr\" | \"rtl\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n label: string;\n value: string;\n icon?: React.ComponentType>;\n count?: number;\n}\n\nexport type CellOpts =\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 | {\n variant: \"url\";\n }\n | {\n variant: \"file\";\n maxFileSize?: number;\n maxFiles?: number;\n accept?: string;\n multiple?: boolean;\n };\n\nexport interface CellUpdate {\n rowIndex: number;\n columnId: string;\n value: unknown;\n}\n\nexport interface DataGridColumnMeta {\n label?: string;\n cell?: CellOpts;\n}\n\nexport interface DataGridTableMeta {\n dataGridRef?: React.RefObject;\n cellMapRef?: React.RefObject>;\n focusedCell?: CellPosition | null;\n editingCell?: CellPosition | null;\n selectionState?: SelectionState;\n searchOpen?: boolean;\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getVisualRowIndex?: (rowId: string) => number | undefined;\n scrollToCell?: (\n rowIndex: number,\n columnId: string,\n align?: \"auto\" | \"start\" | \"center\" | \"end\",\n ) => void;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (value: RowHeightValue) => void;\n onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;\n onDataUpdate?: (params: CellUpdate | Array) => void;\n onRowsDelete?: (rowIndices: number[]) => void | Promise;\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?: (rowIndex: number, columnId: string) => 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 onCellsCopy?: () => void;\n onCellsCut?: () => void;\n onCellsPaste?: (expand?: boolean) => void;\n onSelectionClear?: () => void;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise;\n contextMenu?: ContextMenuState;\n onContextMenuOpenChange?: (open: boolean) => void;\n pasteDialog?: PasteDialogState;\n onPasteDialogOpenChange?: (open: boolean) => void;\n readOnly?: boolean;\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 interface PasteDialogState {\n open: boolean;\n rowsNeeded: number;\n clipboardText: string;\n}\n\nexport type NavigationDirection =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"home\"\n | \"end\"\n | \"ctrl+up\"\n | \"ctrl+down\"\n | \"ctrl+home\"\n | \"ctrl+end\"\n | \"pageup\"\n | \"pagedown\"\n | \"pageleft\"\n | \"pageright\";\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\nexport interface DataGridCellProps {\n cell: Cell;\n tableMeta: DataGridTableMeta;\n rowIndex: number;\n columnId: string;\n rowHeight: RowHeightValue;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n isSearchMatch: boolean;\n isActiveSearchMatch: boolean;\n readOnly: boolean;\n}\n\nexport interface FileCellData {\n id: string;\n name: string;\n size: number;\n type: string;\n url?: string;\n}\n\nexport type TextFilterOperator =\n | \"contains\"\n | \"notContains\"\n | \"equals\"\n | \"notEquals\"\n | \"startsWith\"\n | \"endsWith\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type NumberFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"lessThan\"\n | \"lessThanOrEqual\"\n | \"greaterThan\"\n | \"greaterThanOrEqual\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type DateFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"before\"\n | \"after\"\n | \"onOrBefore\"\n | \"onOrAfter\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type SelectFilterOperator =\n | \"is\"\n | \"isNot\"\n | \"isAnyOf\"\n | \"isNoneOf\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type BooleanFilterOperator = \"isTrue\" | \"isFalse\";\n\nexport type FilterOperator =\n | TextFilterOperator\n | NumberFilterOperator\n | DateFilterOperator\n | SelectFilterOperator\n | BooleanFilterOperator;\n\nexport interface FilterValue {\n operator: FilterOperator;\n value?: string | number | string[];\n endValue?: string | number;\n}\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-features.ts", - "content": "import {\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n createFilteredRowModel,\n createSortedRowModel,\n metaHelper,\n rowSelectionFeature,\n rowSortingFeature,\n tableFeatures,\n} from \"@tanstack/react-table\";\n\nimport type {\n DataGridColumnMeta,\n DataGridTableMeta,\n} from \"@/lib/data-grid-types\";\n\nexport const dataGridFeatures = tableFeatures({\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n rowSelectionFeature,\n rowSortingFeature,\n filteredRowModel: createFilteredRowModel(),\n sortedRowModel: createSortedRowModel(),\n tableMeta: metaHelper(),\n columnMeta: metaHelper(),\n});\n\nexport type DataGridFeatures = typeof dataGridFeatures;\n", - "type": "registry:lib" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/data-table-filter-list.json b/public/r/data-table-filter-list.json deleted file mode 100644 index d6c1351a6..000000000 --- a/public/r/data-table-filter-list.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-table-filter-list", - "title": "Data Table Filter List", - "description": "A filter list component for the data table", - "dependencies": [ - "@tanstack/react-table", - "lucide-react", - "nuqs" - ], - "registryDependencies": [ - "badge", - "button", - "calendar", - "command", - "input", - "popover", - "select" - ], - "files": [ - { - "path": "src/components/data-table/data-table-filter-list.tsx", - "content": "\"use client\";\n\nimport type { Column, RowData, Table } from \"@tanstack/react-table\";\n\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 type {\n DataTableColumnMeta,\n ExtendedColumnFilter,\n FilterOperator,\n JoinOperator,\n} from \"@/lib/data-table-types\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\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 SelectGroup,\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 { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport {\n dataTableConfig,\n getDefaultFilterOperator,\n getFilterOperators,\n} from \"@/lib/data-table-utils\";\nimport { formatDate } from \"@/lib/format\";\nimport { generateId } from \"@/lib/id\";\nimport { getFiltersStateParser } from \"@/lib/parsers\";\nimport { cn } from \"@/lib/utils\";\n\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\nconst FILTER_SHORTCUT_KEY = \"f\";\nconst REMOVE_FILTER_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableFilterListProps<\n TData extends RowData,\n> extends React.ComponentProps {\n table: Table;\n debounceMs?: number;\n throttleMs?: number;\n shallow?: boolean;\n disabled?: boolean;\n}\n\nexport function DataTableFilterList({\n table,\n debounceMs = DEBOUNCE_MS,\n throttleMs = THROTTLE_MS,\n shallow = true,\n disabled,\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 \n {dataTableConfig.joinOperators.map((joinOperator) => (\n \n {joinOperator}\n \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 \n {filterOperators.map((operator) => (\n \n {operator.label}\n \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?: DataTableColumnMeta;\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 \n True\n False\n \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 startDate = dateValue[0]\n ? new Date(Number(dateValue[0]))\n : undefined;\n const endDate = dateValue[1] ? new Date(Number(dateValue[1])) : undefined;\n\n const isSameDate =\n startDate &&\n endDate &&\n startDate.toDateString() === endDate.toDateString();\n\n const displayValue =\n filter.operator === \"isBetween\" && dateValue.length === 2 && !isSameDate\n ? `${formatDate(startDate, { month: \"short\" })} - ${formatDate(endDate, { month: \"short\" })}`\n : startDate\n ? formatDate(startDate, { month: \"short\" })\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 setShowValueSelector(false);\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" - }, - { - "path": "src/components/data-table/data-table-range-filter.tsx", - "content": "\"use client\";\n\nimport type { Column, RowData } from \"@tanstack/react-table\";\n\nimport * as React from \"react\";\n\nimport type { ExtendedColumnFilter } from \"@/lib/data-table-types\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableRangeFilterProps<\n TData extends RowData,\n> extends React.ComponentProps<\"div\"> {\n filter: ExtendedColumnFilter;\n column: Column;\n inputId: string;\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n}\n\nexport function DataTableRangeFilter({\n filter,\n column,\n inputId,\n onFilterUpdate,\n className,\n ...props\n}: DataTableRangeFilterProps) {\n const meta = column.columnDef.meta;\n\n const [min, max] = React.useMemo(() => {\n const range = column.columnDef.meta?.range;\n if (range) return range;\n\n const values = column.getFacetedMinMaxValues();\n if (!values) return [0, 100];\n\n return [values[0], values[1]];\n }, [column]);\n\n const formatValue = React.useCallback(\n (value: string | number | undefined) => {\n if (value === undefined || value === \"\") return \"\";\n const numValue = Number(value);\n return Number.isNaN(numValue)\n ? \"\"\n : numValue.toLocaleString(undefined, {\n maximumFractionDigits: 0,\n });\n },\n [],\n );\n\n const value = React.useMemo(() => {\n if (Array.isArray(filter.value)) return filter.value.map(formatValue);\n return [formatValue(filter.value), \"\"];\n }, [filter.value, formatValue]);\n\n const onRangeValueChange = React.useCallback(\n (value: string, isMin?: boolean) => {\n const numValue = Number(value);\n const currentValues = Array.isArray(filter.value)\n ? filter.value\n : [\"\", \"\"];\n const otherValue = isMin\n ? (currentValues[1] ?? \"\")\n : (currentValues[0] ?? \"\");\n\n if (\n value === \"\" ||\n (!Number.isNaN(numValue) &&\n (isMin\n ? numValue >= min && numValue <= (Number(otherValue) || max)\n : numValue <= max && numValue >= (Number(otherValue) || min)))\n ) {\n onFilterUpdate(filter.filterId, {\n value: isMin ? [value, otherValue] : [otherValue, value],\n });\n }\n },\n [filter.filterId, filter.value, min, max, onFilterUpdate],\n );\n\n return (\n \n onRangeValueChange(event.target.value, true)}\n />\n to\n onRangeValueChange(event.target.value)}\n />\n
\n );\n}\n", - "type": "registry:component", - "target": "src/components/data-table/data-table-range-filter.tsx" - }, - { - "path": "src/components/data-table/data-table-advanced-toolbar.tsx", - "content": "\"use client\";\n\nimport type { RowData, Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableAdvancedToolbarProps<\n TData extends RowData,\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\";\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 as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\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 SortableProps = 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 Sortable(props: SortableProps) {\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 ...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(\n \"`getItemValue` is required when using array of objects\",\n );\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 sortableProps.onDragStart?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(event.active.id);\n },\n [sortableProps.onDragStart],\n );\n\n const onDragEnd = React.useCallback(\n (event: DragEndEvent) => {\n sortableProps.onDragEnd?.(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, sortableProps.onDragEnd],\n );\n\n const onDragCancel = React.useCallback(\n (event: DragEndEvent) => {\n sortableProps.onDragCancel?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(null);\n },\n [sortableProps.onDragCancel],\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 ? SlotPrimitive.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 ? SlotPrimitive.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 ? SlotPrimitive.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 extends Omit<\n React.ComponentProps,\n \"children\"\n> {\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\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 Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n type SortableProps,\n};\n", - "type": "registry:ui", - "target": "src/components/ui/sortable.tsx" - }, - { - "path": "src/components/ui/faceted.tsx", - "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\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 Multiple extends boolean = false,\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" - }, - { - "path": "src/hooks/use-callback-ref.ts", - "content": "import * as React from \"react\";\n\n/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx\n */\n\n/**\n * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a\n * prop or avoid re-executing effects when passed as a dependency\n */\nfunction useCallbackRef unknown>(\n callback: T | undefined,\n): T {\n const callbackRef = React.useRef(callback);\n\n React.useEffect(() => {\n callbackRef.current = callback;\n });\n\n // https://github.com/facebook/react/issues/19240\n return React.useMemo(\n () => ((...args) => callbackRef.current?.(...args)) as T,\n [],\n );\n}\n\nexport { useCallbackRef };\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-debounced-callback.ts", - "content": "/**\n * @see https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts\n */\n\nimport * as React from \"react\";\n\nimport { useCallbackRef } from \"@/hooks/use-callback-ref\";\n\nexport function useDebouncedCallback unknown>(\n callback: T,\n delay: number,\n) {\n const handleCallback = useCallbackRef(callback);\n const debounceTimerRef = React.useRef(0);\n React.useEffect(\n () => () => window.clearTimeout(debounceTimerRef.current),\n [],\n );\n\n const setValue = React.useCallback(\n (...args: Parameters) => {\n window.clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = window.setTimeout(\n () => handleCallback(...args),\n delay,\n );\n },\n [handleCallback, delay],\n );\n\n return setValue;\n}\n", - "type": "registry:hook" - }, - { - "path": "src/lib/compose-refs.ts", - "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef = React.Ref | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef(ref: PossibleRef, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n }\n\n if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs(...refs: PossibleRef[]): React.RefCallback {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup === \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup === \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs(...refs: PossibleRef[]): React.RefCallback {\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-table-utils.ts", - "content": "import type { Column, RowData } from \"@tanstack/react-table\";\n\nimport type {\n ExtendedColumnFilter,\n FilterOperator,\n FilterVariant,\n} from \"@/lib/data-table-types\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nexport type DataTableConfig = typeof dataTableConfig;\n\nexport const dataTableConfig = {\n textOperators: [\n { label: \"Contains\", value: \"iLike\" as const },\n { label: \"Does not contain\", value: \"notILike\" as const },\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n numericOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is less than\", value: \"lt\" as const },\n { label: \"Is less than or equal to\", value: \"lte\" as const },\n { label: \"Is greater than\", value: \"gt\" as const },\n { label: \"Is greater than or equal to\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n dateOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is before\", value: \"lt\" as const },\n { label: \"Is after\", value: \"gt\" as const },\n { label: \"Is on or before\", value: \"lte\" as const },\n { label: \"Is on or after\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is relative to today\", value: \"isRelativeToToday\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n selectOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n multiSelectOperators: [\n { label: \"Has any of\", value: \"inArray\" as const },\n { label: \"Has none of\", value: \"notInArray\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n booleanOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n ],\n sortOrders: [\n { label: \"Asc\", value: \"asc\" as const },\n { label: \"Desc\", value: \"desc\" as const },\n ],\n filterVariants: [\n \"text\",\n \"number\",\n \"range\",\n \"date\",\n \"dateRange\",\n \"boolean\",\n \"select\",\n \"multiSelect\",\n ] as const,\n operators: [\n \"iLike\",\n \"notILike\",\n \"eq\",\n \"ne\",\n \"inArray\",\n \"notInArray\",\n \"isEmpty\",\n \"isNotEmpty\",\n \"lt\",\n \"lte\",\n \"gt\",\n \"gte\",\n \"isBetween\",\n \"isRelativeToToday\",\n ] as const,\n joinOperators: [\"and\", \"or\"] as const,\n};\n\nexport function getColumnPinningStyle({\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 === \"start\" && column.getIsLastColumn(\"start\");\n const isFirstRightPinnedColumn =\n isPinned === \"end\" && column.getIsFirstColumn(\"end\");\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 === \"start\" ? `${column.getStart(\"start\")}px` : undefined,\n right: isPinned === \"end\" ? `${column.getAfter(\"end\")}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" - }, - { - "path": "src/lib/format.ts", - "content": "export function formatDate(\n date: Date | string | number | undefined,\n opts: Intl.DateTimeFormatOptions = {},\n) {\n if (!date) return \"\";\n\n try {\n return new Intl.DateTimeFormat(\"en-US\", {\n month: opts.month ?? \"long\",\n day: opts.day ?? \"numeric\",\n year: opts.year ?? \"numeric\",\n ...opts,\n }).format(new Date(date));\n } catch {\n return \"\";\n }\n}\n", - "type": "registry:lib" - }, - { - "path": "src/lib/id.ts", - "content": "const DEFAULT_ALPHABET =\n \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n\ninterface GenerateIdOptions {\n alphabet?: string;\n length?: number;\n}\n\nexport function generateId(\n prefixOrOptions?: string | GenerateIdOptions,\n inputOptions: GenerateIdOptions = {},\n): string {\n const options =\n typeof prefixOrOptions === \"object\" ? prefixOrOptions : inputOptions;\n\n const { alphabet = DEFAULT_ALPHABET, length = 12 } = options;\n\n return Array.from(\n { length },\n () => alphabet[(Math.random() * alphabet.length) | 0],\n ).join(\"\");\n}\n", - "type": "registry:lib" - }, - { - "path": "src/lib/parsers.ts", - "content": "import { createParser } from \"nuqs/server\";\nimport { z } from \"zod\";\n\nimport type {\n ExtendedColumnFilter,\n ExtendedColumnSort,\n} from \"@/lib/data-table-types\";\n\nimport { dataTableConfig } from \"@/lib/data-table-utils\";\n\nconst sortingItemSchema = z.object({\n id: z.string(),\n desc: z.boolean(),\n});\n\nexport const getSortingStateParser = (\n columnIds?: string[] | Set,\n) => {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) => {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(sortingItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnSort[];\n } catch {\n return null;\n }\n },\n serialize: (value) => JSON.stringify(value),\n eq: (a, b) =>\n a.length === b.length &&\n a.every(\n (item, index) =>\n item.id === b[index]?.id && item.desc === b[index]?.desc,\n ),\n });\n};\n\nconst filterItemSchema = z.object({\n id: z.string(),\n value: z.union([z.string(), z.array(z.string())]),\n variant: z.enum(dataTableConfig.filterVariants),\n operator: z.enum(dataTableConfig.operators),\n filterId: z.string(),\n});\n\nexport type FilterItemSchema = z.infer;\n\nexport const getFiltersStateParser = (\n columnIds?: string[] | Set,\n) => {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) => {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(filterItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnFilter[];\n } catch {\n return null;\n }\n },\n serialize: (value) => JSON.stringify(value),\n eq: (a, b) =>\n a.length === b.length &&\n a.every(\n (filter, index) =>\n filter.id === b[index]?.id &&\n filter.value === b[index]?.value &&\n filter.variant === b[index]?.variant &&\n filter.operator === b[index]?.operator,\n ),\n });\n};\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-table-types.ts", - "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\n\nimport type { DataTableConfig } from \"@/lib/data-table-utils\";\nimport type { FilterItemSchema } from \"@/lib/parsers\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nexport interface DataTableMeta {\n queryKeys?: QueryKeys;\n}\n\nexport interface DataTableColumnMeta {\n label?: string;\n placeholder?: string;\n variant?: FilterVariant;\n options?: Option[];\n range?: [number, number];\n unit?: string;\n icon?: React.ComponentType>;\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.ComponentType>;\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:lib" - }, - { - "path": "src/lib/table-features.ts", - "content": "import {\n columnFacetingFeature,\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n createFacetedMinMaxValues,\n createFacetedRowModel,\n createFacetedUniqueValues,\n createFilteredRowModel,\n createPaginatedRowModel,\n createSortedRowModel,\n metaHelper,\n rowPaginationFeature,\n rowSelectionFeature,\n rowSortingFeature,\n tableFeatures,\n} from \"@tanstack/react-table\";\n\nimport type {\n DataTableColumnMeta,\n DataTableMeta,\n} from \"@/lib/data-table-types\";\n\nexport const dataTableFeatures = tableFeatures({\n columnFilteringFeature,\n columnFacetingFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n rowPaginationFeature,\n rowSelectionFeature,\n rowSortingFeature,\n filteredRowModel: createFilteredRowModel(),\n facetedRowModel: createFacetedRowModel(),\n facetedUniqueValues: createFacetedUniqueValues(),\n facetedMinMaxValues: createFacetedMinMaxValues(),\n paginatedRowModel: createPaginatedRowModel(),\n sortedRowModel: createSortedRowModel(),\n tableMeta: metaHelper(),\n columnMeta: metaHelper(),\n});\n\nexport type DataTableFeatures = typeof dataTableFeatures;\n", - "type": "registry:lib" - }, - { - "path": "src/components/data-table/data-table-view-options.tsx", - "content": "\"use client\";\n\nimport type { RowData, Table } from \"@tanstack/react-table\";\n\nimport { Settings2 } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\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 TData extends RowData,\n> extends React.ComponentProps {\n table: Table;\n disabled?: boolean;\n}\n\nexport function DataTableViewOptions({\n table,\n disabled,\n className,\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", - "type": "registry:component", - "target": "src/components/data-table/data-table-view-options.tsx" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/data-table-filter-menu.json b/public/r/data-table-filter-menu.json deleted file mode 100644 index 862c158ce..000000000 --- a/public/r/data-table-filter-menu.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-table-filter-menu", - "title": "Data Table Filter Menu", - "description": "A filter menu component for the data table", - "dependencies": [ - "@tanstack/react-table", - "lucide-react", - "nuqs" - ], - "registryDependencies": [ - "badge", - "button", - "calendar", - "command", - "input", - "popover", - "select" - ], - "files": [ - { - "path": "src/components/data-table/data-table-filter-menu.tsx", - "content": "\"use client\";\n\nimport type { Column, RowData, Table } from \"@tanstack/react-table\";\n\nimport { BadgeCheck, CalendarIcon, ListFilter, Text, X } from \"lucide-react\";\nimport { useQueryState } from \"nuqs\";\nimport * as React from \"react\";\n\nimport type {\n ExtendedColumnFilter,\n FilterOperator,\n} from \"@/lib/data-table-types\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\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 SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport {\n getDefaultFilterOperator,\n getFilterOperators,\n} from \"@/lib/data-table-utils\";\nimport { formatDate } from \"@/lib/format\";\nimport { generateId } from \"@/lib/id\";\nimport { getFiltersStateParser } from \"@/lib/parsers\";\nimport { cn } from \"@/lib/utils\";\n\nconst DEBOUNCE_MS = 300;\nconst THROTTLE_MS = 50;\nconst FILTER_SHORTCUT_KEY = \"f\";\nconst REMOVE_FILTER_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableFilterMenuProps<\n TData extends RowData,\n> extends React.ComponentProps {\n table: Table;\n debounceMs?: number;\n throttleMs?: number;\n shallow?: boolean;\n disabled?: boolean;\n}\n\nexport function DataTableFilterMenu({\n table,\n debounceMs = DEBOUNCE_MS,\n throttleMs = THROTTLE_MS,\n shallow = true,\n disabled,\n className,\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] = 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 disabled={disabled}\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 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 {filterOperators.map((operator) => (\n \n {operator.label}\n \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 svg:last-child]:hidden\"\n onSelect={() => 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 \n True\n False\n \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 const isMultiSelect = filter.variant === \"multiSelect\";\n const isSelected = selectedValues.includes(option.value);\n\n return (\n {\n const value = isMultiSelect\n ? isSelected\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 \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 startDate = dateValue[0]\n ? new Date(Number(dateValue[0]))\n : undefined;\n const endDate = dateValue[1] ? new Date(Number(dateValue[1])) : undefined;\n\n const isSameDate =\n startDate &&\n endDate &&\n startDate.toDateString() === endDate.toDateString();\n\n const displayValue =\n filter.operator === \"isBetween\" && dateValue.length === 2 && !isSameDate\n ? `${formatDate(startDate, { month: \"short\" })} - ${formatDate(endDate, { month: \"short\" })}`\n : startDate\n ? formatDate(startDate, { month: \"short\" })\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" - }, - { - "path": "src/components/data-table/data-table-range-filter.tsx", - "content": "\"use client\";\n\nimport type { Column, RowData } from \"@tanstack/react-table\";\n\nimport * as React from \"react\";\n\nimport type { ExtendedColumnFilter } from \"@/lib/data-table-types\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableRangeFilterProps<\n TData extends RowData,\n> extends React.ComponentProps<\"div\"> {\n filter: ExtendedColumnFilter;\n column: Column;\n inputId: string;\n onFilterUpdate: (\n filterId: string,\n updates: Partial, \"filterId\">>,\n ) => void;\n}\n\nexport function DataTableRangeFilter({\n filter,\n column,\n inputId,\n onFilterUpdate,\n className,\n ...props\n}: DataTableRangeFilterProps) {\n const meta = column.columnDef.meta;\n\n const [min, max] = React.useMemo(() => {\n const range = column.columnDef.meta?.range;\n if (range) return range;\n\n const values = column.getFacetedMinMaxValues();\n if (!values) return [0, 100];\n\n return [values[0], values[1]];\n }, [column]);\n\n const formatValue = React.useCallback(\n (value: string | number | undefined) => {\n if (value === undefined || value === \"\") return \"\";\n const numValue = Number(value);\n return Number.isNaN(numValue)\n ? \"\"\n : numValue.toLocaleString(undefined, {\n maximumFractionDigits: 0,\n });\n },\n [],\n );\n\n const value = React.useMemo(() => {\n if (Array.isArray(filter.value)) return filter.value.map(formatValue);\n return [formatValue(filter.value), \"\"];\n }, [filter.value, formatValue]);\n\n const onRangeValueChange = React.useCallback(\n (value: string, isMin?: boolean) => {\n const numValue = Number(value);\n const currentValues = Array.isArray(filter.value)\n ? filter.value\n : [\"\", \"\"];\n const otherValue = isMin\n ? (currentValues[1] ?? \"\")\n : (currentValues[0] ?? \"\");\n\n if (\n value === \"\" ||\n (!Number.isNaN(numValue) &&\n (isMin\n ? numValue >= min && numValue <= (Number(otherValue) || max)\n : numValue <= max && numValue >= (Number(otherValue) || min)))\n ) {\n onFilterUpdate(filter.filterId, {\n value: isMin ? [value, otherValue] : [otherValue, value],\n });\n }\n },\n [filter.filterId, filter.value, min, max, onFilterUpdate],\n );\n\n return (\n \n onRangeValueChange(event.target.value, true)}\n />\n to\n onRangeValueChange(event.target.value)}\n />\n \n );\n}\n", - "type": "registry:component", - "target": "src/components/data-table/data-table-range-filter.tsx" - }, - { - "path": "src/components/data-table/data-table-advanced-toolbar.tsx", - "content": "\"use client\";\n\nimport type { RowData, Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nimport { DataTableViewOptions } from \"@/components/data-table/data-table-view-options\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableAdvancedToolbarProps<\n TData extends RowData,\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/hooks/use-callback-ref.ts", - "content": "import * as React from \"react\";\n\n/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx\n */\n\n/**\n * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a\n * prop or avoid re-executing effects when passed as a dependency\n */\nfunction useCallbackRef unknown>(\n callback: T | undefined,\n): T {\n const callbackRef = React.useRef(callback);\n\n React.useEffect(() => {\n callbackRef.current = callback;\n });\n\n // https://github.com/facebook/react/issues/19240\n return React.useMemo(\n () => ((...args) => callbackRef.current?.(...args)) as T,\n [],\n );\n}\n\nexport { useCallbackRef };\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-debounced-callback.ts", - "content": "/**\n * @see https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts\n */\n\nimport * as React from \"react\";\n\nimport { useCallbackRef } from \"@/hooks/use-callback-ref\";\n\nexport function useDebouncedCallback unknown>(\n callback: T,\n delay: number,\n) {\n const handleCallback = useCallbackRef(callback);\n const debounceTimerRef = React.useRef(0);\n React.useEffect(\n () => () => window.clearTimeout(debounceTimerRef.current),\n [],\n );\n\n const setValue = React.useCallback(\n (...args: Parameters) => {\n window.clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = window.setTimeout(\n () => handleCallback(...args),\n delay,\n );\n },\n [handleCallback, delay],\n );\n\n return setValue;\n}\n", - "type": "registry:hook" - }, - { - "path": "src/lib/data-table-utils.ts", - "content": "import type { Column, RowData } from \"@tanstack/react-table\";\n\nimport type {\n ExtendedColumnFilter,\n FilterOperator,\n FilterVariant,\n} from \"@/lib/data-table-types\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nexport type DataTableConfig = typeof dataTableConfig;\n\nexport const dataTableConfig = {\n textOperators: [\n { label: \"Contains\", value: \"iLike\" as const },\n { label: \"Does not contain\", value: \"notILike\" as const },\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n numericOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is less than\", value: \"lt\" as const },\n { label: \"Is less than or equal to\", value: \"lte\" as const },\n { label: \"Is greater than\", value: \"gt\" as const },\n { label: \"Is greater than or equal to\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n dateOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is before\", value: \"lt\" as const },\n { label: \"Is after\", value: \"gt\" as const },\n { label: \"Is on or before\", value: \"lte\" as const },\n { label: \"Is on or after\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is relative to today\", value: \"isRelativeToToday\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n selectOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n multiSelectOperators: [\n { label: \"Has any of\", value: \"inArray\" as const },\n { label: \"Has none of\", value: \"notInArray\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n booleanOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n ],\n sortOrders: [\n { label: \"Asc\", value: \"asc\" as const },\n { label: \"Desc\", value: \"desc\" as const },\n ],\n filterVariants: [\n \"text\",\n \"number\",\n \"range\",\n \"date\",\n \"dateRange\",\n \"boolean\",\n \"select\",\n \"multiSelect\",\n ] as const,\n operators: [\n \"iLike\",\n \"notILike\",\n \"eq\",\n \"ne\",\n \"inArray\",\n \"notInArray\",\n \"isEmpty\",\n \"isNotEmpty\",\n \"lt\",\n \"lte\",\n \"gt\",\n \"gte\",\n \"isBetween\",\n \"isRelativeToToday\",\n ] as const,\n joinOperators: [\"and\", \"or\"] as const,\n};\n\nexport function getColumnPinningStyle({\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 === \"start\" && column.getIsLastColumn(\"start\");\n const isFirstRightPinnedColumn =\n isPinned === \"end\" && column.getIsFirstColumn(\"end\");\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 === \"start\" ? `${column.getStart(\"start\")}px` : undefined,\n right: isPinned === \"end\" ? `${column.getAfter(\"end\")}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" - }, - { - "path": "src/lib/format.ts", - "content": "export function formatDate(\n date: Date | string | number | undefined,\n opts: Intl.DateTimeFormatOptions = {},\n) {\n if (!date) return \"\";\n\n try {\n return new Intl.DateTimeFormat(\"en-US\", {\n month: opts.month ?? \"long\",\n day: opts.day ?? \"numeric\",\n year: opts.year ?? \"numeric\",\n ...opts,\n }).format(new Date(date));\n } catch {\n return \"\";\n }\n}\n", - "type": "registry:lib" - }, - { - "path": "src/lib/id.ts", - "content": "const DEFAULT_ALPHABET =\n \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n\ninterface GenerateIdOptions {\n alphabet?: string;\n length?: number;\n}\n\nexport function generateId(\n prefixOrOptions?: string | GenerateIdOptions,\n inputOptions: GenerateIdOptions = {},\n): string {\n const options =\n typeof prefixOrOptions === \"object\" ? prefixOrOptions : inputOptions;\n\n const { alphabet = DEFAULT_ALPHABET, length = 12 } = options;\n\n return Array.from(\n { length },\n () => alphabet[(Math.random() * alphabet.length) | 0],\n ).join(\"\");\n}\n", - "type": "registry:lib" - }, - { - "path": "src/lib/parsers.ts", - "content": "import { createParser } from \"nuqs/server\";\nimport { z } from \"zod\";\n\nimport type {\n ExtendedColumnFilter,\n ExtendedColumnSort,\n} from \"@/lib/data-table-types\";\n\nimport { dataTableConfig } from \"@/lib/data-table-utils\";\n\nconst sortingItemSchema = z.object({\n id: z.string(),\n desc: z.boolean(),\n});\n\nexport const getSortingStateParser = (\n columnIds?: string[] | Set,\n) => {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) => {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(sortingItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnSort[];\n } catch {\n return null;\n }\n },\n serialize: (value) => JSON.stringify(value),\n eq: (a, b) =>\n a.length === b.length &&\n a.every(\n (item, index) =>\n item.id === b[index]?.id && item.desc === b[index]?.desc,\n ),\n });\n};\n\nconst filterItemSchema = z.object({\n id: z.string(),\n value: z.union([z.string(), z.array(z.string())]),\n variant: z.enum(dataTableConfig.filterVariants),\n operator: z.enum(dataTableConfig.operators),\n filterId: z.string(),\n});\n\nexport type FilterItemSchema = z.infer;\n\nexport const getFiltersStateParser = (\n columnIds?: string[] | Set,\n) => {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) => {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(filterItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnFilter[];\n } catch {\n return null;\n }\n },\n serialize: (value) => JSON.stringify(value),\n eq: (a, b) =>\n a.length === b.length &&\n a.every(\n (filter, index) =>\n filter.id === b[index]?.id &&\n filter.value === b[index]?.value &&\n filter.variant === b[index]?.variant &&\n filter.operator === b[index]?.operator,\n ),\n });\n};\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-table-types.ts", - "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\n\nimport type { DataTableConfig } from \"@/lib/data-table-utils\";\nimport type { FilterItemSchema } from \"@/lib/parsers\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nexport interface DataTableMeta {\n queryKeys?: QueryKeys;\n}\n\nexport interface DataTableColumnMeta {\n label?: string;\n placeholder?: string;\n variant?: FilterVariant;\n options?: Option[];\n range?: [number, number];\n unit?: string;\n icon?: React.ComponentType>;\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.ComponentType>;\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:lib" - }, - { - "path": "src/lib/table-features.ts", - "content": "import {\n columnFacetingFeature,\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n createFacetedMinMaxValues,\n createFacetedRowModel,\n createFacetedUniqueValues,\n createFilteredRowModel,\n createPaginatedRowModel,\n createSortedRowModel,\n metaHelper,\n rowPaginationFeature,\n rowSelectionFeature,\n rowSortingFeature,\n tableFeatures,\n} from \"@tanstack/react-table\";\n\nimport type {\n DataTableColumnMeta,\n DataTableMeta,\n} from \"@/lib/data-table-types\";\n\nexport const dataTableFeatures = tableFeatures({\n columnFilteringFeature,\n columnFacetingFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n rowPaginationFeature,\n rowSelectionFeature,\n rowSortingFeature,\n filteredRowModel: createFilteredRowModel(),\n facetedRowModel: createFacetedRowModel(),\n facetedUniqueValues: createFacetedUniqueValues(),\n facetedMinMaxValues: createFacetedMinMaxValues(),\n paginatedRowModel: createPaginatedRowModel(),\n sortedRowModel: createSortedRowModel(),\n tableMeta: metaHelper(),\n columnMeta: metaHelper(),\n});\n\nexport type DataTableFeatures = typeof dataTableFeatures;\n", - "type": "registry:lib" - }, - { - "path": "src/components/data-table/data-table-view-options.tsx", - "content": "\"use client\";\n\nimport type { RowData, Table } from \"@tanstack/react-table\";\n\nimport { Settings2 } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\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 TData extends RowData,\n> extends React.ComponentProps {\n table: Table;\n disabled?: boolean;\n}\n\nexport function DataTableViewOptions({\n table,\n disabled,\n className,\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", - "type": "registry:component", - "target": "src/components/data-table/data-table-view-options.tsx" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/data-table-sort-list.json b/public/r/data-table-sort-list.json deleted file mode 100644 index 1cf9b73fe..000000000 --- a/public/r/data-table-sort-list.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-table-sort-list", - "title": "Data Table Sort List", - "description": "A sort list component for the data table", - "dependencies": [ - "@tanstack/react-table", - "lucide-react" - ], - "registryDependencies": [ - "badge", - "button", - "command", - "popover", - "select" - ], - "files": [ - { - "path": "src/components/data-table/data-table-sort-list.tsx", - "content": "\"use client\";\n\nimport type {\n ColumnSort,\n RowData,\n SortDirection,\n Table,\n} from \"@tanstack/react-table\";\n\nimport {\n ArrowDownUp,\n ChevronsUpDown,\n GripVertical,\n Trash2,\n} from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\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 SelectGroup,\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 \"@/lib/data-table-utils\";\nimport { cn } from \"@/lib/utils\";\n\nconst SORT_SHORTCUT_KEY = \"s\";\nconst REMOVE_SORT_SHORTCUTS = [\"backspace\", \"delete\"];\n\ninterface DataTableSortListProps<\n TData extends RowData,\n> extends React.ComponentProps {\n table: Table;\n disabled?: boolean;\n}\n\nexport function DataTableSortList({\n table,\n disabled,\n className,\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.store.state.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 \n {dataTableConfig.sortOrders.map((order) => (\n \n {order.label}\n \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\";\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 as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\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 SortableProps = 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 Sortable(props: SortableProps) {\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 ...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(\n \"`getItemValue` is required when using array of objects\",\n );\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 sortableProps.onDragStart?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(event.active.id);\n },\n [sortableProps.onDragStart],\n );\n\n const onDragEnd = React.useCallback(\n (event: DragEndEvent) => {\n sortableProps.onDragEnd?.(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, sortableProps.onDragEnd],\n );\n\n const onDragCancel = React.useCallback(\n (event: DragEndEvent) => {\n sortableProps.onDragCancel?.(event);\n\n if (event.activatorEvent.defaultPrevented) return;\n\n setActiveId(null);\n },\n [sortableProps.onDragCancel],\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 ? SlotPrimitive.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 ? SlotPrimitive.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 ? SlotPrimitive.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 extends Omit<\n React.ComponentProps,\n \"children\"\n> {\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\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 Sortable,\n SortableContent,\n SortableItem,\n SortableItemHandle,\n SortableOverlay,\n type SortableProps,\n};\n", - "type": "registry:ui", - "target": "src/components/ui/sortable.tsx" - }, - { - "path": "src/lib/compose-refs.ts", - "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef = React.Ref | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef(ref: PossibleRef, value: T) {\n if (typeof ref === \"function\") {\n return ref(value);\n }\n\n if (ref !== null && ref !== undefined) {\n ref.current = value;\n }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs(...refs: PossibleRef[]): React.RefCallback {\n return (node) => {\n let hasCleanup = false;\n const cleanups = refs.map((ref) => {\n const cleanup = setRef(ref, node);\n if (!hasCleanup && typeof cleanup === \"function\") {\n hasCleanup = true;\n }\n return cleanup;\n });\n\n // React <19 will log an error to the console if a callback ref returns a\n // value. We don't use ref cleanups internally so this will only happen if a\n // user's ref callback returns a value, which we only expect if they are\n // using the cleanup functionality added in React 19.\n if (hasCleanup) {\n return () => {\n for (let i = 0; i < cleanups.length; i++) {\n const cleanup = cleanups[i];\n if (typeof cleanup === \"function\") {\n cleanup();\n } else {\n setRef(refs[i], null);\n }\n }\n };\n }\n };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs(...refs: PossibleRef[]): React.RefCallback {\n return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-table-utils.ts", - "content": "import type { Column, RowData } from \"@tanstack/react-table\";\n\nimport type {\n ExtendedColumnFilter,\n FilterOperator,\n FilterVariant,\n} from \"@/lib/data-table-types\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nexport type DataTableConfig = typeof dataTableConfig;\n\nexport const dataTableConfig = {\n textOperators: [\n { label: \"Contains\", value: \"iLike\" as const },\n { label: \"Does not contain\", value: \"notILike\" as const },\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n numericOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is less than\", value: \"lt\" as const },\n { label: \"Is less than or equal to\", value: \"lte\" as const },\n { label: \"Is greater than\", value: \"gt\" as const },\n { label: \"Is greater than or equal to\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n dateOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is before\", value: \"lt\" as const },\n { label: \"Is after\", value: \"gt\" as const },\n { label: \"Is on or before\", value: \"lte\" as const },\n { label: \"Is on or after\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is relative to today\", value: \"isRelativeToToday\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n selectOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n multiSelectOperators: [\n { label: \"Has any of\", value: \"inArray\" as const },\n { label: \"Has none of\", value: \"notInArray\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n booleanOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n ],\n sortOrders: [\n { label: \"Asc\", value: \"asc\" as const },\n { label: \"Desc\", value: \"desc\" as const },\n ],\n filterVariants: [\n \"text\",\n \"number\",\n \"range\",\n \"date\",\n \"dateRange\",\n \"boolean\",\n \"select\",\n \"multiSelect\",\n ] as const,\n operators: [\n \"iLike\",\n \"notILike\",\n \"eq\",\n \"ne\",\n \"inArray\",\n \"notInArray\",\n \"isEmpty\",\n \"isNotEmpty\",\n \"lt\",\n \"lte\",\n \"gt\",\n \"gte\",\n \"isBetween\",\n \"isRelativeToToday\",\n ] as const,\n joinOperators: [\"and\", \"or\"] as const,\n};\n\nexport function getColumnPinningStyle({\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 === \"start\" && column.getIsLastColumn(\"start\");\n const isFirstRightPinnedColumn =\n isPinned === \"end\" && column.getIsFirstColumn(\"end\");\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 === \"start\" ? `${column.getStart(\"start\")}px` : undefined,\n right: isPinned === \"end\" ? `${column.getAfter(\"end\")}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" - }, - { - "path": "src/lib/data-table-types.ts", - "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\n\nimport type { DataTableConfig } from \"@/lib/data-table-utils\";\nimport type { FilterItemSchema } from \"@/lib/parsers\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nexport interface DataTableMeta {\n queryKeys?: QueryKeys;\n}\n\nexport interface DataTableColumnMeta {\n label?: string;\n placeholder?: string;\n variant?: FilterVariant;\n options?: Option[];\n range?: [number, number];\n unit?: string;\n icon?: React.ComponentType>;\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.ComponentType>;\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:lib" - }, - { - "path": "src/lib/table-features.ts", - "content": "import {\n columnFacetingFeature,\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n createFacetedMinMaxValues,\n createFacetedRowModel,\n createFacetedUniqueValues,\n createFilteredRowModel,\n createPaginatedRowModel,\n createSortedRowModel,\n metaHelper,\n rowPaginationFeature,\n rowSelectionFeature,\n rowSortingFeature,\n tableFeatures,\n} from \"@tanstack/react-table\";\n\nimport type {\n DataTableColumnMeta,\n DataTableMeta,\n} from \"@/lib/data-table-types\";\n\nexport const dataTableFeatures = tableFeatures({\n columnFilteringFeature,\n columnFacetingFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n rowPaginationFeature,\n rowSelectionFeature,\n rowSortingFeature,\n filteredRowModel: createFilteredRowModel(),\n facetedRowModel: createFacetedRowModel(),\n facetedUniqueValues: createFacetedUniqueValues(),\n facetedMinMaxValues: createFacetedMinMaxValues(),\n paginatedRowModel: createPaginatedRowModel(),\n sortedRowModel: createSortedRowModel(),\n tableMeta: metaHelper(),\n columnMeta: metaHelper(),\n});\n\nexport type DataTableFeatures = typeof dataTableFeatures;\n", - "type": "registry:lib" - }, - { - "path": "src/lib/parsers.ts", - "content": "import { createParser } from \"nuqs/server\";\nimport { z } from \"zod\";\n\nimport type {\n ExtendedColumnFilter,\n ExtendedColumnSort,\n} from \"@/lib/data-table-types\";\n\nimport { dataTableConfig } from \"@/lib/data-table-utils\";\n\nconst sortingItemSchema = z.object({\n id: z.string(),\n desc: z.boolean(),\n});\n\nexport const getSortingStateParser = (\n columnIds?: string[] | Set,\n) => {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) => {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(sortingItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnSort[];\n } catch {\n return null;\n }\n },\n serialize: (value) => JSON.stringify(value),\n eq: (a, b) =>\n a.length === b.length &&\n a.every(\n (item, index) =>\n item.id === b[index]?.id && item.desc === b[index]?.desc,\n ),\n });\n};\n\nconst filterItemSchema = z.object({\n id: z.string(),\n value: z.union([z.string(), z.array(z.string())]),\n variant: z.enum(dataTableConfig.filterVariants),\n operator: z.enum(dataTableConfig.operators),\n filterId: z.string(),\n});\n\nexport type FilterItemSchema = z.infer;\n\nexport const getFiltersStateParser = (\n columnIds?: string[] | Set,\n) => {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) => {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(filterItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnFilter[];\n } catch {\n return null;\n }\n },\n serialize: (value) => JSON.stringify(value),\n eq: (a, b) =>\n a.length === b.length &&\n a.every(\n (filter, index) =>\n filter.id === b[index]?.id &&\n filter.value === b[index]?.value &&\n filter.variant === b[index]?.variant &&\n filter.operator === b[index]?.operator,\n ),\n });\n};\n", - "type": "registry:lib" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/data-table.json b/public/r/data-table.json deleted file mode 100644 index 739b986a7..000000000 --- a/public/r/data-table.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "data-table", - "title": "Data Table", - "description": "A feature-rich data table component with server-side filtering, sorting, and pagination", - "dependencies": [ - "@tanstack/react-table", - "lucide-react", - "nuqs" - ], - "registryDependencies": [ - "badge", - "button", - "calendar", - "command", - "dropdown-menu", - "input", - "popover", - "select", - "separator", - "slider", - "table", - "label", - "skeleton" - ], - "files": [ - { - "path": "src/components/data-table/data-table.tsx", - "content": "import type * as React from \"react\";\n\nimport {\n flexRender,\n type RowData,\n type Table as TanstackTable,\n} from \"@tanstack/react-table\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nimport { DataTablePagination } from \"@/components/data-table/data-table-pagination\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from \"@/components/ui/table\";\nimport { getColumnPinningStyle } from \"@/lib/data-table-utils\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTableProps<\n TData extends RowData,\n> extends React.ComponentProps<\"div\"> {\n table: TanstackTable;\n actionBar?: React.ReactNode;\n}\n\nexport function DataTable({\n table,\n actionBar,\n children,\n className,\n ...props\n}: DataTableProps) {\n return (\n \n {children}\n
\n \n \n {table.getHeaderGroups().map((headerGroup) => (\n \n {headerGroup.headers.map((header) => (\n \n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext(),\n )}\n \n ))}\n \n ))}\n \n \n {table.getRowModel().rows?.length ? (\n table.getRowModel().rows.map((row) => (\n \n {row.getVisibleCells().map((cell) => (\n \n {flexRender(\n cell.column.columnDef.cell,\n cell.getContext(),\n )}\n \n ))}\n \n ))\n ) : (\n \n \n No results.\n \n \n )}\n \n
\n
\n
\n \n {actionBar &&\n table.getFilteredSelectedRowModel().rows.length > 0 &&\n actionBar}\n
\n
\n );\n}\n", - "type": "registry:component", - "target": "src/components/data-table/data-table.tsx" - }, - { - "path": "src/components/data-table/data-table-column-header.tsx", - "content": "\"use client\";\n\nimport type { Column, RowData } from \"@tanstack/react-table\";\n\nimport {\n ChevronDown,\n ChevronsUpDown,\n ChevronUp,\n EyeOff,\n X,\n} from \"lucide-react\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\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 TData extends RowData,\n TValue,\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\"\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\"\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\"\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 { RowData, Table } from \"@tanstack/react-table\";\n\nimport {\n ChevronLeft,\n ChevronRight,\n ChevronsLeft,\n ChevronsRight,\n} from \"lucide-react\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { cn } from \"@/lib/utils\";\n\ninterface DataTablePaginationProps<\n TData extends RowData,\n> 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 \n {pageSizeOptions.map((pageSize) => (\n \n {pageSize}\n \n ))}\n \n \n \n
\n
\n Page {table.store.state.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\";\n\nimport type { RowData, Table } from \"@tanstack/react-table\";\n\nimport { Settings2 } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\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 TData extends RowData,\n> extends React.ComponentProps {\n table: Table;\n disabled?: boolean;\n}\n\nexport function DataTableViewOptions({\n table,\n disabled,\n className,\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", - "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\";\n\nimport type { Column, RowData } from \"@tanstack/react-table\";\n\nimport { Check, PlusCircle, XCircle } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { Option } from \"@/lib/data-table-types\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\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\";\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 ) : (\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 svg:last-child]:hidden\"\n onSelect={() => 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\";\n\nimport type { Column, RowData, Table } from \"@tanstack/react-table\";\n\nimport { X } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\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<\n TData extends RowData,\n> 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.store.state.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-30\", 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\";\n\nimport type { Column, RowData } from \"@tanstack/react-table\";\n\nimport { PlusCircle, XCircle } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\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 \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\";\n\nimport type { Column, RowData } from \"@tanstack/react-table\";\nimport type { DateRange } from \"react-day-picker\";\n\nimport { CalendarIcon, XCircle } from \"lucide-react\";\nimport * as React from \"react\";\n\nimport type { DataTableFeatures } from \"@/lib/table-features\";\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 \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", - "type": "registry:component", - "target": "src/components/data-table/data-table-skeleton.tsx" - }, - { - "path": "src/hooks/use-callback-ref.ts", - "content": "import * as React from \"react\";\n\n/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx\n */\n\n/**\n * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a\n * prop or avoid re-executing effects when passed as a dependency\n */\nfunction useCallbackRef unknown>(\n callback: T | undefined,\n): T {\n const callbackRef = React.useRef(callback);\n\n React.useEffect(() => {\n callbackRef.current = callback;\n });\n\n // https://github.com/facebook/react/issues/19240\n return React.useMemo(\n () => ((...args) => callbackRef.current?.(...args)) as T,\n [],\n );\n}\n\nexport { useCallbackRef };\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-data-table.ts", - "content": "import {\n type ColumnFiltersState,\n type ColumnVisibilityState,\n type PaginationState,\n type RowData,\n type RowSelectionState,\n type SortingState,\n type TableOptions,\n type TableState,\n type Updater,\n useTable,\n} from \"@tanstack/react-table\";\nimport {\n parseAsArrayOf,\n parseAsInteger,\n parseAsString,\n type SingleParser,\n useQueryState,\n type UseQueryStateOptions,\n useQueryStates,\n} from \"nuqs\";\nimport * as React from \"react\";\n\nimport type { ExtendedColumnSort, QueryKeys } from \"@/lib/data-table-types\";\n\nimport { useDebouncedCallback } from \"@/hooks/use-debounced-callback\";\nimport { getSortingStateParser } from \"@/lib/parsers\";\nimport {\n type DataTableFeatures,\n dataTableFeatures,\n} from \"@/lib/table-features\";\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\n Omit<\n TableOptions,\n | \"state\"\n | \"pageCount\"\n | \"features\"\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(\n props: UseDataTableProps,\n) {\n const {\n columns,\n pageCount,\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 void setSorting(newSorting as ExtendedColumnSort[]);\n } else {\n void 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 = useTable({\n ...tableProps,\n features: dataTableFeatures,\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 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 React.useMemo(\n () => ({ table, shallow, debounceMs, throttleMs }),\n [table, shallow, debounceMs, throttleMs],\n );\n}\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-debounced-callback.ts", - "content": "/**\n * @see https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts\n */\n\nimport * as React from \"react\";\n\nimport { useCallbackRef } from \"@/hooks/use-callback-ref\";\n\nexport function useDebouncedCallback unknown>(\n callback: T,\n delay: number,\n) {\n const handleCallback = useCallbackRef(callback);\n const debounceTimerRef = React.useRef(0);\n React.useEffect(\n () => () => window.clearTimeout(debounceTimerRef.current),\n [],\n );\n\n const setValue = React.useCallback(\n (...args: Parameters) => {\n window.clearTimeout(debounceTimerRef.current);\n debounceTimerRef.current = window.setTimeout(\n () => handleCallback(...args),\n delay,\n );\n },\n [handleCallback, delay],\n );\n\n return setValue;\n}\n", - "type": "registry:hook" - }, - { - "path": "src/lib/data-table-utils.ts", - "content": "import type { Column, RowData } from \"@tanstack/react-table\";\n\nimport type {\n ExtendedColumnFilter,\n FilterOperator,\n FilterVariant,\n} from \"@/lib/data-table-types\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nexport type DataTableConfig = typeof dataTableConfig;\n\nexport const dataTableConfig = {\n textOperators: [\n { label: \"Contains\", value: \"iLike\" as const },\n { label: \"Does not contain\", value: \"notILike\" as const },\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n numericOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is less than\", value: \"lt\" as const },\n { label: \"Is less than or equal to\", value: \"lte\" as const },\n { label: \"Is greater than\", value: \"gt\" as const },\n { label: \"Is greater than or equal to\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n dateOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is before\", value: \"lt\" as const },\n { label: \"Is after\", value: \"gt\" as const },\n { label: \"Is on or before\", value: \"lte\" as const },\n { label: \"Is on or after\", value: \"gte\" as const },\n { label: \"Is between\", value: \"isBetween\" as const },\n { label: \"Is relative to today\", value: \"isRelativeToToday\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n selectOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n multiSelectOperators: [\n { label: \"Has any of\", value: \"inArray\" as const },\n { label: \"Has none of\", value: \"notInArray\" as const },\n { label: \"Is empty\", value: \"isEmpty\" as const },\n { label: \"Is not empty\", value: \"isNotEmpty\" as const },\n ],\n booleanOperators: [\n { label: \"Is\", value: \"eq\" as const },\n { label: \"Is not\", value: \"ne\" as const },\n ],\n sortOrders: [\n { label: \"Asc\", value: \"asc\" as const },\n { label: \"Desc\", value: \"desc\" as const },\n ],\n filterVariants: [\n \"text\",\n \"number\",\n \"range\",\n \"date\",\n \"dateRange\",\n \"boolean\",\n \"select\",\n \"multiSelect\",\n ] as const,\n operators: [\n \"iLike\",\n \"notILike\",\n \"eq\",\n \"ne\",\n \"inArray\",\n \"notInArray\",\n \"isEmpty\",\n \"isNotEmpty\",\n \"lt\",\n \"lte\",\n \"gt\",\n \"gte\",\n \"isBetween\",\n \"isRelativeToToday\",\n ] as const,\n joinOperators: [\"and\", \"or\"] as const,\n};\n\nexport function getColumnPinningStyle({\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 === \"start\" && column.getIsLastColumn(\"start\");\n const isFirstRightPinnedColumn =\n isPinned === \"end\" && column.getIsFirstColumn(\"end\");\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 === \"start\" ? `${column.getStart(\"start\")}px` : undefined,\n right: isPinned === \"end\" ? `${column.getAfter(\"end\")}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" - }, - { - "path": "src/lib/format.ts", - "content": "export function formatDate(\n date: Date | string | number | undefined,\n opts: Intl.DateTimeFormatOptions = {},\n) {\n if (!date) return \"\";\n\n try {\n return new Intl.DateTimeFormat(\"en-US\", {\n month: opts.month ?? \"long\",\n day: opts.day ?? \"numeric\",\n year: opts.year ?? \"numeric\",\n ...opts,\n }).format(new Date(date));\n } catch {\n return \"\";\n }\n}\n", - "type": "registry:lib" - }, - { - "path": "src/lib/parsers.ts", - "content": "import { createParser } from \"nuqs/server\";\nimport { z } from \"zod\";\n\nimport type {\n ExtendedColumnFilter,\n ExtendedColumnSort,\n} from \"@/lib/data-table-types\";\n\nimport { dataTableConfig } from \"@/lib/data-table-utils\";\n\nconst sortingItemSchema = z.object({\n id: z.string(),\n desc: z.boolean(),\n});\n\nexport const getSortingStateParser = (\n columnIds?: string[] | Set,\n) => {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) => {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(sortingItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnSort[];\n } catch {\n return null;\n }\n },\n serialize: (value) => JSON.stringify(value),\n eq: (a, b) =>\n a.length === b.length &&\n a.every(\n (item, index) =>\n item.id === b[index]?.id && item.desc === b[index]?.desc,\n ),\n });\n};\n\nconst filterItemSchema = z.object({\n id: z.string(),\n value: z.union([z.string(), z.array(z.string())]),\n variant: z.enum(dataTableConfig.filterVariants),\n operator: z.enum(dataTableConfig.operators),\n filterId: z.string(),\n});\n\nexport type FilterItemSchema = z.infer;\n\nexport const getFiltersStateParser = (\n columnIds?: string[] | Set,\n) => {\n const validKeys = columnIds\n ? columnIds instanceof Set\n ? columnIds\n : new Set(columnIds)\n : null;\n\n return createParser({\n parse: (value) => {\n try {\n const parsed = JSON.parse(value);\n const result = z.array(filterItemSchema).safeParse(parsed);\n\n if (!result.success) return null;\n\n if (validKeys && result.data.some((item) => !validKeys.has(item.id))) {\n return null;\n }\n\n return result.data as ExtendedColumnFilter[];\n } catch {\n return null;\n }\n },\n serialize: (value) => JSON.stringify(value),\n eq: (a, b) =>\n a.length === b.length &&\n a.every(\n (filter, index) =>\n filter.id === b[index]?.id &&\n filter.value === b[index]?.value &&\n filter.variant === b[index]?.variant &&\n filter.operator === b[index]?.operator,\n ),\n });\n};\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-table-types.ts", - "content": "import type { ColumnSort, Row, RowData } from \"@tanstack/react-table\";\n\nimport type { DataTableConfig } from \"@/lib/data-table-utils\";\nimport type { FilterItemSchema } from \"@/lib/parsers\";\nimport type { DataTableFeatures } from \"@/lib/table-features\";\n\nexport interface DataTableMeta {\n queryKeys?: QueryKeys;\n}\n\nexport interface DataTableColumnMeta {\n label?: string;\n placeholder?: string;\n variant?: FilterVariant;\n options?: Option[];\n range?: [number, number];\n unit?: string;\n icon?: React.ComponentType>;\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.ComponentType>;\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:lib" - }, - { - "path": "src/lib/table-features.ts", - "content": "import {\n columnFacetingFeature,\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n createFacetedMinMaxValues,\n createFacetedRowModel,\n createFacetedUniqueValues,\n createFilteredRowModel,\n createPaginatedRowModel,\n createSortedRowModel,\n metaHelper,\n rowPaginationFeature,\n rowSelectionFeature,\n rowSortingFeature,\n tableFeatures,\n} from \"@tanstack/react-table\";\n\nimport type {\n DataTableColumnMeta,\n DataTableMeta,\n} from \"@/lib/data-table-types\";\n\nexport const dataTableFeatures = tableFeatures({\n columnFilteringFeature,\n columnFacetingFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n rowPaginationFeature,\n rowSelectionFeature,\n rowSortingFeature,\n filteredRowModel: createFilteredRowModel(),\n facetedRowModel: createFacetedRowModel(),\n facetedUniqueValues: createFacetedUniqueValues(),\n facetedMinMaxValues: createFacetedMinMaxValues(),\n paginatedRowModel: createPaginatedRowModel(),\n sortedRowModel: createSortedRowModel(),\n tableMeta: metaHelper(),\n columnMeta: metaHelper(),\n});\n\nexport type DataTableFeatures = typeof dataTableFeatures;\n", - "type": "registry:lib" - } - ], - "type": "registry:component" -} \ No newline at end of file diff --git a/public/r/direction.json b/public/r/direction.json deleted file mode 100644 index 05d60c98b..000000000 --- a/public/r/direction.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "direction", - "title": "Direction", - "description": "A direction provider for RTL/LTR support", - "dependencies": [ - "radix-ui" - ], - "files": [ - { - "path": "src/components/ui/direction.tsx", - "content": "\"use client\";\r\n\r\nimport { Direction } from \"radix-ui\";\r\nimport type * as React from \"react\";\r\n\r\nfunction DirectionProvider({\r\n dir,\r\n direction,\r\n children,\r\n ...props\r\n}: React.ComponentProps & {\r\n direction?: React.ComponentProps[\"dir\"];\r\n}) {\r\n return (\r\n \r\n {children}\r\n \r\n );\r\n}\r\n\r\nconst useDirection = Direction.useDirection;\r\n\r\nexport { DirectionProvider, useDirection };\r\n", - "type": "registry:ui", - "target": "src/components/ui/direction.tsx" - } - ], - "type": "registry:ui" -} \ No newline at end of file diff --git a/public/r/registry.json b/public/r/registry.json deleted file mode 100644 index 771757534..000000000 --- a/public/r/registry.json +++ /dev/null @@ -1,689 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry.json", - "name": "tablecn", - "homepage": "https://tablecn.com", - "items": [ - { - "name": "data-table", - "type": "registry:component", - "title": "Data Table", - "description": "A feature-rich data table component with server-side filtering, sorting, and pagination", - "registryDependencies": [ - "badge", - "button", - "calendar", - "command", - "dropdown-menu", - "input", - "popover", - "select", - "separator", - "slider", - "table", - "label", - "skeleton" - ], - "dependencies": ["@tanstack/react-table", "lucide-react", "nuqs"], - "files": [ - { - "path": "src/components/data-table/data-table.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table.tsx" - }, - { - "path": "src/components/data-table/data-table-column-header.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-column-header.tsx" - }, - { - "path": "src/components/data-table/data-table-pagination.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-pagination.tsx" - }, - { - "path": "src/components/data-table/data-table-view-options.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-view-options.tsx" - }, - { - "path": "src/components/data-table/data-table-faceted-filter.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-faceted-filter.tsx" - }, - { - "path": "src/components/data-table/data-table-toolbar.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-toolbar.tsx" - }, - { - "path": "src/components/data-table/data-table-slider-filter.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-slider-filter.tsx" - }, - { - "path": "src/components/data-table/data-table-date-filter.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-date-filter.tsx" - }, - { - "path": "src/components/data-table/data-table-skeleton.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-skeleton.tsx" - }, - { - "path": "src/hooks/use-callback-ref.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-data-table.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-debounced-callback.ts", - "type": "registry:hook" - }, - { - "path": "src/lib/data-table-utils.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/format.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/parsers.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-table-types.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/table-features.ts", - "type": "registry:lib" - } - ] - }, - { - "name": "data-table-sort-list", - "type": "registry:component", - "title": "Data Table Sort List", - "description": "A sort list component for the data table", - "registryDependencies": [ - "badge", - "button", - "command", - "popover", - "select" - ], - "dependencies": ["@tanstack/react-table", "lucide-react"], - "files": [ - { - "path": "src/components/data-table/data-table-sort-list.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-sort-list.tsx" - }, - { - "path": "src/components/ui/sortable.tsx", - "type": "registry:ui", - "target": "src/components/ui/sortable.tsx" - }, - { - "path": "src/lib/compose-refs.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-table-utils.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-table-types.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/table-features.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/parsers.ts", - "type": "registry:lib" - } - ] - }, - { - "name": "data-table-filter-list", - "type": "registry:component", - "title": "Data Table Filter List", - "description": "A filter list component for the data table", - "registryDependencies": [ - "badge", - "button", - "calendar", - "command", - "input", - "popover", - "select" - ], - "dependencies": ["@tanstack/react-table", "lucide-react", "nuqs"], - "files": [ - { - "path": "src/components/data-table/data-table-filter-list.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-filter-list.tsx" - }, - { - "path": "src/components/data-table/data-table-range-filter.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-range-filter.tsx" - }, - { - "path": "src/components/data-table/data-table-advanced-toolbar.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-advanced-toolbar.tsx" - }, - { - "path": "src/components/ui/sortable.tsx", - "type": "registry:ui", - "target": "src/components/ui/sortable.tsx" - }, - { - "path": "src/components/ui/faceted.tsx", - "type": "registry:ui", - "target": "src/components/ui/faceted.tsx" - }, - { - "path": "src/hooks/use-callback-ref.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-debounced-callback.ts", - "type": "registry:hook" - }, - { - "path": "src/lib/compose-refs.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-table-utils.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/format.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/id.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/parsers.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-table-types.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/table-features.ts", - "type": "registry:lib" - }, - { - "path": "src/components/data-table/data-table-view-options.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-view-options.tsx" - } - ] - }, - { - "name": "data-table-filter-menu", - "type": "registry:component", - "title": "Data Table Filter Menu", - "description": "A filter menu component for the data table", - "registryDependencies": [ - "badge", - "button", - "calendar", - "command", - "input", - "popover", - "select" - ], - "dependencies": ["@tanstack/react-table", "lucide-react", "nuqs"], - "files": [ - { - "path": "src/components/data-table/data-table-filter-menu.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-filter-menu.tsx" - }, - { - "path": "src/components/data-table/data-table-range-filter.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-range-filter.tsx" - }, - { - "path": "src/components/data-table/data-table-advanced-toolbar.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-advanced-toolbar.tsx" - }, - { - "path": "src/hooks/use-callback-ref.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-debounced-callback.ts", - "type": "registry:hook" - }, - { - "path": "src/lib/data-table-utils.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/format.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/id.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/parsers.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-table-types.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/table-features.ts", - "type": "registry:lib" - }, - { - "path": "src/components/data-table/data-table-view-options.tsx", - "type": "registry:component", - "target": "src/components/data-table/data-table-view-options.tsx" - } - ] - }, - { - "name": "data-grid", - "type": "registry:component", - "title": "Data Grid", - "description": "A high-performance editable data grid component with virtualization, keyboard navigation, and cell editing", - "registryDependencies": [ - "badge", - "button", - "calendar", - "checkbox", - "command", - "dialog", - "dropdown-menu", - "input", - "popover", - "select", - "separator", - "skeleton", - "textarea", - "tooltip" - ], - "dependencies": [ - "@tanstack/react-table", - "@tanstack/react-virtual", - "lucide-react", - "sonner" - ], - "files": [ - { - "path": "src/components/data-grid/data-grid.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid.tsx" - }, - { - "path": "src/components/data-grid/data-grid-presence.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-presence.tsx" - }, - { - "path": "src/components/data-grid/data-grid-cell.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-cell.tsx" - }, - { - "path": "src/components/data-grid/data-grid-cell-wrapper.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-cell-wrapper.tsx" - }, - { - "path": "src/components/data-grid/data-grid-cell-variants.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-cell-variants.tsx" - }, - { - "path": "src/components/data-grid/data-grid-column-header.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-column-header.tsx" - }, - { - "path": "src/components/data-grid/data-grid-context-menu.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-context-menu.tsx" - }, - { - "path": "src/components/data-grid/data-grid-paste-dialog.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-paste-dialog.tsx" - }, - { - "path": "src/components/data-grid/data-grid-row.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-row.tsx" - }, - { - "path": "src/components/data-grid/data-grid-search.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-search.tsx" - }, - { - "path": "src/components/ui/direction.tsx", - "type": "registry:ui", - "target": "src/components/ui/direction.tsx" - }, - { - "path": "src/hooks/use-as-ref.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-badge-overflow.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-callback-ref.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-data-grid.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-debounced-callback.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-isomorphic-layout-effect.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-lazy-ref.ts", - "type": "registry:hook" - }, - { - "path": "src/lib/compose-refs.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-utils.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-features.ts", - "type": "registry:lib" - } - ] - }, - { - "name": "data-grid-select-column", - "type": "registry:component", - "title": "Data Grid Select Column", - "description": "A reusable select column definition for the data grid with checkbox selection and shift-click support", - "registryDependencies": ["checkbox"], - "dependencies": ["@tanstack/react-table"], - "files": [ - { - "path": "src/components/data-grid/data-grid-select-column.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-select-column.tsx" - }, - { - "path": "src/lib/data-grid-features.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "type": "registry:lib" - } - ] - }, - { - "name": "data-grid-sort-menu", - "type": "registry:component", - "title": "Data Grid Sort Menu", - "description": "A sort menu component for the data grid with drag-and-drop reordering", - "registryDependencies": [ - "badge", - "button", - "command", - "popover", - "select" - ], - "dependencies": ["@tanstack/react-table", "lucide-react"], - "files": [ - { - "path": "src/components/data-grid/data-grid-sort-menu.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-sort-menu.tsx" - }, - { - "path": "src/components/ui/direction.tsx", - "type": "registry:ui", - "target": "src/components/ui/direction.tsx" - }, - { - "path": "src/components/ui/sortable.tsx", - "type": "registry:ui", - "target": "src/components/ui/sortable.tsx" - }, - { - "path": "src/lib/compose-refs.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-features.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "type": "registry:lib" - } - ] - }, - { - "name": "data-grid-row-height-menu", - "type": "registry:component", - "title": "Data Grid Row Height Menu", - "description": "A row height menu component for the data grid to adjust row sizes", - "registryDependencies": ["select"], - "dependencies": ["@tanstack/react-table", "lucide-react"], - "files": [ - { - "path": "src/components/data-grid/data-grid-row-height-menu.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-row-height-menu.tsx" - }, - { - "path": "src/lib/data-grid-features.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "type": "registry:lib" - } - ] - }, - { - "name": "data-grid-view-menu", - "type": "registry:component", - "title": "Data Grid View Menu", - "description": "A view menu component for the data grid to toggle column visibility", - "registryDependencies": ["button", "command", "popover"], - "dependencies": ["@tanstack/react-table", "lucide-react"], - "files": [ - { - "path": "src/components/data-grid/data-grid-view-menu.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-view-menu.tsx" - }, - { - "path": "src/components/ui/direction.tsx", - "type": "registry:ui", - "target": "src/components/ui/direction.tsx" - }, - { - "path": "src/lib/data-grid-features.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "type": "registry:lib" - } - ] - }, - { - "name": "data-grid-keyboard-shortcuts", - "type": "registry:component", - "title": "Data Grid Keyboard Shortcuts", - "description": "A keyboard shortcuts dialog component for the data grid showing all available shortcuts", - "registryDependencies": ["button", "dialog", "input", "kbd", "separator"], - "dependencies": ["lucide-react"], - "files": [ - { - "path": "src/components/data-grid/data-grid-keyboard-shortcuts.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-keyboard-shortcuts.tsx" - }, - { - "path": "src/components/ui/direction.tsx", - "type": "registry:ui", - "target": "src/components/ui/direction.tsx" - } - ] - }, - { - "name": "data-grid-filter-menu", - "type": "registry:component", - "title": "Data Grid Filter Menu", - "description": "A filter menu component for the data grid with drag-and-drop reordering and advanced filtering", - "registryDependencies": [ - "badge", - "button", - "calendar", - "command", - "input", - "popover", - "select" - ], - "dependencies": ["@tanstack/react-table", "lucide-react"], - "files": [ - { - "path": "src/components/data-grid/data-grid-filter-menu.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-filter-menu.tsx" - }, - { - "path": "src/components/ui/direction.tsx", - "type": "registry:ui", - "target": "src/components/ui/direction.tsx" - }, - { - "path": "src/components/ui/sortable.tsx", - "type": "registry:ui", - "target": "src/components/ui/sortable.tsx" - }, - { - "path": "src/hooks/use-debounced-callback.ts", - "type": "registry:hook" - }, - { - "path": "src/lib/compose-refs.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-filters.ts", - "type": "registry:lib", - "target": "src/lib/data-grid-filters.ts" - }, - { - "path": "src/lib/format.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-features.ts", - "type": "registry:lib" - }, - { - "path": "src/hooks/use-callback-ref.ts", - "type": "registry:hook" - } - ] - }, - { - "name": "data-grid-skeleton", - "type": "registry:component", - "title": "Data Grid Skeleton", - "description": "A composable skeleton component for the data grid with toolbar and grid parts", - "registryDependencies": ["skeleton"], - "dependencies": [], - "files": [ - { - "path": "src/components/data-grid/data-grid-skeleton.tsx", - "type": "registry:component", - "target": "src/components/data-grid/data-grid-skeleton.tsx" - } - ] - }, - { - "name": "use-data-grid-undo-redo", - "type": "registry:hook", - "title": "Use Data Grid Undo Redo", - "description": "An undo/redo hook for the data grid with keyboard shortcuts and history management", - "registryDependencies": [], - "dependencies": ["sonner"], - "files": [ - { - "path": "src/hooks/use-data-grid-undo-redo.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-as-ref.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-lazy-ref.ts", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-isomorphic-layout-effect.ts", - "type": "registry:hook" - }, - { - "path": "src/lib/data-grid-utils.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-features.ts", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "type": "registry:lib" - } - ] - } - ] -} diff --git a/public/r/styles/index.json b/public/r/styles/index.json new file mode 100644 index 000000000..40c7daf5d --- /dev/null +++ b/public/r/styles/index.json @@ -0,0 +1,50 @@ +[ + { + "name": "radix-nova" + }, + { + "name": "radix-vega" + }, + { + "name": "radix-maia" + }, + { + "name": "radix-lyra" + }, + { + "name": "radix-mira" + }, + { + "name": "radix-luma" + }, + { + "name": "radix-sera" + }, + { + "name": "radix-rhea" + }, + { + "name": "base-nova" + }, + { + "name": "base-vega" + }, + { + "name": "base-maia" + }, + { + "name": "base-lyra" + }, + { + "name": "base-mira" + }, + { + "name": "base-luma" + }, + { + "name": "base-sera" + }, + { + "name": "base-rhea" + } +] diff --git a/public/r/use-data-grid-undo-redo.json b/public/r/use-data-grid-undo-redo.json deleted file mode 100644 index 9ec7ed307..000000000 --- a/public/r/use-data-grid-undo-redo.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema/registry-item.json", - "name": "use-data-grid-undo-redo", - "title": "Use Data Grid Undo Redo", - "description": "An undo/redo hook for the data grid with keyboard shortcuts and history management", - "dependencies": [ - "sonner" - ], - "registryDependencies": [], - "files": [ - { - "path": "src/hooks/use-data-grid-undo-redo.ts", - "content": "import * as React from \"react\";\nimport { toast } from \"sonner\";\n\nimport { useAsRef } from \"@/hooks/use-as-ref\";\nimport { useLazyRef } from \"@/hooks/use-lazy-ref\";\nimport { getIsInPopover } from \"@/lib/data-grid-utils\";\n\nconst DEFAULT_MAX_HISTORY = 100;\nconst BATCH_TIMEOUT = 300;\n\ninterface HistoryEntry {\n variant: \"cells_update\" | \"rows_add\" | \"rows_delete\";\n count: number;\n timestamp: number;\n undo: (currentData: TData[]) => TData[];\n redo: (currentData: TData[]) => TData[];\n}\n\ninterface UndoRedoCellUpdate {\n rowId: string;\n columnId: string;\n previousValue: unknown;\n newValue: unknown;\n}\n\ninterface StoreState {\n undoStack: HistoryEntry[];\n redoStack: HistoryEntry[];\n hasPendingChanges: boolean;\n}\n\ninterface Store {\n subscribe: (callback: () => void) => () => void;\n getState: () => StoreState;\n push: (entry: HistoryEntry) => void;\n undo: () => HistoryEntry | null;\n redo: () => HistoryEntry | null;\n clear: () => void;\n setPendingChanges: (value: boolean) => void;\n notify: () => void;\n}\n\nfunction useStore(\n store: Store,\n selector: (state: StoreState) => boolean,\n): boolean {\n const getSnapshot = React.useCallback(\n () => selector(store.getState()),\n [store, selector],\n );\n\n return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);\n}\n\nfunction buildIndexById(\n data: TData[],\n getRowId: (row: TData) => string,\n): Map {\n const map = new Map();\n for (let i = 0; i < data.length; i++) {\n const row = data[i];\n if (row) {\n map.set(getRowId(row), i);\n }\n }\n return map;\n}\n\nfunction getPendingKey(rowId: string, columnId: string): string {\n return `${rowId}\\0${columnId}`;\n}\n\ninterface UseDataGridUndoRedoProps {\n data: TData[];\n onDataChange: (data: TData[]) => void;\n getRowId: (row: TData) => string;\n maxHistory?: number;\n enabled?: boolean;\n}\n\ninterface UseDataGridUndoRedoReturn {\n canUndo: boolean;\n canRedo: boolean;\n onUndo: () => void;\n onRedo: () => void;\n onClear: () => void;\n trackCellsUpdate: (updates: UndoRedoCellUpdate[]) => void;\n trackRowsAdd: (rows: TData[]) => void;\n trackRowsDelete: (rows: TData[]) => void;\n}\n\nfunction useDataGridUndoRedo({\n data,\n onDataChange,\n getRowId,\n maxHistory = DEFAULT_MAX_HISTORY,\n enabled = true,\n}: UseDataGridUndoRedoProps): UseDataGridUndoRedoReturn {\n const propsRef = useAsRef({\n data,\n onDataChange,\n getRowId,\n maxHistory,\n enabled,\n });\n\n const listenersRef = useLazyRef(() => new Set<() => void>());\n\n const stateRef = useLazyRef>(() => ({\n undoStack: [],\n redoStack: [],\n hasPendingChanges: false,\n }));\n\n const pendingBatchRef = React.useRef<{\n byKey: Map;\n timeoutId: ReturnType | null;\n }>({\n byKey: new Map(),\n timeoutId: null,\n });\n\n const pendingNotifyRef = React.useRef(false);\n\n const store = React.useMemo>(() => {\n return {\n subscribe: (callback) => {\n listenersRef.current.add(callback);\n return () => listenersRef.current.delete(callback);\n },\n getState: () => stateRef.current,\n push: (entry) => {\n const state = stateRef.current;\n const newUndoStack = [...state.undoStack, entry];\n\n if (newUndoStack.length > propsRef.current.maxHistory) {\n newUndoStack.shift();\n }\n\n stateRef.current = {\n undoStack: newUndoStack,\n redoStack: [],\n hasPendingChanges: false,\n };\n store.notify();\n },\n undo: () => {\n const state = stateRef.current;\n if (state.undoStack.length === 0) return null;\n\n const entry = state.undoStack[state.undoStack.length - 1];\n if (!entry) return null;\n\n stateRef.current = {\n undoStack: state.undoStack.slice(0, -1),\n redoStack: [...state.redoStack, entry],\n hasPendingChanges: false,\n };\n store.notify();\n return entry;\n },\n redo: () => {\n const state = stateRef.current;\n if (state.redoStack.length === 0) return null;\n\n const entry = state.redoStack[state.redoStack.length - 1];\n if (!entry) return null;\n\n stateRef.current = {\n undoStack: [...state.undoStack, entry],\n redoStack: state.redoStack.slice(0, -1),\n hasPendingChanges: false,\n };\n store.notify();\n return entry;\n },\n clear: () => {\n stateRef.current = {\n undoStack: [],\n redoStack: [],\n hasPendingChanges: false,\n };\n store.notify();\n },\n setPendingChanges: (value) => {\n if (stateRef.current.hasPendingChanges === value) return;\n stateRef.current = {\n ...stateRef.current,\n hasPendingChanges: value,\n };\n\n if (!pendingNotifyRef.current) {\n pendingNotifyRef.current = true;\n queueMicrotask(() => {\n pendingNotifyRef.current = false;\n store.notify();\n });\n }\n },\n notify: () => {\n for (const listener of listenersRef.current) {\n listener();\n }\n },\n };\n }, [listenersRef, stateRef, propsRef]);\n\n const canUndo = useStore(\n store,\n (state) => state.undoStack.length > 0 || state.hasPendingChanges,\n );\n const canRedo = useStore(store, (state) => state.redoStack.length > 0);\n\n const onCommit = React.useCallback(() => {\n const pending = pendingBatchRef.current;\n if (pending.byKey.size === 0) return;\n\n if (pending.timeoutId) {\n clearTimeout(pending.timeoutId);\n pending.timeoutId = null;\n }\n\n const updates = Array.from(pending.byKey.values());\n pending.byKey.clear();\n\n const { getRowId } = propsRef.current;\n\n const entry: HistoryEntry = {\n variant: \"cells_update\",\n count: updates.length,\n timestamp: Date.now(),\n undo: (currentData) => {\n const newData = [...currentData];\n const indexById = buildIndexById(newData, getRowId);\n\n for (const update of updates) {\n const index = indexById.get(update.rowId);\n if (index !== undefined) {\n const row = newData[index];\n if (row) {\n newData[index] = {\n ...row,\n [update.columnId]: update.previousValue,\n };\n }\n }\n }\n return newData;\n },\n redo: (currentData) => {\n const newData = [...currentData];\n const indexById = buildIndexById(newData, getRowId);\n\n for (const update of updates) {\n const index = indexById.get(update.rowId);\n if (index !== undefined) {\n const row = newData[index];\n if (row) {\n newData[index] = { ...row, [update.columnId]: update.newValue };\n }\n }\n }\n return newData;\n },\n };\n\n store.push(entry);\n }, [store, propsRef]);\n\n const onUndo = React.useCallback(() => {\n if (!propsRef.current.enabled) return;\n\n onCommit();\n\n const entry = store.undo();\n if (!entry) {\n toast.info(\"No actions to undo\");\n return;\n }\n\n const newData = entry.undo(propsRef.current.data);\n propsRef.current.onDataChange(newData);\n\n toast.success(\n `${entry.count} action${entry.count !== 1 ? \"s\" : \"\"} undone`,\n );\n }, [store, propsRef, onCommit]);\n\n const onRedo = React.useCallback(() => {\n if (!propsRef.current.enabled) return;\n\n onCommit();\n\n const entry = store.redo();\n if (!entry) {\n toast.info(\"No actions to redo\");\n return;\n }\n\n const newData = entry.redo(propsRef.current.data);\n propsRef.current.onDataChange(newData);\n\n toast.success(\n `${entry.count} action${entry.count !== 1 ? \"s\" : \"\"} redone`,\n );\n }, [store, propsRef, onCommit]);\n\n const onClear = React.useCallback(() => {\n const pending = pendingBatchRef.current;\n if (pending.timeoutId) {\n clearTimeout(pending.timeoutId);\n pending.timeoutId = null;\n }\n pending.byKey.clear();\n\n store.clear();\n }, [store]);\n\n const trackCellsUpdate = React.useCallback(\n (updates: UndoRedoCellUpdate[]) => {\n if (!propsRef.current.enabled || updates.length === 0) return;\n\n const filteredUpdates = updates.filter(\n (u) => !Object.is(u.previousValue, u.newValue),\n );\n if (filteredUpdates.length === 0) return;\n\n const pending = pendingBatchRef.current;\n\n for (const update of filteredUpdates) {\n const key = getPendingKey(update.rowId, update.columnId);\n const existing = pending.byKey.get(key);\n\n if (existing) {\n pending.byKey.set(key, { ...existing, newValue: update.newValue });\n } else {\n pending.byKey.set(key, update);\n }\n }\n\n store.setPendingChanges(true);\n\n if (pending.timeoutId) {\n clearTimeout(pending.timeoutId);\n }\n pending.timeoutId = setTimeout(onCommit, BATCH_TIMEOUT);\n },\n [store, propsRef, onCommit],\n );\n\n const trackRowsAdd = React.useCallback(\n (rows: TData[]) => {\n if (!propsRef.current.enabled || rows.length === 0) return;\n\n onCommit();\n\n const { getRowId } = propsRef.current;\n\n const rowIds = new Set(rows.map((row) => getRowId(row)));\n const rowsCopy = rows.map((row) => ({ ...row }));\n\n const entry: HistoryEntry = {\n variant: \"rows_add\",\n count: rows.length,\n timestamp: Date.now(),\n undo: (currentData) => {\n return currentData.filter((row) => !rowIds.has(getRowId(row)));\n },\n redo: (currentData) => {\n return [...currentData, ...rowsCopy.map((row) => ({ ...row }))];\n },\n };\n\n store.push(entry);\n },\n [store, propsRef, onCommit],\n );\n\n const trackRowsDelete = React.useCallback(\n (rows: TData[]) => {\n if (!propsRef.current.enabled || rows.length === 0) return;\n\n onCommit();\n\n const { getRowId, data: currentData } = propsRef.current;\n\n const indexById = buildIndexById(currentData, getRowId);\n\n const rowsWithPositions: Array<{ index: number; row: TData }> = [];\n for (const row of rows) {\n const rowId = getRowId(row);\n const currentIndex = indexById.get(rowId);\n if (currentIndex !== undefined) {\n rowsWithPositions.push({\n index: currentIndex,\n row: { ...row },\n });\n }\n }\n\n rowsWithPositions.sort((a, b) => a.index - b.index);\n\n const rowIds = new Set(rows.map((row) => getRowId(row)));\n\n const entry: HistoryEntry = {\n variant: \"rows_delete\",\n count: rows.length,\n timestamp: Date.now(),\n undo: (currentData) => {\n const newData = [...currentData];\n for (const { index, row } of rowsWithPositions) {\n const insertIndex = Math.min(index, newData.length);\n newData.splice(insertIndex, 0, { ...row });\n }\n return newData;\n },\n redo: (currentData) => {\n return currentData.filter((row) => !rowIds.has(getRowId(row)));\n },\n };\n\n store.push(entry);\n },\n [store, propsRef, onCommit],\n );\n\n React.useEffect(() => {\n const pending = pendingBatchRef.current;\n return () => {\n if (pending.timeoutId) {\n clearTimeout(pending.timeoutId);\n }\n };\n }, []);\n\n React.useEffect(() => {\n if (!enabled) return;\n\n function onKeyDown(event: KeyboardEvent) {\n const isCtrlOrCmd = event.ctrlKey || event.metaKey;\n const key = event.key.toLowerCase();\n\n if (!isCtrlOrCmd || (key !== \"z\" && key !== \"y\")) return;\n\n const activeElement = document.activeElement;\n if (activeElement) {\n const isInput =\n activeElement.tagName === \"INPUT\" ||\n activeElement.tagName === \"TEXTAREA\";\n const isContentEditable =\n activeElement.getAttribute(\"contenteditable\") === \"true\";\n const isInPopover = getIsInPopover(activeElement);\n\n if (isInput || isContentEditable || isInPopover) return;\n }\n\n if (key === \"z\" && !event.shiftKey) {\n event.preventDefault();\n onUndo();\n return;\n }\n\n if ((key === \"z\" && event.shiftKey) || key === \"y\") {\n event.preventDefault();\n onRedo();\n }\n }\n\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [enabled, onUndo, onRedo]);\n\n return React.useMemo(\n () => ({\n canUndo,\n canRedo,\n onUndo,\n onRedo,\n onClear,\n trackCellsUpdate,\n trackRowsAdd,\n trackRowsDelete,\n }),\n [\n canUndo,\n canRedo,\n onUndo,\n onRedo,\n onClear,\n trackCellsUpdate,\n trackRowsAdd,\n trackRowsDelete,\n ],\n );\n}\n\nexport {\n //\n type UndoRedoCellUpdate,\n useDataGridUndoRedo,\n};\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-as-ref.ts", - "content": "import * as React from \"react\";\n\nimport { useIsomorphicLayoutEffect } from \"@/hooks/use-isomorphic-layout-effect\";\n\nfunction useAsRef(props: T) {\n const ref = React.useRef(props);\n\n useIsomorphicLayoutEffect(() => {\n ref.current = props;\n });\n\n return ref;\n}\n\nexport { useAsRef };\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-lazy-ref.ts", - "content": "import * as React from \"react\";\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\nexport { useLazyRef };\n", - "type": "registry:hook" - }, - { - "path": "src/hooks/use-isomorphic-layout-effect.ts", - "content": "import * as React from \"react\";\n\nconst useIsomorphicLayoutEffect =\n typeof window !== \"undefined\" ? React.useLayoutEffect : React.useEffect;\n\nexport { useIsomorphicLayoutEffect };\n", - "type": "registry:hook" - }, - { - "path": "src/lib/data-grid-utils.ts", - "content": "import type { Column, RowData, Table } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\n\nimport {\n BaselineIcon,\n CalendarIcon,\n CheckSquareIcon,\n File,\n FileArchive,\n FileAudio,\n FileIcon,\n FileImage,\n FileSpreadsheet,\n FileText,\n FileVideo,\n HashIcon,\n LinkIcon,\n ListChecksIcon,\n ListIcon,\n Presentation,\n TextInitialIcon,\n} from \"lucide-react\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\nimport type {\n CellOpts,\n CellPosition,\n Direction,\n FileCellData,\n RowHeightValue,\n} from \"@/lib/data-grid-types\";\n\nexport function stringifyUnknown(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n if (value instanceof Date) return value.toISOString();\n try {\n return JSON.stringify(value) ?? \"\";\n } catch {\n return \"\";\n }\n}\n\nexport function flexRender(\n Comp: ((props: TProps) => React.ReactNode) | string | undefined,\n props: TProps,\n): React.ReactNode {\n if (typeof Comp === \"string\") {\n return Comp;\n }\n return Comp?.(props);\n}\n\nexport function getIsFileCellData(item: unknown): item is FileCellData {\n return (\n !!item &&\n typeof item === \"object\" &&\n \"id\" in item &&\n \"name\" in item &&\n \"size\" in item &&\n \"type\" in item\n );\n}\n\nexport function matchSelectOption(\n value: string,\n options: { value: string; label: string }[],\n): string | undefined {\n return options.find(\n (o) =>\n o.value === value ||\n o.value.toLowerCase() === value.toLowerCase() ||\n o.label.toLowerCase() === value.toLowerCase(),\n )?.value;\n}\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\nexport function getColumnBorderVisibility(params: {\n column: Column;\n nextColumn?: Column;\n isLastColumn: boolean;\n}): {\n showEndBorder: boolean;\n showStartBorder: boolean;\n} {\n const { column, nextColumn, isLastColumn } = params;\n\n const isPinned = column.getIsPinned();\n const isFirstRightPinnedColumn =\n isPinned === \"end\" && column.getIsFirstColumn(\"end\");\n const isLastRightPinnedColumn =\n isPinned === \"end\" && column.getIsLastColumn(\"end\");\n\n const nextIsPinned = nextColumn?.getIsPinned();\n const isBeforeRightPinned =\n nextIsPinned === \"end\" && nextColumn?.getIsFirstColumn(\"end\");\n\n const showEndBorder =\n !isBeforeRightPinned && (isLastColumn || !isLastRightPinnedColumn);\n\n const showStartBorder = isFirstRightPinnedColumn;\n\n return {\n showEndBorder,\n showStartBorder,\n };\n}\n\nexport function getColumnPinningStyle(params: {\n column: Column;\n withBorder?: boolean;\n dir?: Direction;\n}): React.CSSProperties {\n const { column, dir = \"ltr\", withBorder = false } = params;\n\n const isPinned = column.getIsPinned();\n const isLastLeftPinnedColumn =\n isPinned === \"start\" && column.getIsLastColumn(\"start\");\n const isFirstRightPinnedColumn =\n isPinned === \"end\" && column.getIsFirstColumn(\"end\");\n\n const isRtl = dir === \"rtl\";\n\n const leftPosition =\n isPinned === \"start\" ? `${column.getStart(\"start\")}px` : undefined;\n const rightPosition =\n isPinned === \"end\" ? `${column.getAfter(\"end\")}px` : undefined;\n\n return {\n boxShadow: withBorder\n ? isLastLeftPinnedColumn\n ? isRtl\n ? \"4px 0 4px -4px var(--border) inset\"\n : \"-4px 0 4px -4px var(--border) inset\"\n : isFirstRightPinnedColumn\n ? isRtl\n ? \"-4px 0 4px -4px var(--border) inset\"\n : \"4px 0 4px -4px var(--border) inset\"\n : undefined\n : undefined,\n left: isRtl ? rightPosition : leftPosition,\n right: isRtl ? leftPosition : rightPosition,\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 getScrollDirection(\n direction: string,\n): \"left\" | \"right\" | \"home\" | \"end\" | undefined {\n if (\n direction === \"left\" ||\n direction === \"right\" ||\n direction === \"home\" ||\n direction === \"end\"\n ) {\n return direction as \"left\" | \"right\" | \"home\" | \"end\";\n }\n if (direction === \"pageleft\") return \"left\";\n if (direction === \"pageright\") return \"right\";\n return undefined;\n}\n\nexport function scrollCellIntoView(params: {\n container: HTMLDivElement;\n targetCell: HTMLDivElement;\n tableRef: React.RefObject | null>;\n viewportOffset: number;\n direction?: \"left\" | \"right\" | \"home\" | \"end\";\n isRtl: boolean;\n}): void {\n const { container, targetCell, tableRef, direction, viewportOffset, isRtl } =\n params;\n\n const containerRect = container.getBoundingClientRect();\n const cellRect = targetCell.getBoundingClientRect();\n\n const hasNegativeScroll = container.scrollLeft < 0;\n const isActuallyRtl = isRtl || hasNegativeScroll;\n\n const currentTable = tableRef.current;\n const leftPinnedColumns = currentTable?.getStartVisibleLeafColumns() ?? [];\n const rightPinnedColumns = currentTable?.getEndVisibleLeafColumns() ?? [];\n\n const leftPinnedWidth = leftPinnedColumns.reduce(\n (sum, c) => sum + c.getSize(),\n 0,\n );\n const rightPinnedWidth = rightPinnedColumns.reduce(\n (sum, c) => sum + c.getSize(),\n 0,\n );\n\n const viewportLeft = isActuallyRtl\n ? containerRect.left + rightPinnedWidth + viewportOffset\n : containerRect.left + leftPinnedWidth + viewportOffset;\n const viewportRight = isActuallyRtl\n ? containerRect.right - leftPinnedWidth - viewportOffset\n : containerRect.right - rightPinnedWidth - viewportOffset;\n\n const isFullyVisible =\n cellRect.left >= viewportLeft && cellRect.right <= viewportRight;\n\n if (isFullyVisible) return;\n\n const isClippedLeft = cellRect.left < viewportLeft;\n const isClippedRight = cellRect.right > viewportRight;\n\n let scrollDelta = 0;\n\n if (!direction) {\n if (isClippedRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else if (isClippedLeft) {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n } else {\n const shouldScrollRight = isActuallyRtl\n ? direction === \"right\" || direction === \"home\"\n : direction === \"right\" || direction === \"end\";\n\n if (shouldScrollRight) {\n scrollDelta = cellRect.right - viewportRight;\n } else {\n scrollDelta = -(viewportLeft - cellRect.left);\n }\n }\n\n container.scrollLeft += scrollDelta;\n}\n\nfunction countTabs(s: string): number {\n let n = 0;\n for (let i = 0; i < s.length; i++) if (s[i] === \"\\t\") n++;\n return n;\n}\n\nexport function parseTsv(\n text: string,\n fallbackColumnCount: number,\n): string[][] {\n if (text.startsWith('\"') || text.includes('\\t\"')) {\n const rows: string[][] = [];\n let currentRow: string[] = [];\n let currentField = \"\";\n let inQuotes = false;\n let i = 0;\n\n while (i < text.length) {\n const char = text[i];\n const nextChar = text[i + 1];\n\n if (inQuotes) {\n if (char === '\"' && nextChar === '\"') {\n currentField += '\"';\n i += 2;\n } else if (char === '\"') {\n inQuotes = false;\n i++;\n } else {\n currentField += char;\n i++;\n }\n } else {\n if (char === '\"' && currentField === \"\") {\n inQuotes = true;\n i++;\n } else if (char === \"\\t\") {\n currentRow.push(currentField);\n currentField = \"\";\n i++;\n } else if (char === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i++;\n } else if (char === \"\\r\" && nextChar === \"\\n\") {\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n currentRow = [];\n currentField = \"\";\n i += 2;\n } else {\n currentField += char;\n i++;\n }\n }\n }\n\n currentRow.push(currentField);\n if (currentRow.length > 1 || currentRow.some((f) => f.length > 0)) {\n rows.push(currentRow);\n }\n\n return rows;\n }\n\n const lines = text.split(\"\\n\").map((l) => l.replace(/\\r$/, \"\"));\n let maxTabCount = 0;\n for (const line of lines) {\n const n = countTabs(line);\n if (n > maxTabCount) maxTabCount = n;\n }\n const columnCount = maxTabCount > 0 ? maxTabCount + 1 : fallbackColumnCount;\n if (columnCount <= 0) return [];\n\n const expectedTabCount = columnCount - 1;\n const rows: string[][] = [];\n let buf = \"\";\n let bufTabCount = 0;\n\n for (const line of lines) {\n const tc = countTabs(line);\n\n if (tc === expectedTabCount) {\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n rows.push(line.split(\"\\t\"));\n } else {\n buf = buf ? `${buf}\\n${line}` : line;\n bufTabCount += tc;\n if (bufTabCount === expectedTabCount) {\n rows.push(buf.split(\"\\t\"));\n buf = \"\";\n bufTabCount = 0;\n }\n }\n }\n\n if (buf && bufTabCount === expectedTabCount) rows.push(buf.split(\"\\t\"));\n\n return rows.length > 0\n ? rows\n : lines.filter((l) => l.length > 0).map((l) => l.split(\"\\t\"));\n}\n\nexport function getIsInPopover(element: unknown): boolean {\n if (!(element instanceof Element)) return false;\n\n return (\n element.closest(\"[data-grid-cell-editor]\") !== null ||\n element.closest(\"[data-grid-popover]\") !== null ||\n element.closest(\"[data-slot='dropdown-menu-content']\") !== null ||\n element.closest(\"[data-slot='popover-content']\") !== null\n );\n}\n\nexport function getColumnVariant(variant?: CellOpts[\"variant\"]): {\n icon: React.ComponentType>;\n label: string;\n} | null {\n switch (variant) {\n case \"short-text\":\n return { label: \"Short text\", icon: BaselineIcon };\n case \"long-text\":\n return { label: \"Long text\", icon: TextInitialIcon };\n case \"number\":\n return { label: \"Number\", icon: HashIcon };\n case \"url\":\n return { label: \"URL\", icon: LinkIcon };\n case \"checkbox\":\n return { label: \"Checkbox\", icon: CheckSquareIcon };\n case \"select\":\n return { label: \"Select\", icon: ListIcon };\n case \"multi-select\":\n return { label: \"Multi-select\", icon: ListChecksIcon };\n case \"date\":\n return { label: \"Date\", icon: CalendarIcon };\n case \"file\":\n return { label: \"File\", icon: FileIcon };\n default:\n return null;\n }\n}\n\nexport function getEmptyCellValue(\n variant: CellOpts[\"variant\"] | undefined,\n): unknown {\n if (variant === \"multi-select\" || variant === \"file\") return [];\n if (variant === \"number\" || variant === \"date\" || variant === \"select\")\n return null;\n if (variant === \"checkbox\") return false;\n return \"\";\n}\n\nexport function getUrlHref(urlString: string): string {\n if (!urlString || urlString.trim() === \"\") return \"\";\n\n const trimmed = urlString.trim();\n\n // Reject dangerous protocols (extra safety, though our http:// prefix would neutralize them)\n if (/^(javascript|data|vbscript|file):/i.test(trimmed)) {\n return \"\";\n }\n\n if (trimmed.startsWith(\"http://\") || trimmed.startsWith(\"https://\")) {\n return trimmed;\n }\n\n return `http://${trimmed}`;\n}\n\nexport function parseLocalDate(dateStr: unknown): Date | null {\n if (!dateStr) return null;\n if (dateStr instanceof Date) return dateStr;\n if (typeof dateStr !== \"string\") return null;\n const [year, month, day] = dateStr.split(\"-\").map(Number);\n if (!year || !month || !day) return null;\n const date = new Date(year, month - 1, day);\n // Verify date wasn't auto-corrected (e.g. Feb 30 -> Mar 1)\n if (\n date.getFullYear() !== year ||\n date.getMonth() !== month - 1 ||\n date.getDate() !== day\n ) {\n return null;\n }\n return date;\n}\n\nexport function formatDateToString(date: Date): string {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const day = String(date.getDate()).padStart(2, \"0\");\n return `${year}-${month}-${day}`;\n}\n\nexport function formatDateForDisplay(dateStr: unknown): string {\n if (!dateStr) return \"\";\n const date = parseLocalDate(dateStr);\n if (!date) return typeof dateStr === \"string\" ? dateStr : \"\";\n return date.toLocaleDateString();\n}\n\nexport function formatFileSize(bytes: number): string {\n if (bytes <= 0 || !Number.isFinite(bytes)) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n const i = Math.min(\n sizes.length - 1,\n Math.floor(Math.log(bytes) / Math.log(k)),\n );\n return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;\n}\n\nexport function getFileIcon(\n type: string,\n): React.ComponentType> {\n if (type.startsWith(\"image/\")) return FileImage;\n if (type.startsWith(\"video/\")) return FileVideo;\n if (type.startsWith(\"audio/\")) return FileAudio;\n if (type.includes(\"pdf\")) return FileText;\n if (type.includes(\"zip\") || type.includes(\"rar\")) return FileArchive;\n if (\n type.includes(\"word\") ||\n type.includes(\"document\") ||\n type.includes(\"doc\")\n )\n return FileText;\n if (type.includes(\"sheet\") || type.includes(\"excel\") || type.includes(\"xls\"))\n return FileSpreadsheet;\n if (\n type.includes(\"presentation\") ||\n type.includes(\"powerpoint\") ||\n type.includes(\"ppt\")\n )\n return Presentation;\n return File;\n}\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-features.ts", - "content": "import {\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n createFilteredRowModel,\n createSortedRowModel,\n metaHelper,\n rowSelectionFeature,\n rowSortingFeature,\n tableFeatures,\n} from \"@tanstack/react-table\";\n\nimport type {\n DataGridColumnMeta,\n DataGridTableMeta,\n} from \"@/lib/data-grid-types\";\n\nexport const dataGridFeatures = tableFeatures({\n columnFilteringFeature,\n columnOrderingFeature,\n columnPinningFeature,\n columnResizingFeature,\n columnSizingFeature,\n columnVisibilityFeature,\n rowSelectionFeature,\n rowSortingFeature,\n filteredRowModel: createFilteredRowModel(),\n sortedRowModel: createSortedRowModel(),\n tableMeta: metaHelper(),\n columnMeta: metaHelper(),\n});\n\nexport type DataGridFeatures = typeof dataGridFeatures;\n", - "type": "registry:lib" - }, - { - "path": "src/lib/data-grid-types.ts", - "content": "import type { Cell, RowData } from \"@tanstack/react-table\";\n\nimport type { DataGridFeatures } from \"@/lib/data-grid-features\";\n\nexport type Direction = \"ltr\" | \"rtl\";\n\nexport type RowHeightValue = \"short\" | \"medium\" | \"tall\" | \"extra-tall\";\n\nexport interface CellSelectOption {\n label: string;\n value: string;\n icon?: React.ComponentType>;\n count?: number;\n}\n\nexport type CellOpts =\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 | {\n variant: \"url\";\n }\n | {\n variant: \"file\";\n maxFileSize?: number;\n maxFiles?: number;\n accept?: string;\n multiple?: boolean;\n };\n\nexport interface CellUpdate {\n rowIndex: number;\n columnId: string;\n value: unknown;\n}\n\nexport interface DataGridColumnMeta {\n label?: string;\n cell?: CellOpts;\n}\n\nexport interface DataGridTableMeta {\n dataGridRef?: React.RefObject;\n cellMapRef?: React.RefObject>;\n focusedCell?: CellPosition | null;\n editingCell?: CellPosition | null;\n selectionState?: SelectionState;\n searchOpen?: boolean;\n getIsCellSelected?: (rowIndex: number, columnId: string) => boolean;\n getIsSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getIsActiveSearchMatch?: (rowIndex: number, columnId: string) => boolean;\n getVisualRowIndex?: (rowId: string) => number | undefined;\n scrollToCell?: (\n rowIndex: number,\n columnId: string,\n align?: \"auto\" | \"start\" | \"center\" | \"end\",\n ) => void;\n rowHeight?: RowHeightValue;\n onRowHeightChange?: (value: RowHeightValue) => void;\n onRowSelect?: (rowId: string, checked: boolean, shiftKey: boolean) => void;\n onDataUpdate?: (params: CellUpdate | Array) => void;\n onRowsDelete?: (rowIndices: number[]) => void | Promise;\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?: (rowIndex: number, columnId: string) => 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 onCellsCopy?: () => void;\n onCellsCut?: () => void;\n onCellsPaste?: (expand?: boolean) => void;\n onSelectionClear?: () => void;\n onFilesUpload?: (params: {\n files: File[];\n rowIndex: number;\n columnId: string;\n }) => Promise;\n onFilesDelete?: (params: {\n fileIds: string[];\n rowIndex: number;\n columnId: string;\n }) => void | Promise;\n contextMenu?: ContextMenuState;\n onContextMenuOpenChange?: (open: boolean) => void;\n pasteDialog?: PasteDialogState;\n onPasteDialogOpenChange?: (open: boolean) => void;\n readOnly?: boolean;\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 interface PasteDialogState {\n open: boolean;\n rowsNeeded: number;\n clipboardText: string;\n}\n\nexport type NavigationDirection =\n | \"up\"\n | \"down\"\n | \"left\"\n | \"right\"\n | \"home\"\n | \"end\"\n | \"ctrl+up\"\n | \"ctrl+down\"\n | \"ctrl+home\"\n | \"ctrl+end\"\n | \"pageup\"\n | \"pagedown\"\n | \"pageleft\"\n | \"pageright\";\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\nexport interface DataGridCellProps {\n cell: Cell;\n tableMeta: DataGridTableMeta;\n rowIndex: number;\n columnId: string;\n rowHeight: RowHeightValue;\n isEditing: boolean;\n isFocused: boolean;\n isSelected: boolean;\n isSearchMatch: boolean;\n isActiveSearchMatch: boolean;\n readOnly: boolean;\n}\n\nexport interface FileCellData {\n id: string;\n name: string;\n size: number;\n type: string;\n url?: string;\n}\n\nexport type TextFilterOperator =\n | \"contains\"\n | \"notContains\"\n | \"equals\"\n | \"notEquals\"\n | \"startsWith\"\n | \"endsWith\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type NumberFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"lessThan\"\n | \"lessThanOrEqual\"\n | \"greaterThan\"\n | \"greaterThanOrEqual\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type DateFilterOperator =\n | \"equals\"\n | \"notEquals\"\n | \"before\"\n | \"after\"\n | \"onOrBefore\"\n | \"onOrAfter\"\n | \"isBetween\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type SelectFilterOperator =\n | \"is\"\n | \"isNot\"\n | \"isAnyOf\"\n | \"isNoneOf\"\n | \"isEmpty\"\n | \"isNotEmpty\";\n\nexport type BooleanFilterOperator = \"isTrue\" | \"isFalse\";\n\nexport type FilterOperator =\n | TextFilterOperator\n | NumberFilterOperator\n | DateFilterOperator\n | SelectFilterOperator\n | BooleanFilterOperator;\n\nexport interface FilterValue {\n operator: FilterOperator;\n value?: string | number | string[];\n endValue?: string | number;\n}\n", - "type": "registry:lib" - } - ], - "type": "registry:hook" -} \ No newline at end of file diff --git a/registry.json b/registry.json index 771757534..7434bdc5a 100644 --- a/registry.json +++ b/registry.json @@ -26,54 +26,50 @@ "dependencies": ["@tanstack/react-table", "lucide-react", "nuqs"], "files": [ { - "path": "src/components/data-table/data-table.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table.tsx", "type": "registry:component", "target": "src/components/data-table/data-table.tsx" }, { - "path": "src/components/data-table/data-table-column-header.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-column-header.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-column-header.tsx" }, { - "path": "src/components/data-table/data-table-pagination.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-pagination.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-pagination.tsx" }, { - "path": "src/components/data-table/data-table-view-options.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-view-options.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-view-options.tsx" }, { - "path": "src/components/data-table/data-table-faceted-filter.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-faceted-filter.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-faceted-filter.tsx" }, { - "path": "src/components/data-table/data-table-toolbar.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-toolbar.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-toolbar.tsx" }, { - "path": "src/components/data-table/data-table-slider-filter.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-slider-filter.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-slider-filter.tsx" }, { - "path": "src/components/data-table/data-table-date-filter.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-date-filter.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-date-filter.tsx" }, { - "path": "src/components/data-table/data-table-skeleton.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-skeleton.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-skeleton.tsx" }, - { - "path": "src/hooks/use-callback-ref.ts", - "type": "registry:hook" - }, { "path": "src/hooks/use-data-table.ts", "type": "registry:hook" @@ -119,19 +115,15 @@ "dependencies": ["@tanstack/react-table", "lucide-react"], "files": [ { - "path": "src/components/data-table/data-table-sort-list.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-sort-list.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-sort-list.tsx" }, { - "path": "src/components/ui/sortable.tsx", + "path": "src/registry/bases/radix/ui/sortable.tsx", "type": "registry:ui", "target": "src/components/ui/sortable.tsx" }, - { - "path": "src/lib/compose-refs.ts", - "type": "registry:lib" - }, { "path": "src/lib/data-table-utils.ts", "type": "registry:lib" @@ -147,6 +139,10 @@ { "path": "src/lib/parsers.ts", "type": "registry:lib" + }, + { + "path": "src/hooks/use-isomorphic-layout-effect.ts", + "type": "registry:hook" } ] }, @@ -167,42 +163,34 @@ "dependencies": ["@tanstack/react-table", "lucide-react", "nuqs"], "files": [ { - "path": "src/components/data-table/data-table-filter-list.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-filter-list.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-filter-list.tsx" }, { - "path": "src/components/data-table/data-table-range-filter.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-range-filter.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-range-filter.tsx" }, { - "path": "src/components/data-table/data-table-advanced-toolbar.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-advanced-toolbar.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-advanced-toolbar.tsx" }, { - "path": "src/components/ui/sortable.tsx", + "path": "src/registry/bases/radix/ui/sortable.tsx", "type": "registry:ui", "target": "src/components/ui/sortable.tsx" }, { - "path": "src/components/ui/faceted.tsx", + "path": "src/registry/bases/radix/ui/faceted.tsx", "type": "registry:ui", "target": "src/components/ui/faceted.tsx" }, - { - "path": "src/hooks/use-callback-ref.ts", - "type": "registry:hook" - }, { "path": "src/hooks/use-debounced-callback.ts", "type": "registry:hook" }, - { - "path": "src/lib/compose-refs.ts", - "type": "registry:lib" - }, { "path": "src/lib/data-table-utils.ts", "type": "registry:lib" @@ -228,9 +216,13 @@ "type": "registry:lib" }, { - "path": "src/components/data-table/data-table-view-options.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-view-options.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-view-options.tsx" + }, + { + "path": "src/hooks/use-isomorphic-layout-effect.ts", + "type": "registry:hook" } ] }, @@ -251,24 +243,20 @@ "dependencies": ["@tanstack/react-table", "lucide-react", "nuqs"], "files": [ { - "path": "src/components/data-table/data-table-filter-menu.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-filter-menu.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-filter-menu.tsx" }, { - "path": "src/components/data-table/data-table-range-filter.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-range-filter.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-range-filter.tsx" }, { - "path": "src/components/data-table/data-table-advanced-toolbar.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-advanced-toolbar.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-advanced-toolbar.tsx" }, - { - "path": "src/hooks/use-callback-ref.ts", - "type": "registry:hook" - }, { "path": "src/hooks/use-debounced-callback.ts", "type": "registry:hook" @@ -298,7 +286,7 @@ "type": "registry:lib" }, { - "path": "src/components/data-table/data-table-view-options.tsx", + "path": "src/registry/bases/radix/components/data-table/data-table-view-options.tsx", "type": "registry:component", "target": "src/components/data-table/data-table-view-options.tsx" } @@ -333,57 +321,57 @@ ], "files": [ { - "path": "src/components/data-grid/data-grid.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid.tsx" }, { - "path": "src/components/data-grid/data-grid-presence.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-presence.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-presence.tsx" }, { - "path": "src/components/data-grid/data-grid-cell.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-cell.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-cell.tsx" }, { - "path": "src/components/data-grid/data-grid-cell-wrapper.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-cell-wrapper.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-cell-wrapper.tsx" }, { - "path": "src/components/data-grid/data-grid-cell-variants.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-cell-variants.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-cell-variants.tsx" }, { - "path": "src/components/data-grid/data-grid-column-header.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-column-header.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-column-header.tsx" }, { - "path": "src/components/data-grid/data-grid-context-menu.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-context-menu.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-context-menu.tsx" }, { - "path": "src/components/data-grid/data-grid-paste-dialog.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-paste-dialog.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-paste-dialog.tsx" }, { - "path": "src/components/data-grid/data-grid-row.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-row.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-row.tsx" }, { - "path": "src/components/data-grid/data-grid-search.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-search.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-search.tsx" }, { - "path": "src/components/ui/direction.tsx", + "path": "src/registry/bases/radix/ui/direction.tsx", "type": "registry:ui", "target": "src/components/ui/direction.tsx" }, @@ -395,10 +383,6 @@ "path": "src/hooks/use-badge-overflow.ts", "type": "registry:hook" }, - { - "path": "src/hooks/use-callback-ref.ts", - "type": "registry:hook" - }, { "path": "src/hooks/use-data-grid.ts", "type": "registry:hook" @@ -415,10 +399,6 @@ "path": "src/hooks/use-lazy-ref.ts", "type": "registry:hook" }, - { - "path": "src/lib/compose-refs.ts", - "type": "registry:lib" - }, { "path": "src/lib/data-grid-utils.ts", "type": "registry:lib" @@ -442,7 +422,7 @@ "dependencies": ["@tanstack/react-table"], "files": [ { - "path": "src/components/data-grid/data-grid-select-column.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-select-column.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-select-column.tsx" }, @@ -471,24 +451,20 @@ "dependencies": ["@tanstack/react-table", "lucide-react"], "files": [ { - "path": "src/components/data-grid/data-grid-sort-menu.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-sort-menu.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-sort-menu.tsx" }, { - "path": "src/components/ui/direction.tsx", + "path": "src/registry/bases/radix/ui/direction.tsx", "type": "registry:ui", "target": "src/components/ui/direction.tsx" }, { - "path": "src/components/ui/sortable.tsx", + "path": "src/registry/bases/radix/ui/sortable.tsx", "type": "registry:ui", "target": "src/components/ui/sortable.tsx" }, - { - "path": "src/lib/compose-refs.ts", - "type": "registry:lib" - }, { "path": "src/lib/data-grid-features.ts", "type": "registry:lib" @@ -496,6 +472,10 @@ { "path": "src/lib/data-grid-types.ts", "type": "registry:lib" + }, + { + "path": "src/hooks/use-isomorphic-layout-effect.ts", + "type": "registry:hook" } ] }, @@ -508,7 +488,7 @@ "dependencies": ["@tanstack/react-table", "lucide-react"], "files": [ { - "path": "src/components/data-grid/data-grid-row-height-menu.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-row-height-menu.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-row-height-menu.tsx" }, @@ -531,12 +511,12 @@ "dependencies": ["@tanstack/react-table", "lucide-react"], "files": [ { - "path": "src/components/data-grid/data-grid-view-menu.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-view-menu.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-view-menu.tsx" }, { - "path": "src/components/ui/direction.tsx", + "path": "src/registry/bases/radix/ui/direction.tsx", "type": "registry:ui", "target": "src/components/ui/direction.tsx" }, @@ -559,12 +539,12 @@ "dependencies": ["lucide-react"], "files": [ { - "path": "src/components/data-grid/data-grid-keyboard-shortcuts.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-keyboard-shortcuts.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-keyboard-shortcuts.tsx" }, { - "path": "src/components/ui/direction.tsx", + "path": "src/registry/bases/radix/ui/direction.tsx", "type": "registry:ui", "target": "src/components/ui/direction.tsx" } @@ -587,17 +567,17 @@ "dependencies": ["@tanstack/react-table", "lucide-react"], "files": [ { - "path": "src/components/data-grid/data-grid-filter-menu.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-filter-menu.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-filter-menu.tsx" }, { - "path": "src/components/ui/direction.tsx", + "path": "src/registry/bases/radix/ui/direction.tsx", "type": "registry:ui", "target": "src/components/ui/direction.tsx" }, { - "path": "src/components/ui/sortable.tsx", + "path": "src/registry/bases/radix/ui/sortable.tsx", "type": "registry:ui", "target": "src/components/ui/sortable.tsx" }, @@ -605,14 +585,9 @@ "path": "src/hooks/use-debounced-callback.ts", "type": "registry:hook" }, - { - "path": "src/lib/compose-refs.ts", - "type": "registry:lib" - }, { "path": "src/lib/data-grid-filters.ts", - "type": "registry:lib", - "target": "src/lib/data-grid-filters.ts" + "type": "registry:lib" }, { "path": "src/lib/format.ts", @@ -627,7 +602,7 @@ "type": "registry:lib" }, { - "path": "src/hooks/use-callback-ref.ts", + "path": "src/hooks/use-isomorphic-layout-effect.ts", "type": "registry:hook" } ] @@ -641,7 +616,7 @@ "dependencies": [], "files": [ { - "path": "src/components/data-grid/data-grid-skeleton.tsx", + "path": "src/registry/bases/radix/components/data-grid/data-grid-skeleton.tsx", "type": "registry:component", "target": "src/components/data-grid/data-grid-skeleton.tsx" } diff --git a/scripts/build-registry.ts b/scripts/build-registry.ts new file mode 100644 index 000000000..6222c6d03 --- /dev/null +++ b/scripts/build-registry.ts @@ -0,0 +1,179 @@ +/** + * Builds the registry once per base, then fans each build out to every + * `{base}-{style}` id that shadcn publishes. + * + * Styles are purely CSS for our components, so the JSON for a given base is + * byte-identical across styles. The per-style copies exist so the `{style}` + * placeholder in a consumer's registry URL resolves for every official style id + * instead of 404ing. They are written to `public/r` at build time and are not + * committed — same as shadcn's `registry:build` on deploy. + * + * Base resolution: registry.json points at the radix tree, and a file is swapped + * to its base-tree counterpart when one exists. Files outside the bases (hooks, + * lib) and primitives with no base variant stay shared, so base-agnostic code + * lives in exactly one place. + */ +import { execFileSync } from "node:child_process"; +import { + cpSync, + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import { + type Base, + BASES, + DEFAULT_STYLE_ID, + getStyleIds, + STYLES, +} from "../src/registry/styles"; +import { OUT_ROOT, STYLES_ROOT, wipeRegistryOutput } from "./clean-registry"; + +const DEFAULT_BASE = DEFAULT_STYLE_ID.split("-")[0] as Base; + +const ROOT = process.cwd(); +const RADIX_TREE = "src/registry/bases/radix/"; +const BASE_TREE = "src/registry/bases/base/"; + +interface RegistryFile { + path: string; + target?: string; + type: string; +} + +interface RegistryItem { + name: string; + dependencies?: string[]; + files?: RegistryFile[]; +} + +interface Registry { + items: RegistryItem[]; +} + +/** + * Repoints a file at the base tree when an override exists there. + * `target` is deliberately untouched so consumers get the same paths either way. + */ +function resolveFile(file: RegistryFile, base: Base): RegistryFile { + if (base === "radix") return file; + + const override = file.path.replace(RADIX_TREE, BASE_TREE); + + return existsSync(path.join(ROOT, override)) + ? { ...file, path: override } + : file; +} + +/** + * Base UI is a peer of the base tree, not of registry.json, so the dependency is + * added per item based on what its resolved files actually import. + */ +function withImportedDependencies( + dependencies: string[] | undefined, + files: RegistryFile[] | undefined, +) { + const contents = + files?.map((file) => readFileSync(path.join(ROOT, file.path), "utf8")) ?? + []; + + const next = [...(dependencies ?? [])]; + + for (const pkg of ["@base-ui/react", "@base-ui/utils"] as const) { + if (contents.some((content) => content.includes(`"${pkg}`))) { + if (!next.includes(pkg)) next.push(pkg); + } + } + + return next.length > 0 ? next : dependencies; +} + +function buildBase(base: Base, outDir: string) { + const registry = JSON.parse( + readFileSync(path.join(ROOT, "registry.json"), "utf8"), + ) as Registry; + + const items = registry.items.map((item) => { + const files = item.files?.map((file) => resolveFile(file, base)); + + return { + ...item, + dependencies: withImportedDependencies(item.dependencies, files), + files, + }; + }); + + const overrides = items.flatMap( + (item) => + item.files?.filter((file) => file.path.startsWith(BASE_TREE)) ?? [], + ); + + const registryPath = path.join(outDir, "registry.input.json"); + writeFileSync(registryPath, JSON.stringify({ ...registry, items }, null, 2)); + + execFileSync( + path.join(ROOT, "node_modules/.bin/shadcn"), + ["build", registryPath, "--output", path.join(outDir, "r")], + { cwd: ROOT, stdio: "inherit" }, + ); + + return overrides.length; +} + +function main() { + wipeRegistryOutput(); + mkdirSync(STYLES_ROOT, { recursive: true }); + + const scratch = mkdtempSync(path.join(tmpdir(), "tablecn-registry-")); + + try { + for (const base of BASES) { + const baseDir = path.join(scratch, base); + mkdirSync(baseDir, { recursive: true }); + + const overrideCount = buildBase(base, baseDir); + const built = path.join(baseDir, "r"); + + for (const style of STYLES) { + cpSync(built, path.join(STYLES_ROOT, `${base}-${style}`), { + recursive: true, + }); + } + + // Flat `/r/{name}.json` keeps serving the default base so existing + // DiceUI redirects and style-less installs keep working. + if (base === DEFAULT_BASE) { + cpSync(built, OUT_ROOT, { recursive: true }); + } + + console.log( + `${base}: ${overrideCount} base-tree file(s), ${STYLES.length} style ids`, + ); + } + } finally { + rmSync(scratch, { recursive: true, force: true }); + } + + const ids = getStyleIds(); + + writeFileSync( + path.join(STYLES_ROOT, "index.json"), + `${JSON.stringify( + ids.map((name) => ({ name })), + null, + 2, + )}\n`, + ); + + console.log( + `\nPublished ${ids.length} style ids (default: ${DEFAULT_BASE}).`, + ); +} + +main(); diff --git a/scripts/clean-registry.ts b/scripts/clean-registry.ts new file mode 100644 index 000000000..ad746a8ba --- /dev/null +++ b/scripts/clean-registry.ts @@ -0,0 +1,144 @@ +/** + * Removes leftover generated registry files so a deleted item or old style + * id cannot linger under `public/r` after the next build. + * + * `pnpm build:registry` wipes the output first, then regenerates. Generated + * JSON under `public/r` is gitignored except `styles/index.json`. This script + * is the standalone / surgical version: it keeps current items and only deletes + * paths that are no longer expected. + * + * pnpm clean:registry remove stale files + * pnpm clean:registry -- --dry-run print what would be removed + * pnpm clean:registry -- --all wipe public/r entirely + */ +import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +import { getStyleIds } from "../src/registry/styles"; + +const ROOT = process.cwd(); +const OUT_ROOT = path.join(ROOT, "public/r"); +const STYLES_ROOT = path.join(OUT_ROOT, "styles"); + +interface Registry { + items: { name: string }[]; +} + +function getItemNames() { + const registry = JSON.parse( + readFileSync(path.join(ROOT, "registry.json"), "utf8"), + ) as Registry; + + return registry.items.map((item) => item.name); +} + +function getExpectedFiles() { + const items = getItemNames(); + const files = new Set([ + path.join(OUT_ROOT, "registry.json"), + path.join(STYLES_ROOT, "index.json"), + ]); + + for (const name of items) { + files.add(path.join(OUT_ROOT, `${name}.json`)); + } + + for (const styleId of getStyleIds()) { + files.add(path.join(STYLES_ROOT, styleId, "registry.json")); + for (const name of items) { + files.add(path.join(STYLES_ROOT, styleId, `${name}.json`)); + } + } + + return files; +} + +function listFiles(dir: string): string[] { + if (!existsSync(dir)) return []; + + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const fullPath = path.join(dir, entry.name); + return entry.isDirectory() ? listFiles(fullPath) : [fullPath]; + }); +} + +function listDirs(dir: string): string[] { + if (!existsSync(dir)) return []; + + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + if (!entry.isDirectory()) return []; + const fullPath = path.join(dir, entry.name); + return [fullPath, ...listDirs(fullPath)]; + }); +} + +function remove(target: string, dryRun: boolean, silent = false) { + const relative = path.relative(ROOT, target); + if (dryRun) { + console.log(`would remove ${relative}`); + return; + } + rmSync(target, { recursive: true, force: true }); + if (!silent) console.log(`removed ${relative}`); +} + +function wipeRegistryOutput(dryRun = false) { + if (!existsSync(OUT_ROOT)) return 0; + + const entries = readdirSync(OUT_ROOT); + for (const name of entries) { + remove(path.join(OUT_ROOT, name), dryRun, !dryRun); + } + + return entries.length; +} + +function cleanStaleRegistryOutput(dryRun = false) { + if (!existsSync(OUT_ROOT)) { + console.log("public/r is already empty."); + return 0; + } + + const expected = getExpectedFiles(); + const staleFiles = listFiles(OUT_ROOT).filter((file) => !expected.has(file)); + + for (const file of staleFiles) { + remove(file, dryRun); + } + + const expectedDirs = new Set([...expected].map((file) => path.dirname(file))); + const staleDirs = listDirs(OUT_ROOT) + .filter((dir) => !expectedDirs.has(dir)) + .sort((a, b) => b.length - a.length); + + for (const dir of staleDirs) { + if (!existsSync(dir)) continue; + if (readdirSync(dir).length > 0) continue; + remove(dir, dryRun); + } + + return staleFiles.length + staleDirs.length; +} + +function main() { + const args = new Set(process.argv.slice(2)); + const dryRun = args.has("--dry-run"); + const wipeAll = args.has("--all"); + + if (wipeAll) { + const removed = wipeRegistryOutput(dryRun); + if (removed === 0) console.log("public/r is already empty."); + else if (!dryRun) console.log(`Wiped ${removed} entries from public/r.`); + return; + } + + const removed = cleanStaleRegistryOutput(dryRun); + if (removed === 0) console.log("No stale registry files."); +} + +export { cleanStaleRegistryOutput, OUT_ROOT, STYLES_ROOT, wipeRegistryOutput }; + +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { + main(); +} diff --git a/src/app/components/create-task-sheet.tsx b/src/app/components/create-task-sheet.tsx index cf1e5d032..c8c215217 100644 --- a/src/app/components/create-task-sheet.tsx +++ b/src/app/components/create-task-sheet.tsx @@ -6,7 +6,7 @@ import * as React from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/registry/bases/radix/ui/button"; import { Sheet, SheetClose, @@ -16,7 +16,7 @@ import { SheetHeader, SheetTitle, SheetTrigger, -} from "@/components/ui/sheet"; +} from "@/registry/bases/radix/ui/sheet"; import type { CreateTaskSchema } from "../lib/validations"; diff --git a/src/app/components/delete-tasks-dialog.tsx b/src/app/components/delete-tasks-dialog.tsx index dc9853dbb..3c41cf63a 100644 --- a/src/app/components/delete-tasks-dialog.tsx +++ b/src/app/components/delete-tasks-dialog.tsx @@ -9,7 +9,8 @@ import { toast } from "sonner"; import type { Task } from "@/db/schema"; import type { DataTableFeatures } from "@/lib/table-features"; -import { Button } from "@/components/ui/button"; +import { useMediaQuery } from "@/hooks/use-media-query"; +import { Button } from "@/registry/bases/radix/ui/button"; import { Dialog, DialogClose, @@ -19,7 +20,7 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@/components/ui/dialog"; +} from "@/registry/bases/radix/ui/dialog"; import { Drawer, DrawerClose, @@ -29,8 +30,7 @@ import { DrawerHeader, DrawerTitle, DrawerTrigger, -} from "@/components/ui/drawer"; -import { useMediaQuery } from "@/hooks/use-media-query"; +} from "@/registry/bases/radix/ui/drawer"; import { deleteTasks } from "../lib/actions"; diff --git a/src/app/components/feature-flags-provider.tsx b/src/app/components/feature-flags-provider.tsx index fda846948..45c9d2a92 100644 --- a/src/app/components/feature-flags-provider.tsx +++ b/src/app/components/feature-flags-provider.tsx @@ -3,13 +3,16 @@ import { useQueryState } from "nuqs"; import * as React from "react"; -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { type FlagConfig, flagConfig } from "@/config/flag"; +import { + ToggleGroup, + ToggleGroupItem, +} from "@/registry/bases/radix/ui/toggle-group"; import { Tooltip, TooltipContent, TooltipTrigger, -} from "@/components/ui/tooltip"; -import { type FlagConfig, flagConfig } from "@/config/flag"; +} from "@/registry/bases/radix/ui/tooltip"; type FilterFlag = FlagConfig["featureFlags"][number]["value"]; diff --git a/src/app/components/task-form.tsx b/src/app/components/task-form.tsx index a15010000..b0d34d42a 100644 --- a/src/app/components/task-form.tsx +++ b/src/app/components/task-form.tsx @@ -3,6 +3,7 @@ import type * as React from "react"; import type { FieldPath, FieldValues, UseFormReturn } from "react-hook-form"; +import { tasks } from "@/db/schema"; import { Form, FormControl, @@ -10,8 +11,8 @@ import { FormItem, FormLabel, FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; +} from "@/registry/bases/radix/ui/form"; +import { Input } from "@/registry/bases/radix/ui/input"; import { Select, SelectContent, @@ -19,9 +20,8 @@ import { SelectItem, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { Textarea } from "@/components/ui/textarea"; -import { tasks } from "@/db/schema"; +} from "@/registry/bases/radix/ui/select"; +import { Textarea } from "@/registry/bases/radix/ui/textarea"; interface TaskFormProps extends Omit< React.ComponentPropsWithRef<"form">, diff --git a/src/app/components/tasks-table-action-bar.tsx b/src/app/components/tasks-table-action-bar.tsx index 4817eeffb..45e7abf32 100644 --- a/src/app/components/tasks-table-action-bar.tsx +++ b/src/app/components/tasks-table-action-bar.tsx @@ -8,6 +8,8 @@ import { toast } from "sonner"; import type { DataTableFeatures } from "@/lib/table-features"; +import { type Task, tasks } from "@/db/schema"; +import { exportTableToCSV } from "@/lib/export"; import { ActionBar, ActionBarClose, @@ -15,15 +17,13 @@ import { ActionBarItem, ActionBarSelection, ActionBarSeparator, -} from "@/components/ui/action-bar"; +} from "@/registry/bases/radix/ui/action-bar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { type Task, tasks } from "@/db/schema"; -import { exportTableToCSV } from "@/lib/export"; +} from "@/registry/bases/radix/ui/dropdown-menu"; import { deleteTasks, updateTasks } from "../lib/actions"; diff --git a/src/app/components/tasks-table-columns.tsx b/src/app/components/tasks-table-columns.tsx index b0bf1d745..4748f5f4b 100644 --- a/src/app/components/tasks-table-columns.tsx +++ b/src/app/components/tasks-table-columns.tsx @@ -16,10 +16,13 @@ import { toast } from "sonner"; import type { DataTableRowAction } from "@/lib/data-table-types"; import type { DataTableFeatures } from "@/lib/table-features"; -import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; +import { type Task, tasks } from "@/db/schema"; +import { getErrorMessage } from "@/lib/error"; +import { formatDate } from "@/lib/format"; +import { DataTableColumnHeader } from "@/registry/bases/radix/components/data-table/data-table-column-header"; +import { Badge } from "@/registry/bases/radix/ui/badge"; +import { Button } from "@/registry/bases/radix/ui/button"; +import { Checkbox } from "@/registry/bases/radix/ui/checkbox"; import { DropdownMenu, DropdownMenuContent, @@ -31,10 +34,7 @@ import { DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { type Task, tasks } from "@/db/schema"; -import { formatDate } from "@/lib/format"; -import { getErrorMessage } from "@/lib/handle-error"; +} from "@/registry/bases/radix/ui/dropdown-menu"; import { updateTask } from "../lib/actions"; import { getPriorityIcon, getStatusIcon } from "../lib/utils"; diff --git a/src/app/components/tasks-table-toolbar-actions.tsx b/src/app/components/tasks-table-toolbar-actions.tsx index c881ecaa9..ac06e8b4d 100644 --- a/src/app/components/tasks-table-toolbar-actions.tsx +++ b/src/app/components/tasks-table-toolbar-actions.tsx @@ -7,8 +7,8 @@ import { Download } from "lucide-react"; import type { Task } from "@/db/schema"; import type { DataTableFeatures } from "@/lib/table-features"; -import { Button } from "@/components/ui/button"; import { exportTableToCSV } from "@/lib/export"; +import { Button } from "@/registry/bases/radix/ui/button"; import { CreateTaskSheet } from "./create-task-sheet"; import { DeleteTasksDialog } from "./delete-tasks-dialog"; diff --git a/src/app/components/tasks-table.tsx b/src/app/components/tasks-table.tsx index 15331d592..bd095a49c 100644 --- a/src/app/components/tasks-table.tsx +++ b/src/app/components/tasks-table.tsx @@ -5,13 +5,13 @@ import * as React from "react"; import type { Task } from "@/db/schema"; import type { DataTableRowAction, QueryKeys } from "@/lib/data-table-types"; -import { DataTable } from "@/components/data-table/data-table"; -import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"; -import { DataTableFilterList } from "@/components/data-table/data-table-filter-list"; -import { DataTableFilterMenu } from "@/components/data-table/data-table-filter-menu"; -import { DataTableSortList } from "@/components/data-table/data-table-sort-list"; -import { DataTableToolbar } from "@/components/data-table/data-table-toolbar"; import { useDataTable } from "@/hooks/use-data-table"; +import { DataTable } from "@/registry/bases/radix/components/data-table/data-table"; +import { DataTableAdvancedToolbar } from "@/registry/bases/radix/components/data-table/data-table-advanced-toolbar"; +import { DataTableFilterList } from "@/registry/bases/radix/components/data-table/data-table-filter-list"; +import { DataTableFilterMenu } from "@/registry/bases/radix/components/data-table/data-table-filter-menu"; +import { DataTableSortList } from "@/registry/bases/radix/components/data-table/data-table-sort-list"; +import { DataTableToolbar } from "@/registry/bases/radix/components/data-table/data-table-toolbar"; import type { getEstimatedHoursRange, diff --git a/src/app/components/update-task-sheet.tsx b/src/app/components/update-task-sheet.tsx index 5d6ac3a0e..ce119d40d 100644 --- a/src/app/components/update-task-sheet.tsx +++ b/src/app/components/update-task-sheet.tsx @@ -8,7 +8,7 @@ import { toast } from "sonner"; import type { Task } from "@/db/schema"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/registry/bases/radix/ui/button"; import { Sheet, SheetClose, @@ -17,7 +17,7 @@ import { SheetFooter, SheetHeader, SheetTitle, -} from "@/components/ui/sheet"; +} from "@/registry/bases/radix/ui/sheet"; import { updateTask } from "../lib/actions"; import { type UpdateTaskSchema, updateTaskSchema } from "../lib/validations"; diff --git a/src/app/data-grid-live/components/data-grid-action-bar.tsx b/src/app/data-grid-live/components/data-grid-action-bar.tsx index 0ff118cb6..9f6de964d 100644 --- a/src/app/data-grid-live/components/data-grid-action-bar.tsx +++ b/src/app/data-grid-live/components/data-grid-action-bar.tsx @@ -18,13 +18,13 @@ import { ActionBarItem, ActionBarSelection, ActionBarSeparator, -} from "@/components/ui/action-bar"; +} from "@/registry/bases/radix/ui/action-bar"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; +} from "@/registry/bases/radix/ui/dropdown-menu"; interface DataGridActionBarProps { table: Table; diff --git a/src/app/data-grid-live/components/data-grid-live-demo.tsx b/src/app/data-grid-live/components/data-grid-live-demo.tsx index 358dc1125..c339deca9 100644 --- a/src/app/data-grid-live/components/data-grid-live-demo.tsx +++ b/src/app/data-grid-live/components/data-grid-live-demo.tsx @@ -14,13 +14,6 @@ import { getStanceIcon, getStyleIcon, } from "@/app/lib/utils"; -import { DataGrid } from "@/components/data-grid/data-grid"; -import { DataGridFilterMenu } from "@/components/data-grid/data-grid-filter-menu"; -import { DataGridKeyboardShortcuts } from "@/components/data-grid/data-grid-keyboard-shortcuts"; -import { DataGridRowHeightMenu } from "@/components/data-grid/data-grid-row-height-menu"; -import { getDataGridSelectColumn } from "@/components/data-grid/data-grid-select-column"; -import { DataGridSortMenu } from "@/components/data-grid/data-grid-sort-menu"; -import { DataGridViewMenu } from "@/components/data-grid/data-grid-view-menu"; import { skaters } from "@/db/schema"; import { type UseDataGridProps, useDataGrid } from "@/hooks/use-data-grid"; import { @@ -31,6 +24,13 @@ import { useWindowSize } from "@/hooks/use-window-size"; import { getFilterFn } from "@/lib/data-grid-filters"; import { generateId } from "@/lib/id"; import { useUploadThing } from "@/lib/uploadthing"; +import { DataGrid } from "@/registry/bases/radix/components/data-grid/data-grid"; +import { DataGridFilterMenu } from "@/registry/bases/radix/components/data-grid/data-grid-filter-menu"; +import { DataGridKeyboardShortcuts } from "@/registry/bases/radix/components/data-grid/data-grid-keyboard-shortcuts"; +import { DataGridRowHeightMenu } from "@/registry/bases/radix/components/data-grid/data-grid-row-height-menu"; +import { getDataGridSelectColumn } from "@/registry/bases/radix/components/data-grid/data-grid-select-column"; +import { DataGridSortMenu } from "@/registry/bases/radix/components/data-grid/data-grid-sort-menu"; +import { DataGridViewMenu } from "@/registry/bases/radix/components/data-grid/data-grid-view-menu"; import type { SkaterSchema } from "../lib/validation"; diff --git a/src/app/data-grid-live/page.tsx b/src/app/data-grid-live/page.tsx index 215a9380b..ae8839d86 100644 --- a/src/app/data-grid-live/page.tsx +++ b/src/app/data-grid-live/page.tsx @@ -6,7 +6,7 @@ import { DataGridSkeleton, DataGridSkeletonGrid, DataGridSkeletonToolbar, -} from "@/components/data-grid/data-grid-skeleton"; +} from "@/registry/bases/radix/components/data-grid/data-grid-skeleton"; // Dynamic import with ssr: false is required because: // 1. useLiveQuery uses useSyncExternalStore which needs getServerSnapshot for SSR diff --git a/src/app/data-grid-multiplayer/components/data-grid-multiplayer-demo.tsx b/src/app/data-grid-multiplayer/components/data-grid-multiplayer-demo.tsx index 74b84eba1..d36bab544 100644 --- a/src/app/data-grid-multiplayer/components/data-grid-multiplayer-demo.tsx +++ b/src/app/data-grid-multiplayer/components/data-grid-multiplayer-demo.tsx @@ -16,18 +16,6 @@ import { getStanceIcon, getStyleIcon, } from "@/app/lib/utils"; -import { DataGrid } from "@/components/data-grid/data-grid"; -import { DataGridFilterMenu } from "@/components/data-grid/data-grid-filter-menu"; -import { DataGridKeyboardShortcuts } from "@/components/data-grid/data-grid-keyboard-shortcuts"; -import { - type DataGridCellPresence, - DataGridPresenceProvider, -} from "@/components/data-grid/data-grid-presence"; -import { DataGridRowHeightMenu } from "@/components/data-grid/data-grid-row-height-menu"; -import { getDataGridSelectColumn } from "@/components/data-grid/data-grid-select-column"; -import { DataGridSortMenu } from "@/components/data-grid/data-grid-sort-menu"; -import { DataGridViewMenu } from "@/components/data-grid/data-grid-view-menu"; -import { Button } from "@/components/ui/button"; import { skaters } from "@/db/schema"; import { type UseDataGridProps, useDataGrid } from "@/hooks/use-data-grid"; import { @@ -39,6 +27,18 @@ import { useWindowSize } from "@/hooks/use-window-size"; import { getFilterFn } from "@/lib/data-grid-filters"; import { getCellKey } from "@/lib/data-grid-utils"; import { generateId } from "@/lib/id"; +import { DataGrid } from "@/registry/bases/radix/components/data-grid/data-grid"; +import { DataGridFilterMenu } from "@/registry/bases/radix/components/data-grid/data-grid-filter-menu"; +import { DataGridKeyboardShortcuts } from "@/registry/bases/radix/components/data-grid/data-grid-keyboard-shortcuts"; +import { + type DataGridCellPresence, + DataGridPresenceProvider, +} from "@/registry/bases/radix/components/data-grid/data-grid-presence"; +import { DataGridRowHeightMenu } from "@/registry/bases/radix/components/data-grid/data-grid-row-height-menu"; +import { getDataGridSelectColumn } from "@/registry/bases/radix/components/data-grid/data-grid-select-column"; +import { DataGridSortMenu } from "@/registry/bases/radix/components/data-grid/data-grid-sort-menu"; +import { DataGridViewMenu } from "@/registry/bases/radix/components/data-grid/data-grid-view-menu"; +import { Button } from "@/registry/bases/radix/ui/button"; import { multiplayerCollection, diff --git a/src/app/data-grid-multiplayer/components/data-grid-multiplayer-skeleton.tsx b/src/app/data-grid-multiplayer/components/data-grid-multiplayer-skeleton.tsx index 032e49b14..6b1cf6e85 100644 --- a/src/app/data-grid-multiplayer/components/data-grid-multiplayer-skeleton.tsx +++ b/src/app/data-grid-multiplayer/components/data-grid-multiplayer-skeleton.tsx @@ -2,7 +2,7 @@ import { DataGridSkeleton, DataGridSkeletonGrid, DataGridSkeletonToolbar, -} from "@/components/data-grid/data-grid-skeleton"; +} from "@/registry/bases/radix/components/data-grid/data-grid-skeleton"; export function DataGridMultiplayerSkeleton() { return ( diff --git a/src/app/data-grid-multiplayer/components/data-grid-presence-avatars.tsx b/src/app/data-grid-multiplayer/components/data-grid-presence-avatars.tsx index e53c42abd..39bb8f07d 100644 --- a/src/app/data-grid-multiplayer/components/data-grid-presence-avatars.tsx +++ b/src/app/data-grid-multiplayer/components/data-grid-presence-avatars.tsx @@ -2,12 +2,12 @@ import type { UserPresence } from "@party/types"; +import { cn } from "@/lib/utils"; import { Tooltip, TooltipContent, TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; +} from "@/registry/bases/radix/ui/tooltip"; function getInitials(name: string): string { return name diff --git a/src/app/data-grid-render/components/data-grid-render-demo.tsx b/src/app/data-grid-render/components/data-grid-render-demo.tsx index ae4e9e399..5a2c1c5b8 100644 --- a/src/app/data-grid-render/components/data-grid-render-demo.tsx +++ b/src/app/data-grid-render/components/data-grid-render-demo.tsx @@ -9,9 +9,11 @@ import * as React from "react"; import type { DataGridFeatures } from "@/lib/data-grid-features"; import type { CellUpdate } from "@/lib/data-grid-types"; -import { DataGrid } from "@/components/data-grid/data-grid"; -import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; +import { useDataGrid } from "@/hooks/use-data-grid"; +import { getFilterFn } from "@/lib/data-grid-filters"; +import { DataGrid } from "@/registry/bases/radix/components/data-grid/data-grid"; +import { Button } from "@/registry/bases/radix/ui/button"; +import { Checkbox } from "@/registry/bases/radix/ui/checkbox"; import { Select, SelectContent, @@ -20,9 +22,7 @@ import { SelectLabel, SelectTrigger, SelectValue, -} from "@/components/ui/select"; -import { useDataGrid } from "@/hooks/use-data-grid"; -import { getFilterFn } from "@/lib/data-grid-filters"; +} from "@/registry/bases/radix/ui/select"; interface Person { id: string; diff --git a/src/app/data-grid-render/page.tsx b/src/app/data-grid-render/page.tsx index 7f657a6fb..182c5d1fd 100644 --- a/src/app/data-grid-render/page.tsx +++ b/src/app/data-grid-render/page.tsx @@ -1,8 +1,8 @@ "use client"; -import { Fps } from "@/components/ui/fps"; -import { Skeleton } from "@/components/ui/skeleton"; import { useMounted } from "@/hooks/use-mounted"; +import { Fps } from "@/registry/bases/radix/ui/fps"; +import { Skeleton } from "@/registry/bases/radix/ui/skeleton"; import { DataGridRenderDemo } from "./components/data-grid-render-demo"; diff --git a/src/app/data-grid/components/data-grid-demo.tsx b/src/app/data-grid/components/data-grid-demo.tsx index 75b0ae13b..e9ef636fe 100644 --- a/src/app/data-grid/components/data-grid-demo.tsx +++ b/src/app/data-grid/components/data-grid-demo.tsx @@ -8,15 +8,6 @@ import * as React from "react"; import type { DataGridFeatures } from "@/lib/data-grid-features"; import type { Direction } from "@/lib/data-grid-types"; -import { DataGrid } from "@/components/data-grid/data-grid"; -import { DataGridFilterMenu } from "@/components/data-grid/data-grid-filter-menu"; -import { DataGridKeyboardShortcuts } from "@/components/data-grid/data-grid-keyboard-shortcuts"; -import { DataGridRowHeightMenu } from "@/components/data-grid/data-grid-row-height-menu"; -import { getDataGridSelectColumn } from "@/components/data-grid/data-grid-select-column"; -import { DataGridSortMenu } from "@/components/data-grid/data-grid-sort-menu"; -import { DataGridViewMenu } from "@/components/data-grid/data-grid-view-menu"; -import { DirectionProvider } from "@/components/ui/direction"; -import { Toggle } from "@/components/ui/toggle"; import { type UseDataGridProps, useDataGrid } from "@/hooks/use-data-grid"; import { type UndoRedoCellUpdate, @@ -25,6 +16,15 @@ import { import { useWindowSize } from "@/hooks/use-window-size"; import { getFilterFn } from "@/lib/data-grid-filters"; import { generateId } from "@/lib/id"; +import { DataGrid } from "@/registry/bases/radix/components/data-grid/data-grid"; +import { DataGridFilterMenu } from "@/registry/bases/radix/components/data-grid/data-grid-filter-menu"; +import { DataGridKeyboardShortcuts } from "@/registry/bases/radix/components/data-grid/data-grid-keyboard-shortcuts"; +import { DataGridRowHeightMenu } from "@/registry/bases/radix/components/data-grid/data-grid-row-height-menu"; +import { getDataGridSelectColumn } from "@/registry/bases/radix/components/data-grid/data-grid-select-column"; +import { DataGridSortMenu } from "@/registry/bases/radix/components/data-grid/data-grid-sort-menu"; +import { DataGridViewMenu } from "@/registry/bases/radix/components/data-grid/data-grid-view-menu"; +import { DirectionProvider } from "@/registry/bases/radix/ui/direction"; +import { Toggle } from "@/registry/bases/radix/ui/toggle"; import { departments, diff --git a/src/app/data-grid/page.tsx b/src/app/data-grid/page.tsx index a485f32dd..f7b457544 100644 --- a/src/app/data-grid/page.tsx +++ b/src/app/data-grid/page.tsx @@ -4,7 +4,7 @@ import { DataGridSkeleton, DataGridSkeletonGrid, DataGridSkeletonToolbar, -} from "@/components/data-grid/data-grid-skeleton"; +} from "@/registry/bases/radix/components/data-grid/data-grid-skeleton"; import { DataGridDemo } from "./components/data-grid-demo"; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 865ba3b2a..10de8b088 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,12 +7,12 @@ import { Suspense } from "react"; import { SiteHeader } from "@/components/layouts/site-header"; import { ThemeProvider } from "@/components/providers"; import { TailwindIndicator } from "@/components/tailwind-indicator"; -import "@/styles/globals.css"; -import { Toaster } from "@/components/ui/sonner"; import { UploadThingSSR } from "@/components/uploadthing-ssr"; import { siteConfig } from "@/config/site"; import { fontMono, fontSans } from "@/lib/fonts"; import { cn } from "@/lib/utils"; +import { Toaster } from "@/registry/bases/radix/ui/sonner"; +import "@/styles/globals.css"; const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); diff --git a/src/app/lib/actions.ts b/src/app/lib/actions.ts index 5941ad81d..a6d874392 100644 --- a/src/app/lib/actions.ts +++ b/src/app/lib/actions.ts @@ -6,7 +6,7 @@ import { updateTag } from "next/cache"; import { db } from "@/db/index"; import { type Task, tasks } from "@/db/schema"; import { takeFirstOrThrow } from "@/db/utils"; -import { getErrorMessage } from "@/lib/handle-error"; +import { getErrorMessage } from "@/lib/error"; import { generateId } from "@/lib/id"; import type { CreateTaskSchema, UpdateTaskSchema } from "./validations"; diff --git a/src/app/page.tsx b/src/app/page.tsx index f4066af7f..ab8d65d49 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,9 +2,9 @@ import { Suspense } from "react"; import type { SearchParams } from "@/types"; -import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; import { Shell } from "@/components/shell"; import { getValidFilters } from "@/lib/data-table-utils"; +import { DataTableSkeleton } from "@/registry/bases/radix/components/data-table/data-table-skeleton"; import { FeatureFlagsProvider } from "./components/feature-flags-provider"; import { TasksTable } from "./components/tasks-table"; diff --git a/src/components/active-link.tsx b/src/components/active-link.tsx index 35080650b..e34d93def 100644 --- a/src/components/active-link.tsx +++ b/src/components/active-link.tsx @@ -3,8 +3,8 @@ import Link from "next/link"; import { useSelectedLayoutSegment } from "next/navigation"; -import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; +import { Button } from "@/registry/bases/radix/ui/button"; export function ActiveLink({ href, diff --git a/src/components/layouts/mobile-nav.tsx b/src/components/layouts/mobile-nav.tsx index b96d96325..44d72bcdd 100644 --- a/src/components/layouts/mobile-nav.tsx +++ b/src/components/layouts/mobile-nav.tsx @@ -2,8 +2,8 @@ import { ActiveLink } from "@/components/active-link"; import { DocsLink } from "@/components/layouts/docs-link"; -import { SheetClose } from "@/components/ui/sheet"; import { siteConfig } from "@/config/site"; +import { SheetClose } from "@/registry/bases/radix/ui/sheet"; export function MobileNav() { return ( diff --git a/src/components/layouts/mode-toggle.tsx b/src/components/layouts/mode-toggle.tsx index 37b2fd88b..47519ce4f 100644 --- a/src/components/layouts/mode-toggle.tsx +++ b/src/components/layouts/mode-toggle.tsx @@ -3,13 +3,13 @@ import { LaptopIcon, MoonIcon, SunIcon } from "lucide-react"; import { useTheme } from "next-themes"; -import { Button } from "@/components/ui/button"; +import { Button } from "@/registry/bases/radix/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; +} from "@/registry/bases/radix/ui/dropdown-menu"; export function ModeToggle() { const { setTheme } = useTheme(); diff --git a/src/components/layouts/site-header.tsx b/src/components/layouts/site-header.tsx index d21f686d4..adab7a1ac 100644 --- a/src/components/layouts/site-header.tsx +++ b/src/components/layouts/site-header.tsx @@ -6,15 +6,15 @@ import { Icons } from "@/components/icons"; import { DocsLink } from "@/components/layouts/docs-link"; import { MobileNav } from "@/components/layouts/mobile-nav"; import { ModeToggle } from "@/components/layouts/mode-toggle"; -import { Button } from "@/components/ui/button"; +import { siteConfig } from "@/config/site"; +import { Button } from "@/registry/bases/radix/ui/button"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, -} from "@/components/ui/sheet"; -import { siteConfig } from "@/config/site"; +} from "@/registry/bases/radix/ui/sheet"; export function SiteHeader() { return ( diff --git a/src/components/providers.tsx b/src/components/providers.tsx index d8e3a1b8d..b847f6130 100644 --- a/src/components/providers.tsx +++ b/src/components/providers.tsx @@ -8,7 +8,7 @@ import { import { NuqsAdapter } from "nuqs/adapters/next/app"; import { useState } from "react"; -import { TooltipProvider } from "@/components/ui/tooltip"; +import { TooltipProvider } from "@/registry/bases/radix/ui/tooltip"; export function ThemeProvider({ children, ...props }: ThemeProviderProps) { const [queryClient] = useState( diff --git a/src/hooks/test/use-data-grid.test.tsx b/src/hooks/test/use-data-grid.test.tsx index 1c6a26a77..e9d2c9a31 100644 --- a/src/hooks/test/use-data-grid.test.tsx +++ b/src/hooks/test/use-data-grid.test.tsx @@ -18,7 +18,7 @@ vi.mock("sonner", () => ({ })); // Mock useDirection -vi.mock("@/components/ui/direction", () => ({ +vi.mock("@/registry/bases/radix/ui/direction", () => ({ useDirection: () => "ltr", })); diff --git a/src/hooks/use-callback-ref.ts b/src/hooks/use-callback-ref.ts deleted file mode 100644 index b47cbfc4d..000000000 --- a/src/hooks/use-callback-ref.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from "react"; - -/** - * @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-callback-ref/src/useCallbackRef.tsx - */ - -/** - * A custom hook that converts a callback to a ref to avoid triggering re-renders when passed as a - * prop or avoid re-executing effects when passed as a dependency - */ -function useCallbackRef unknown>( - callback: T | undefined, -): T { - const callbackRef = React.useRef(callback); - - React.useEffect(() => { - callbackRef.current = callback; - }); - - // https://github.com/facebook/react/issues/19240 - return React.useMemo( - () => ((...args) => callbackRef.current?.(...args)) as T, - [], - ); -} - -export { useCallbackRef }; diff --git a/src/hooks/use-data-grid.ts b/src/hooks/use-data-grid.ts index 3379bbadf..40e157f28 100644 --- a/src/hooks/use-data-grid.ts +++ b/src/hooks/use-data-grid.ts @@ -28,7 +28,6 @@ import type { SelectionState, } from "@/lib/data-grid-types"; -import { useDirection } from "@/components/ui/direction"; import { useAsRef } from "@/hooks/use-as-ref"; import { useIsomorphicLayoutEffect } from "@/hooks/use-isomorphic-layout-effect"; import { useLazyRef } from "@/hooks/use-lazy-ref"; @@ -49,6 +48,7 @@ import { scrollCellIntoView, stringifyUnknown, } from "@/lib/data-grid-utils"; +import { useDirection } from "@/registry/bases/radix/ui/direction"; const DEFAULT_ROW_HEIGHT = "short"; const OVERSCAN = 6; @@ -3649,4 +3649,4 @@ function useDataGrid({ ); } -export { type UseDataGridProps, useDataGrid }; +export { useDataGrid, type UseDataGridProps }; diff --git a/src/hooks/use-debounced-callback.ts b/src/hooks/use-debounced-callback.ts index cd05c2414..6280e654c 100644 --- a/src/hooks/use-debounced-callback.ts +++ b/src/hooks/use-debounced-callback.ts @@ -1,32 +1,27 @@ -/** - * @see https://github.com/mantinedev/mantine/blob/master/packages/@mantine/hooks/src/use-debounced-callback/use-debounced-callback.ts - */ - import * as React from "react"; -import { useCallbackRef } from "@/hooks/use-callback-ref"; - export function useDebouncedCallback unknown>( callback: T, delay: number, ) { - const handleCallback = useCallbackRef(callback); + const callbackRef = React.useRef(callback); + callbackRef.current = callback; + const debounceTimerRef = React.useRef(0); + React.useEffect( () => () => window.clearTimeout(debounceTimerRef.current), [], ); - const setValue = React.useCallback( + return React.useCallback( (...args: Parameters) => { window.clearTimeout(debounceTimerRef.current); debounceTimerRef.current = window.setTimeout( - () => handleCallback(...args), + () => callbackRef.current(...args), delay, ); }, - [handleCallback, delay], + [delay], ); - - return setValue; } diff --git a/src/lib/compose-refs.ts b/src/lib/compose-refs.ts deleted file mode 100644 index 19bffc16c..000000000 --- a/src/lib/compose-refs.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx - */ - -import * as React from "react"; - -type PossibleRef = React.Ref | undefined; - -/** - * Set a given ref to a given value - * This utility takes care of different types of refs: callback refs and RefObject(s) - */ -function setRef(ref: PossibleRef, value: T) { - if (typeof ref === "function") { - return ref(value); - } - - if (ref !== null && ref !== undefined) { - ref.current = value; - } -} - -/** - * A utility to compose multiple refs together - * Accepts callback refs and RefObject(s) - */ -function composeRefs(...refs: PossibleRef[]): React.RefCallback { - return (node) => { - let hasCleanup = false; - const cleanups = refs.map((ref) => { - const cleanup = setRef(ref, node); - if (!hasCleanup && typeof cleanup === "function") { - hasCleanup = true; - } - return cleanup; - }); - - // React <19 will log an error to the console if a callback ref returns a - // value. We don't use ref cleanups internally so this will only happen if a - // user's ref callback returns a value, which we only expect if they are - // using the cleanup functionality added in React 19. - if (hasCleanup) { - return () => { - for (let i = 0; i < cleanups.length; i++) { - const cleanup = cleanups[i]; - if (typeof cleanup === "function") { - cleanup(); - } else { - setRef(refs[i], null); - } - } - }; - } - }; -} - -/** - * A custom hook that composes multiple refs - * Accepts callback refs and RefObject(s) - */ -function useComposedRefs(...refs: PossibleRef[]): React.RefCallback { - return React.useCallback(composeRefs(...refs), refs); -} - -export { composeRefs, useComposedRefs }; diff --git a/src/lib/handle-error.ts b/src/lib/error.ts similarity index 100% rename from src/lib/handle-error.ts rename to src/lib/error.ts diff --git a/src/registry/bases/base/components/data-grid/data-grid-cell-variants.tsx b/src/registry/bases/base/components/data-grid/data-grid-cell-variants.tsx new file mode 100644 index 000000000..a6af92ecd --- /dev/null +++ b/src/registry/bases/base/components/data-grid/data-grid-cell-variants.tsx @@ -0,0 +1,2174 @@ +"use client"; + +import type { RowData } from "@tanstack/react-table"; + +import * as React from "react"; +import { toast } from "sonner"; + +import type { DataGridCellProps, FileCellData } from "@/lib/data-grid-types"; + +import { useBadgeOverflow } from "@/hooks/use-badge-overflow"; +import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; +import { + formatDateForDisplay, + formatDateToString, + formatFileSize, + getCellKey, + getFileIcon, + getLineCount, + getUrlHref, + parseLocalDate, +} from "@/lib/data-grid-utils"; +import { cn } from "@/lib/utils"; +import { DataGridCellWrapper } from "@/registry/bases/base/components/data-grid/data-grid-cell-wrapper"; +import { Badge } from "@/registry/bases/base/ui/badge"; +import { Button } from "@/registry/bases/base/ui/button"; +import { Checkbox } from "@/registry/bases/base/ui/checkbox"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/registry/bases/base/ui/popover"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/registry/bases/base/ui/select"; +import { Calendar } from "@/registry/bases/radix/ui/calendar"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/registry/bases/radix/ui/command"; +import { Skeleton } from "@/registry/bases/radix/ui/skeleton"; +import { Textarea } from "@/registry/bases/radix/ui/textarea"; +import { IconPlaceholder } from "@/registry/icon-placeholder"; + +export function ShortTextCell({ + cell, + tableMeta, + rowIndex, + columnId, + rowHeight, + isEditing, + isFocused, + isSelected, + isSearchMatch, + isActiveSearchMatch, + readOnly, +}: DataGridCellProps) { + const initialValue = cell.getValue() as string; + const [value, setValue] = React.useState(initialValue); + const cellRef = React.useRef(null); + const containerRef = React.useRef(null); + const prevIsEditingRef = React.useRef(false); + + const prevInitialValueRef = React.useRef(initialValue); + if (initialValue !== prevInitialValueRef.current) { + prevInitialValueRef.current = initialValue; + setValue(initialValue); + if (cellRef.current && !isEditing) { + cellRef.current.textContent = initialValue; + } + } + + const onBlur = React.useCallback(() => { + // Read the current value directly from the DOM to avoid stale state + const currentValue = cellRef.current?.textContent ?? ""; + if (!readOnly && currentValue !== initialValue) { + tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: currentValue }); + } + tableMeta?.onCellEditingStop?.(); + }, [tableMeta, rowIndex, columnId, initialValue, readOnly]); + + const onInput = React.useCallback( + (event: React.FormEvent) => { + const currentValue = event.currentTarget.textContent ?? ""; + setValue(currentValue); + }, + [], + ); + + const onWrapperKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (isEditing) { + if (event.key === "Enter") { + event.preventDefault(); + const currentValue = cellRef.current?.textContent ?? ""; + if (currentValue !== initialValue) { + tableMeta?.onDataUpdate?.({ + rowIndex, + columnId, + value: currentValue, + }); + } + tableMeta?.onCellEditingStop?.({ moveToNextRow: true }); + } else if (event.key === "Tab") { + event.preventDefault(); + const currentValue = cellRef.current?.textContent ?? ""; + if (currentValue !== initialValue) { + tableMeta?.onDataUpdate?.({ + rowIndex, + columnId, + value: currentValue, + }); + } + tableMeta?.onCellEditingStop?.({ + direction: event.shiftKey ? "left" : "right", + }); + } else if (event.key === "Escape") { + event.preventDefault(); + setValue(initialValue); + cellRef.current?.blur(); + } + } else if ( + isFocused && + event.key.length === 1 && + !event.ctrlKey && + !event.metaKey + ) { + // Handle typing to pre-fill the value when editing starts + setValue(event.key); + + queueMicrotask(() => { + if (cellRef.current && cellRef.current.contentEditable === "true") { + cellRef.current.textContent = event.key; + const range = document.createRange(); + const selection = window.getSelection(); + range.selectNodeContents(cellRef.current); + range.collapse(false); + selection?.removeAllRanges(); + selection?.addRange(range); + } + }); + } + }, + [isEditing, isFocused, initialValue, tableMeta, rowIndex, columnId], + ); + + React.useEffect(() => { + const wasEditing = prevIsEditingRef.current; + prevIsEditingRef.current = isEditing; + + if (isEditing && !wasEditing && cellRef.current) { + cellRef.current.focus(); + + if (!cellRef.current.textContent && value) { + cellRef.current.textContent = value; + } + + if (cellRef.current.textContent) { + const range = document.createRange(); + const selection = window.getSelection(); + range.selectNodeContents(cellRef.current); + range.collapse(false); + selection?.removeAllRanges(); + selection?.addRange(range); + } + } + }, [isEditing, value]); + + const displayValue = !isEditing ? (value ?? "") : ""; + + return ( + + ref={containerRef} + cell={cell} + tableMeta={tableMeta} + rowIndex={rowIndex} + columnId={columnId} + rowHeight={rowHeight} + isEditing={isEditing} + isFocused={isFocused} + isSelected={isSelected} + isSearchMatch={isSearchMatch} + isActiveSearchMatch={isActiveSearchMatch} + readOnly={readOnly} + onKeyDown={onWrapperKeyDown} + > +
+ {displayValue} +
+ + ); +} + +export function LongTextCell({ + cell, + tableMeta, + rowIndex, + columnId, + rowHeight, + isFocused, + isEditing, + isSelected, + isSearchMatch, + isActiveSearchMatch, + readOnly, +}: DataGridCellProps) { + const initialValue = cell.getValue() as string; + const [value, setValue] = React.useState(initialValue ?? ""); + const textareaRef = React.useRef(null); + const containerRef = React.useRef(null); + const pendingCharRef = React.useRef(null); + const sideOffset = -(containerRef.current?.clientHeight ?? 0); + + const prevInitialValueRef = React.useRef(initialValue); + if (initialValue !== prevInitialValueRef.current) { + prevInitialValueRef.current = initialValue; + setValue(initialValue ?? ""); + } + + const debouncedSave = useDebouncedCallback((newValue: string) => { + if (!readOnly) { + tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: newValue }); + } + }, 300); + + const onSave = React.useCallback(() => { + // Immediately save any pending changes and close the popover + if (!readOnly && value !== initialValue) { + tableMeta?.onDataUpdate?.({ rowIndex, columnId, value }); + } + tableMeta?.onCellEditingStop?.(); + }, [tableMeta, value, initialValue, rowIndex, columnId, readOnly]); + + const onCancel = React.useCallback(() => { + // Restore the original value + setValue(initialValue ?? ""); + if (!readOnly) { + tableMeta?.onDataUpdate?.({ rowIndex, columnId, value: initialValue }); + } + tableMeta?.onCellEditingStop?.(); + }, [tableMeta, initialValue, rowIndex, columnId, readOnly]); + + const onOpenChange = React.useCallback( + (open: boolean) => { + if (open && !readOnly) { + tableMeta?.onCellEditingStart?.(rowIndex, columnId); + } else { + // Immediately save any pending changes when closing + if (!readOnly && value !== initialValue) { + tableMeta?.onDataUpdate?.({ rowIndex, columnId, value }); + } + tableMeta?.onCellEditingStop?.(); + } + }, + [tableMeta, value, initialValue, rowIndex, columnId, readOnly], + ); + + const onInitialFocus = React.useCallback(() => { + const textarea = textareaRef.current; + if (!textarea) return false; + + textarea.focus(); + const length = textarea.value.length; + textarea.setSelectionRange(length, length); + + // Insert pending character using execCommand so it's part of undo history + // Use requestAnimationFrame to ensure focus has fully settled + if (pendingCharRef.current) { + const char = pendingCharRef.current; + pendingCharRef.current = null; + requestAnimationFrame(() => { + if ( + textareaRef.current && + document.activeElement === textareaRef.current + ) { + document.execCommand("insertText", false, char); + textareaRef.current.scrollTop = textareaRef.current.scrollHeight; + } + }); + } else { + textarea.scrollTop = textarea.scrollHeight; + } + + return false; + }, []); + + const onWrapperKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if ( + isFocused && + !isEditing && + !readOnly && + event.key.length === 1 && + !event.ctrlKey && + !event.metaKey + ) { + // Store the character to be inserted after textarea focuses + // This ensures it's part of the textarea's undo history + pendingCharRef.current = event.key; + } + }, + [isFocused, isEditing, readOnly], + ); + + const onBlur = React.useCallback(() => { + // Immediately save any pending changes on blur + if (!readOnly && value !== initialValue) { + tableMeta?.onDataUpdate?.({ rowIndex, columnId, value }); + } + tableMeta?.onCellEditingStop?.(); + }, [tableMeta, value, initialValue, rowIndex, columnId, readOnly]); + + const onChange = React.useCallback( + (event: React.ChangeEvent) => { + const newValue = event.target.value; + setValue(newValue); + debouncedSave(newValue); + }, + [debouncedSave], + ); + + const onKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === "Escape") { + event.preventDefault(); + onCancel(); + } else if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) { + event.preventDefault(); + onSave(); + } else if (event.key === "Tab") { + event.preventDefault(); + // Save any pending changes + if (value !== initialValue) { + tableMeta?.onDataUpdate?.({ rowIndex, columnId, value }); + } + tableMeta?.onCellEditingStop?.({ + direction: event.shiftKey ? "left" : "right", + }); + return; + } + // Stop propagation to prevent grid navigation + event.stopPropagation(); + }, + [onSave, onCancel, value, initialValue, tableMeta, rowIndex, columnId], + ); + + return ( + + + ref={containerRef} + cell={cell} + tableMeta={tableMeta} + rowIndex={rowIndex} + columnId={columnId} + rowHeight={rowHeight} + isEditing={isEditing} + isFocused={isFocused} + isSelected={isSelected} + isSearchMatch={isSearchMatch} + isActiveSearchMatch={isActiveSearchMatch} + readOnly={readOnly} + onKeyDown={onWrapperKeyDown} + /> + } + > + {value} + + +