{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "server-filter-table",
  "title": "SSR Filter Table",
  "description": "Column filtering resolved on the server the same way SSR pagination resolves pages, with manualFiltering and a URL-driven filter state.",
  "dependencies": [
    "@tanstack/react-table",
    "@tanstack/react-router",
    "@tanstack/react-start"
  ],
  "registryDependencies": [
    "table",
    "button",
    "select"
  ],
  "files": [
    {
      "path": "src/components/ssr/data.ts",
      "content": "import { createServerFn } from '@tanstack/react-start'\nimport type { User } from './pagination-example/columns'\n\nconst Users: User[] = [\n  {\n    id: 1,\n    name: 'Ava Thompson',\n    email: 'ava.t@example.com',\n    role: 'Admin',\n    status: 'Active',\n  },\n  {\n    id: 2,\n    name: 'Liam Chen',\n    email: 'liam.chen@example.com',\n    role: 'Editor',\n    status: 'Active',\n  },\n  {\n    id: 3,\n    name: 'Sofia Patel',\n    email: 'sofia.p@example.com',\n    role: 'Viewer',\n    status: 'Inactive',\n  },\n  {\n    id: 4,\n    name: 'Noah Garcia',\n    email: 'noah.g@example.com',\n    role: 'Editor',\n    status: 'Active',\n  },\n  {\n    id: 5,\n    name: 'Mia Johnson',\n    email: 'mia.j@example.com',\n    role: 'Admin',\n    status: 'Pending',\n  },\n  {\n    id: 6,\n    name: 'Ethan Kim',\n    email: 'ethan.kim@example.com',\n    role: 'Viewer',\n    status: 'Active',\n  },\n  {\n    id: 7,\n    name: 'Isabella Rossi',\n    email: 'isabella.r@example.com',\n    role: 'Editor',\n    status: 'Active',\n  },\n  {\n    id: 8,\n    name: 'Lucas Martin',\n    email: 'lucas.m@example.com',\n    role: 'Admin',\n    status: 'Inactive',\n  },\n  {\n    id: 9,\n    name: 'Amelia Novak',\n    email: 'amelia.n@example.com',\n    role: 'Viewer',\n    status: 'Active',\n  },\n  {\n    id: 10,\n    name: 'Mason Lee',\n    email: 'mason.lee@example.com',\n    role: 'Editor',\n    status: 'Pending',\n  },\n  {\n    id: 11,\n    name: 'Charlotte Diaz',\n    email: 'charlotte.d@example.com',\n    role: 'Admin',\n    status: 'Active',\n  },\n  {\n    id: 12,\n    name: 'James Wilson',\n    email: 'james.w@example.com',\n    role: 'Viewer',\n    status: 'Active',\n  },\n  {\n    id: 13,\n    name: 'Harper Nguyen',\n    email: 'harper.n@example.com',\n    role: 'Editor',\n    status: 'Inactive',\n  },\n  {\n    id: 14,\n    name: 'Benjamin Cruz',\n    email: 'ben.cruz@example.com',\n    role: 'Admin',\n    status: 'Active',\n  },\n  {\n    id: 15,\n    name: 'Ella Fischer',\n    email: 'ella.f@example.com',\n    role: 'Viewer',\n    status: 'Pending',\n  },\n  {\n    id: 16,\n    name: 'Alexander Reed',\n    email: 'alex.reed@example.com',\n    role: 'Editor',\n    status: 'Active',\n  },\n  {\n    id: 17,\n    name: 'Grace Coleman',\n    email: 'grace.c@example.com',\n    role: 'Admin',\n    status: 'Active',\n  },\n  {\n    id: 18,\n    name: 'Daniel Torres',\n    email: 'daniel.t@example.com',\n    role: 'Viewer',\n    status: 'Inactive',\n  },\n  {\n    id: 19,\n    name: 'Chloe Bennett',\n    email: 'chloe.b@example.com',\n    role: 'Editor',\n    status: 'Active',\n  },\n  {\n    id: 20,\n    name: 'Henry Brooks',\n    email: 'henry.b@example.com',\n    role: 'Admin',\n    status: 'Pending',\n  },\n  {\n    id: 21,\n    name: 'Zoe Sanders',\n    email: 'zoe.s@example.com',\n    role: 'Viewer',\n    status: 'Active',\n  },\n  {\n    id: 22,\n    name: 'Sebastian Ortiz',\n    email: 'sebastian.o@example.com',\n    role: 'Editor',\n    status: 'Active',\n  },\n  {\n    id: 23,\n    name: 'Layla Morgan',\n    email: 'layla.m@example.com',\n    role: 'Admin',\n    status: 'Inactive',\n  },\n  {\n    id: 24,\n    name: 'Jack Foster',\n    email: 'jack.f@example.com',\n    role: 'Viewer',\n    status: 'Active',\n  },\n  {\n    id: 25,\n    name: 'Victoria Hayes',\n    email: 'victoria.h@example.com',\n    role: 'Editor',\n    status: 'Pending',\n  },\n]\n\nexport const getUsersPage = createServerFn({ method: 'GET' })\n  .validator((input: { page: number; pageSize: number }) => input)\n  .handler(async ({ data }) => {\n    await new Promise((resolve) => setTimeout(resolve, 400))\n\n    const start = data.page * data.pageSize\n    return {\n      rows: Users.slice(start, start + data.pageSize),\n      pageCount: Math.ceil(Users.length / data.pageSize),\n    }\n  })\n\nexport const getUserPageWithFilter = createServerFn({ method: \"GET\" })\n  .validator((input: { page: number; pageSize: number; role?: string; status?: string }) => input)\n  .handler(async ({ data }) => {\n    await new Promise((resolve => setTimeout(resolve, 400)))\n\n    const filtered = Users.filter((u) => {\n      if (data.role && u.role !== data.role) return false\n      if (data.status && u.status !== data.status) return false\n      return true\n    })\n\n    const start = data.page * data.pageSize\n    return {\n      rows: filtered.slice(start, start + data.pageSize),\n      pageCount: Math.ceil(filtered.length / data.pageSize),\n    }\n  })\n\nexport const getUsersPageCombined = createServerFn({ method: 'GET' })\n  .validator(\n    (input: {\n      page: number\n      pageSize: number\n      role?: string\n      status?: string\n      sortBy?: string\n      sortDir?: 'asc' | 'desc'\n    }) => input,\n  )\n  .handler(async ({ data }) => {\n    await new Promise((resolve) => setTimeout(resolve, 400))\n\n    // 1. Filter — same as getUserPageWithFilter, resolved server-side first.\n    const filtered = Users.filter((u) => {\n      if (data.role && u.role !== data.role) return false\n      if (data.status && u.status !== data.status) return false\n      return true\n    })\n\n    // 2. Sort — runs against the already-filtered set, never the full table.\n    const sorted = data.sortBy\n      ? [...filtered].sort((a, b) => {\n          const key = data.sortBy as keyof User\n          const aVal = a[key]\n          const bVal = b[key]\n          const dir = data.sortDir === 'desc' ? -1 : 1\n          if (aVal < bVal) return -1 * dir\n          if (aVal > bVal) return 1 * dir\n          return 0\n        })\n      : filtered\n\n    // 3. Paginate — the last step, so page counts reflect filtered+sorted rows.\n    const start = data.page * data.pageSize\n    return {\n      rows: sorted.slice(start, start + data.pageSize),\n      pageCount: Math.ceil(sorted.length / data.pageSize),\n    }\n  })\n",
      "type": "registry:component",
      "target": "@components/tables/server-filter-table/data.ts"
    },
    {
      "path": "src/components/ssr/filter-example/columns.tsx",
      "content": "'use client'\n\nimport type { ColumnDef } from '@tanstack/react-table'\n\n// This type is used to define the shape of our data.\n// You can use a Zod schema here if you want.\nexport type User = {\n  id: number\n  name: string\n  email: string\n  role: string\n  status: string\n}\n\nexport const columns: ColumnDef<User>[] = [\n  {\n    accessorKey: 'name',\n    header: 'Name',\n    enableSorting: false,\n  },\n  {\n    accessorKey: 'email',\n    header: 'Email',\n    sortingFn: 'alphanumeric',\n  },\n  {\n    accessorKey: 'role',\n    header: 'Role',\n    sortingFn: 'basic',\n    filterFn: 'equalsString',\n  },\n  {\n    accessorKey: 'status',\n    header: 'Status',\n    sortingFn: 'basic',\n    filterFn: 'equalsString',\n  },\n]\n",
      "type": "registry:component",
      "target": "@components/tables/server-filter-table/columns.tsx"
    },
    {
      "path": "src/components/ssr/filter-example/data-table.tsx",
      "content": "'use client'\n\nimport {\n  flexRender,\n  getCoreRowModel,\n  getSortedRowModel,\n  useReactTable,\n} from '@tanstack/react-table'\n\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { useRouterState } from '@tanstack/react-router'\n\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from '#/components/ui/table'\nimport { useState } from 'react'\nimport { Button } from '#/components/ui/button'\nimport {\n  ArrowDown,\n  ArrowUp,\n  ArrowUpDown,\n  ChevronLeft,\n  ChevronRight,\n  ChevronsLeft,\n  ChevronsRight,\n} from 'lucide-react'\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '#/components/ui/select'\nimport { cn } from '#/lib/utils.ts'\nimport type { User } from './columns'\nimport { Route } from '#/routes/server-filter'\n\ninterface DataTableProps {\n  columns: ColumnDef<User>[]\n}\n\nconst PAGES_SIZE = [\n  { label: '10', value: '10' },\n  { label: '20', value: '20' },\n  { label: '30', value: '30' },\n  { label: '40', value: '40' },\n  { label: '50', value: '50' },\n]\n\nexport function DataTable({ columns }: DataTableProps) {\n  const { page, pageSize, role } = Route.useSearch()\n  const { rows, pageCount } = Route.useLoaderData()\n  const navigate = Route.useNavigate()\n  const isPending = useRouterState({ select: (s) => s.isLoading })\n\n  const [sorting, setSorting] = useState<SortingState>([])\n\n  const table = useReactTable({\n    data: rows,\n    columns,\n    pageCount,\n    manualPagination: true,\n    getCoreRowModel: getCoreRowModel(),\n    getSortedRowModel: getSortedRowModel(),\n    state: {\n      sorting,\n      pagination: { pageIndex: page, pageSize },\n    },\n    onPaginationChange: (updater) => {\n      const next =\n        typeof updater === 'function'\n          ? updater({ pageIndex: page, pageSize })\n          : updater\n      navigate({\n        search: (prev) => ({\n          ...prev,\n          page: next.pageIndex,\n          pageSize: next.pageSize,\n        }),\n      })\n    },\n    onSortingChange: setSorting,\n  })\n\n  return (\n    <div>\n      <Select\n\n        value={role || 'all'}\n        onValueChange={(val) =>\n          navigate({\n            search: (prev) => ({\n              ...prev,\n              role: val === 'all' ? '' : val,\n              page: 0, // reset to page 0 — the result set size changed\n            }),\n          })\n        }\n      >\n        <SelectTrigger className='mb-4 w-32'><SelectValue placeholder=\"Role\" /></SelectTrigger>\n        <SelectContent>\n          <SelectItem value=\"all\">All roles</SelectItem>\n          <SelectItem value=\"Admin\">Admin</SelectItem>\n          <SelectItem value=\"Editor\">Editor</SelectItem>\n          <SelectItem value=\"Viewer\">Viewer</SelectItem>\n        </SelectContent>\n      </Select>\n      <div\n        className={cn(\n          'overflow-hidden rounded-md border transition-opacity',\n          isPending && 'opacity-50',\n        )}\n      >\n\n        <Table>\n          <TableHeader>\n            {table.getHeaderGroups().map((headerGroup) => (\n              <TableRow key={headerGroup.id}>\n                {headerGroup.headers.map((header) => {\n                  const canSort = header.column.getCanSort()\n                  const sortHandler = header.column.getToggleSortingHandler()\n                  return (\n                    <TableHead\n                      key={header.id}\n                      onClick={sortHandler}\n                      onKeyDown={(e) => {\n                        if (canSort && (e.key === 'Enter' || e.key === ' ')) {\n                          e.preventDefault()\n                          sortHandler?.(e)\n                        }\n                      }}\n                      tabIndex={canSort ? 0 : undefined}\n                      role={canSort ? 'button' : undefined}\n                      className={\n                        canSort ? 'cursor-pointer select-none' : undefined\n                      }\n                    >\n                      <div className=\"flex flex-row items-center gap-2\">\n                        {header.isPlaceholder\n                          ? null\n                          : flexRender(\n                              header.column.columnDef.header,\n                              header.getContext(),\n                            )}\n                        {canSort ? (\n                          <span>\n                            {header.column.getIsSorted() === 'asc' ? (\n                              <ArrowUp className=\"h-3 w-3\" />\n                            ) : header.column.getIsSorted() === 'desc' ? (\n                              <ArrowDown className=\"h-3 w-3\" />\n                            ) : (\n                              <ArrowUpDown className=\"h-3 w-3 opacity-50\" />\n                            )}\n                          </span>\n                        ) : null}\n                      </div>\n                    </TableHead>\n                  )\n                })}\n              </TableRow>\n            ))}\n          </TableHeader>\n          <TableBody>\n            {table.getRowModel().rows.length ? (\n              table.getRowModel().rows.map((row) => (\n                <TableRow\n                  key={row.id}\n                  data-state={row.getIsSelected() && 'selected'}\n                >\n                  {row.getVisibleCells().map((cell) => (\n                    <TableCell key={cell.id}>\n                      {flexRender(\n                        cell.column.columnDef.cell,\n                        cell.getContext(),\n                      )}\n                    </TableCell>\n                  ))}\n                </TableRow>\n              ))\n            ) : (\n              <TableRow>\n                <TableCell\n                  colSpan={columns.length}\n                  className=\"h-24 text-center\"\n                >\n                  No results.\n                </TableCell>\n              </TableRow>\n            )}\n          </TableBody>\n        </Table>\n      </div>\n      <div className=\"flex gap-1 justify-end mt-5\">\n        <Button\n          onClick={() => table.firstPage()}\n          disabled={!table.getCanPreviousPage()}\n          variant={'outline'}\n        >\n          <ChevronsLeft className=\"h-3 w-3\" />\n        </Button>\n        <Button\n          onClick={() => table.previousPage()}\n          disabled={!table.getCanPreviousPage()}\n          variant={'outline'}\n        >\n          <ChevronLeft className=\"h-3 w-3\" />\n        </Button>\n        <Button\n          onClick={() => table.nextPage()}\n          disabled={!table.getCanNextPage()}\n          variant={'outline'}\n        >\n          <ChevronRight className=\"h-3 w-3\" />\n        </Button>\n        <Button\n          onClick={() => table.lastPage()}\n          disabled={!table.getCanNextPage()}\n          variant={'outline'}\n        >\n          <ChevronsRight className=\"h-3 w-3\" />\n        </Button>\n        <Select\n          value={pageSize.toString()}\n          onValueChange={(val) => table.setPageSize(Number(val))}\n        >\n          <SelectTrigger className=\"h-1/3\">\n            <SelectValue placeholder={PAGES_SIZE[0].label} />\n          </SelectTrigger>\n          <SelectContent>\n            <SelectGroup>\n              {PAGES_SIZE.map((item) => (\n                <SelectItem key={item.value} value={item.value}>\n                  {item.label}\n                </SelectItem>\n              ))}\n            </SelectGroup>\n          </SelectContent>\n        </Select>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/tables/server-filter-table/data-table.tsx"
    },
    {
      "path": "src/components/ssr/filter-example/index.tsx",
      "content": "import { columns } from './columns'\nimport { DataTable } from './data-table'\n\nexport function ServerFilterDemo() {\n  return <DataTable columns={columns} />\n}\n",
      "type": "registry:component",
      "target": "@components/tables/server-filter-table/index.tsx"
    },
    {
      "path": "src/routes/server-filter.ts",
      "content": "import { createFileRoute } from '@tanstack/react-router'\nimport { ServerFilterPage } from '#/components/ssr/filter-example/filter-page'\nimport { getUserPageWithFilter } from '#/components/ssr/data'\n\nexport const Route = createFileRoute('/server-filter')({\n  head: () => ({\n    meta: [\n      { title: 'SSR Filter — ShadTable' },\n      {\n        name: 'description',\n        content:\n          'Filtering by column, resolved on the server the same way SSR Pagination resolves pages — every filter change is a real server request, not an in-memory slice.',\n      },\n      { property: 'og:title', content: 'SSR Filter — ShadTable' },\n      {\n        property: 'og:description',\n        content:\n          'A server-filtered table where column filters are resolved on the server, built on shadcn/ui and TanStack Table.',\n      },\n      {\n        'script:ld+json': {\n          '@context': 'https://schema.org',\n          '@type': 'SoftwareSourceCode',\n          name: 'SSR Filter Table',\n          description:\n            'A server-filtered table where column filters are resolved on the server.',\n          codeRepository: 'https://github.com/coros-hq/shadcn-table-library',\n          programmingLanguage: 'TypeScript',\n        },\n      },\n    ],\n    links: [\n      {\n        rel: 'canonical',\n        href: 'https://shad-table.dev/server-filter',\n      },\n    ],\n  }),\n  validateSearch: (search) => ({\n    page: Number(search.page ?? 0),\n    pageSize: Number(search.pageSize ?? 10),\n    role: (search.role as string) ?? '',\n    status: (search.status as string) ?? '',\n  }),\n  loaderDeps: ({ search }) => search,\n  loader: ({ deps }) => getUserPageWithFilter({ data: deps }),\n  component: ServerFilterPage,\n})\n",
      "type": "registry:component",
      "target": "src/routes/server-filter.ts"
    }
  ],
  "type": "registry:block"
}