diff --git a/web/e2e/namespace-skill-pagination.spec.ts b/web/e2e/namespace-skill-pagination.spec.ts
new file mode 100644
index 00000000..f3d492bb
--- /dev/null
+++ b/web/e2e/namespace-skill-pagination.spec.ts
@@ -0,0 +1,78 @@
+import { expect, test } from '@playwright/test'
+import { setEnglishLocale } from './helpers/auth-fixtures'
+import { registerSession } from './helpers/session'
+import { E2eTestDataBuilder } from './helpers/test-data-builder'
+
+test.describe('Namespace Skill List Pagination (Real API)', () => {
+ test.beforeEach(async ({ page }, testInfo) => {
+ await setEnglishLocale(page)
+ await registerSession(page, testInfo)
+ })
+
+ test('shows namespace page with skills and no pagination when under 20 skills', async ({ page }, testInfo) => {
+ const builder = new E2eTestDataBuilder(page, testInfo)
+ await builder.init()
+
+ try {
+ const namespace = await builder.ensureWritableNamespace()
+ await builder.publishSkill(namespace.slug)
+
+ await page.goto(`/space/${namespace.slug}`)
+
+ await expect(page.getByText(`@${namespace.slug}`).first()).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'Skills', exact: true })).toBeVisible()
+
+ // Verify pagination controls do not appear when there are fewer than 20 skills
+ await expect(page.getByRole('button', { name: 'Previous' })).toHaveCount(0)
+ await expect(page.getByRole('button', { name: 'Next' })).toHaveCount(0)
+ } finally {
+ await builder.cleanup()
+ }
+ })
+
+ test('shows pagination controls when there are more than 20 skills', async ({ page }, testInfo) => {
+ const builder = new E2eTestDataBuilder(page, testInfo)
+ await builder.init()
+
+ try {
+ const namespace = await builder.ensureWritableNamespace()
+
+ // Publish 21 skills to trigger pagination (PAGE_SIZE = 20)
+ for (let i = 0; i < 21; i += 1) {
+ await builder.publishSkill(namespace.slug, {
+ name: `e2e-skill-${i}`,
+ description: `Test skill ${i} for pagination`,
+ })
+ }
+
+ await page.goto(`/space/${namespace.slug}`)
+
+ await expect(page.getByText(`@${namespace.slug}`).first()).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'Skills', exact: true })).toBeVisible()
+
+ // Verify pagination controls appear
+ const previousButton = page.getByRole('button', { name: 'Previous' }).first()
+ const nextButton = page.getByRole('button', { name: 'Next' }).first()
+
+ await expect(previousButton).toBeVisible()
+ await expect(nextButton).toBeVisible()
+
+ // First page: Previous should be disabled, Next should be enabled
+ await expect(previousButton).toBeDisabled()
+ await expect(nextButton).toBeEnabled()
+
+ // Navigate to second page
+ await nextButton.click()
+
+ // Second page: both buttons should be enabled (or Previous enabled, Next disabled if only 2 pages)
+ await expect(previousButton).toBeEnabled()
+
+ // Navigate back to first page
+ await previousButton.click()
+ await expect(previousButton).toBeDisabled()
+ await expect(nextButton).toBeEnabled()
+ } finally {
+ await builder.cleanup()
+ }
+ })
+})
diff --git a/web/src/pages/namespace.tsx b/web/src/pages/namespace.tsx
index 6754e953..69ac0127 100644
--- a/web/src/pages/namespace.tsx
+++ b/web/src/pages/namespace.tsx
@@ -1,12 +1,16 @@
+import { useState, useEffect } from 'react'
import { useNavigate, useParams } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { NamespaceHeader } from '@/features/namespace/namespace-header'
import { SkillCard } from '@/features/skill/skill-card'
import { SkeletonList } from '@/shared/components/skeleton-loader'
import { EmptyState } from '@/shared/components/empty-state'
+import { Pagination } from '@/shared/components/pagination'
import { useSearchSkills } from '@/shared/hooks/use-skill-queries'
import { useNamespaceDetail } from '@/shared/hooks/use-namespace-queries'
+const PAGE_SIZE = 20
+
/**
* Public namespace page showing namespace metadata and the skills currently discoverable inside it.
*/
@@ -14,13 +18,22 @@ export function NamespacePage() {
const { t } = useTranslation()
const navigate = useNavigate()
const { namespace } = useParams({ from: '/space/$namespace' })
+ const [page, setPage] = useState(0)
+
+ // Reset page when namespace changes
+ useEffect(() => {
+ setPage(0)
+ }, [namespace])
const { data: namespaceData, isLoading: isLoadingNamespace } = useNamespaceDetail(namespace)
const { data: skillsData, isLoading: isLoadingSkills } = useSearchSkills({
namespace,
- size: 20,
+ page,
+ size: PAGE_SIZE,
})
+ const totalPages = skillsData ? Math.max(Math.ceil(skillsData.total / skillsData.size), 1) : 1
+
const handleSkillClick = (slug: string) => {
navigate({ to: `/space/${namespace}/${encodeURIComponent(slug)}` })
}
@@ -47,16 +60,22 @@ export function NamespacePage() {
{isLoadingSkills ? (