{#if visible.length === 0}
-
- {#if source?.type === "org"}
-
Nothing in {source.login} matches.Loosen language, stars, or the homepage toggle.
- {:else if source?.type === "all"}
-
Nothing across personal and orgs matches.Loosen language, stars, or the homepage toggle.
- {:else}
-
Nothing matches these filters.Loosen language, stars, or the homepage toggle.
- {/if}
+
+
+ {#if source?.type === "org"}
+ Nothing in {source.login} matches.Loosen language, stars, or the homepage toggle.
+ {:else if source?.type === "all"}
+ Nothing across personal and orgs matches.Loosen language, stars, or the homepage toggle.
+ {:else}
+ Nothing matches these filters.Loosen language, stars, or the homepage toggle.
+ {/if}
+
{:else}
- {#each visible as repo, index}
-
- {/each}
+
+ {#each catalogPage.items as repo, index}
+
+ {/each}
+
+
{/if}
{:else}
diff --git a/web/src/pages/Home.test.js b/web/src/pages/Home.test.js
index 135474b..89ea02f 100644
--- a/web/src/pages/Home.test.js
+++ b/web/src/pages/Home.test.js
@@ -124,4 +124,60 @@ describe("Home", () => {
await fireEvent.click(screen.getByRole("tab", { name: "Catalog" }));
expect(screen.getByRole("link", { name: "hello-world" })).toBeInTheDocument();
});
+
+ test("paginates the catalog twenty repositories at a time", async () => {
+ const repos = Array.from({ length: 21 }, (_, i) => ({
+ name: `repo-${i + 1}`,
+ html_url: `https://github.com/octocat/repo-${i + 1}`,
+ stargazers_count: 21 - i,
+ forks_count: 0,
+ language: "Go",
+ description: "demo",
+ updated_at: "2026-08-01T00:00:00Z",
+ }));
+ render(Home, {
+ props: {
+ populated: true,
+ lookup: "octocat",
+ profile: { login: "octocat", name: "The Octocat", html_url: "https://github.com/octocat", avatar_url: "https://example.com/a.png", public_repos: 21, followers: 1, following: 1 },
+ orgs: [],
+ source: { type: "user", login: "octocat" },
+ repos,
+ error: "",
+ },
+ });
+ expect(screen.getByRole("link", { name: "repo-1" })).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: "repo-20" })).toBeInTheDocument();
+ expect(screen.queryByRole("link", { name: "repo-21" })).not.toBeInTheDocument();
+ await fireEvent.click(screen.getByRole("button", { name: "Next page" }));
+ expect(screen.getByRole("link", { name: "repo-21" })).toBeInTheDocument();
+ expect(screen.queryByRole("link", { name: "repo-1" })).not.toBeInTheDocument();
+ });
+
+ test("lists public repositories by default and offers visibility options", async () => {
+ render(Home, {
+ props: {
+ populated: true,
+ lookup: "octocat",
+ profile: { login: "octocat", name: "The Octocat", html_url: "https://github.com/octocat", avatar_url: "https://example.com/a.png", public_repos: 2, followers: 1, following: 1 },
+ orgs: [],
+ source: { type: "user", login: "octocat" },
+ repos: [
+ { name: "hello-world", html_url: "https://github.com/octocat/hello-world", stargazers_count: 10, forks_count: 0, language: "Go", description: "public", updated_at: "2026-08-01T00:00:00Z", private: false },
+ { name: "secret", html_url: "https://github.com/octocat/secret", stargazers_count: 1, forks_count: 0, language: "Go", description: "private", updated_at: "2026-08-01T00:00:00Z", private: true },
+ ],
+ error: "",
+ },
+ });
+ expect(screen.getByRole("link", { name: "hello-world" })).toBeInTheDocument();
+ expect(screen.queryByRole("link", { name: "secret" })).not.toBeInTheDocument();
+ const visibility = screen.getByLabelText("Visibility");
+ expect(within(visibility).getByRole("option", { name: "Public" })).toBeInTheDocument();
+ expect(within(visibility).getByRole("option", { name: "Private" })).toBeInTheDocument();
+ expect(within(visibility).getByRole("option", { name: "All" })).toBeInTheDocument();
+ await fireEvent.change(visibility, { target: { value: "all" } });
+ expect(screen.getByRole("link", { name: "secret" })).toBeInTheDocument();
+ });
});
+
+
diff --git a/web/static/css/atlas.css b/web/static/css/atlas.css
index cc6d2e5..8322174 100644
--- a/web/static/css/atlas.css
+++ b/web/static/css/atlas.css
@@ -572,7 +572,8 @@ a.stat:hover {
grid-column: span 3;
}
-.field-stars {
+.field-stars,
+.field-visibility {
grid-column: span 2;
}
@@ -1356,6 +1357,45 @@ a.stat:hover {
margin-left: 0.45rem;
}
+.catalog-pager {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.85rem;
+ margin: 1.35rem 0 0.4rem;
+}
+
+.pager-btn {
+ border: 1px solid var(--line-strong);
+ background: var(--bg-elev);
+ color: var(--ink);
+ border-radius: 999px;
+ padding: 0.45rem 1.05rem;
+ font: 600 0.88rem/1 var(--font-ui);
+ cursor: pointer;
+}
+
+.pager-btn:hover:not(:disabled),
+.pager-btn:focus-visible {
+ border-color: var(--copper);
+ color: var(--copper-hot);
+ outline: none;
+}
+
+.pager-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+.pager-status {
+ margin: 0;
+ font-family: var(--font-mono);
+ font-size: 0.76rem;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--ink-mute);
+}
+
@keyframes rise {
from {
opacity: 0;
@@ -1380,6 +1420,7 @@ a.stat:hover {
.field,
.field-stars,
+ .field-visibility,
.field-grow,
.field-sort {
grid-column: span 6;
@@ -1431,6 +1472,7 @@ a.stat:hover {
.field,
.field-stars,
+ .field-visibility,
.field-grow,
.field-sort {
grid-column: span 12;