Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions website/src/components/brand/AgentsCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ import { useEffect, useMemo, useRef, useState } from "react";

import { PageShell } from "./PageShell";
import { CatalogSortControl, CATALOG_SORT_OPTIONS } from "./CatalogSortControl";
import {
CatalogResourceSizeControl,
CATALOG_RESOURCE_SIZE_OPTIONS,
DEFAULT_CATALOG_RESOURCE_SIZE,
type CatalogResourceSize,
} from "./CatalogResourceSizeControl";
import { daysSince, toggleValue, updatedBuckets, updatedBucketOf } from "./catalogFilters";
import { pageHref } from "./pageHref";
import { downloadFile } from "./resourceActions";
Expand Down Expand Up @@ -91,8 +97,6 @@ const emptyFilters: FilterState = { model: [], tools: [], updated: [] };

type SortMode = "az" | "newest";

const PAGE_SIZE = 6;

/**
* Long facets collapse behind a "Show N more" toggle, matching the prototype's
* extensions page. Real data produces far longer option lists than the
Expand All @@ -119,6 +123,9 @@ export function AgentsCatalog({
}) {
const { colorMode } = useTheme();
const [sortMode, setSortMode] = useState<SortMode>("az");
const [pageSize, setPageSize] = useState<CatalogResourceSize>(
DEFAULT_CATALOG_RESOURCE_SIZE,
);
const [currentPage, setCurrentPage] = useState(1);
const previousPage = useRef(currentPage);
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
Expand Down Expand Up @@ -189,9 +196,9 @@ export function AgentsCatalog({
const activeFilterCount =
filters.model.length + filters.tools.length + filters.updated.length;
const hasActiveFilters = activeFilterCount > 0;
const pageCount = Math.max(1, Math.ceil(sortedAgents.length / PAGE_SIZE));
const pageCount = Math.max(1, Math.ceil(sortedAgents.length / pageSize));
const page = Math.min(currentPage, pageCount);
const visibleAgents = sortedAgents.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
const visibleAgents = sortedAgents.slice((page - 1) * pageSize, page * pageSize);

useEffect(() => {
if (previousPage.current === currentPage) return;
Expand Down Expand Up @@ -368,6 +375,17 @@ export function AgentsCatalog({
setCurrentPage(1);
}}
/>
<div className={styles.toolbarDivider} aria-hidden="true" />
<CatalogResourceSizeControl
ariaLabel="Agents per page"
value={pageSize}
options={CATALOG_RESOURCE_SIZE_OPTIONS}
styles={styles}
onChange={(value) => {
setPageSize(value);
setCurrentPage(1);
}}
/>
</Box>
<Box className={styles.gridFrame} data-mode={colorMode}>
<Box className={styles.gridContent}>
Expand Down
82 changes: 82 additions & 0 deletions website/src/components/brand/CatalogResourceSizeControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { CheckIcon, ChevronDownIcon } from "@primer/octicons-react";
import { clsx } from "clsx";

export type CatalogResourceSizeOption<T extends number> = {
value: T;
label: string;
};

export const CATALOG_RESOURCE_SIZE_OPTIONS = [
{ value: 6, label: "6" },
{ value: 25, label: "25" },
{ value: 50, label: "50" },
{ value: 100, label: "100" },
] as const satisfies readonly CatalogResourceSizeOption<number>[];

/** A page size the catalogs offer: 6, 25, 50, or 100 items. */
export type CatalogResourceSize =
(typeof CATALOG_RESOURCE_SIZE_OPTIONS)[number]["value"];

/** How many items a catalog page shows until the reader picks a size. */
export const DEFAULT_CATALOG_RESOURCE_SIZE: CatalogResourceSize = 6;

type CatalogResourceSizeControlProps<T extends number> = {
ariaLabel: string;
onChange: (value: T) => void;
options: readonly CatalogResourceSizeOption<T>[];
/** The catalog's CSS module. The menu reuses the sort menu's classes so the
* two toolbar menus look the same. */
styles: Record<string, string | undefined>;
value: T;
};

export function CatalogResourceSizeControl<T extends number>({
ariaLabel,
onChange,
options,
styles,
value,
}: CatalogResourceSizeControlProps<T>) {
const selectedLabel =
options.find((option) => option.value === value)?.label ?? String(value);

return (
<div className={styles.sortControl}>
<span className={styles.sortLabel}>Resource page size:</span>
<details className={styles.sortMenu}>
<summary
className={styles.sortTrigger}
aria-label={`${ariaLabel}: ${selectedLabel}`}
Comment thread
jhauga marked this conversation as resolved.
Outdated
>
{selectedLabel}
<ChevronDownIcon size={16} className={styles.sortChevron} />
</summary>
<div className={styles.sortOverlay} role="menu">
Comment thread
jhauga marked this conversation as resolved.
Outdated
{options.map((option) => (
<button
key={option.value}
type="button"
role="menuitemradio"
aria-checked={value === option.value}
className={clsx(
styles.sortOption,
value === option.value && styles.sortOptionActive,
)}
onClick={(event) => {
onChange(option.value);
event.currentTarget.closest("details")?.removeAttribute("open");
}}
>
{option.label}
<CheckIcon
size={16}
className={styles.sortOptionCheck}
aria-hidden={value !== option.value}
/>
</button>
))}
</div>
</details>
</div>
);
}
28 changes: 23 additions & 5 deletions website/src/components/brand/ExtensionsCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ import {
import { ExtensionsIcon } from "./ExtensionsIcon";
import { PageShell } from "./PageShell";
import { CatalogSortControl, CATALOG_SORT_OPTIONS } from "./CatalogSortControl";
import {
CatalogResourceSizeControl,
CATALOG_RESOURCE_SIZE_OPTIONS,
DEFAULT_CATALOG_RESOURCE_SIZE,
type CatalogResourceSize,
} from "./CatalogResourceSizeControl";
import { TypingText } from "./TypingText";
import { httpUrl } from "./resourceActions";
import {
Expand Down Expand Up @@ -84,8 +90,6 @@ type FilterState = Record<FilterGroupId, string[]>;

const emptyFilters: FilterState = { author: [], updated: [], keyword: [] };

const PAGE_SIZE = 6;

type SortMode = "az" | "newest";

function CopyInstallButton({
Expand Down Expand Up @@ -148,6 +152,9 @@ export function ExtensionsCatalog({
}) {
const { colorMode } = useTheme();
const [sortMode, setSortMode] = useState<SortMode>("az");
const [pageSize, setPageSize] = useState<CatalogResourceSize>(
DEFAULT_CATALOG_RESOURCE_SIZE,
);
const [currentPage, setCurrentPage] = useState(1);
const previousPage = React.useRef(currentPage);
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
Expand Down Expand Up @@ -234,11 +241,11 @@ export function ExtensionsCatalog({
const activeFilterCount =
filters.author.length + filters.updated.length + filters.keyword.length;
const hasActiveFilters = activeFilterCount > 0;
const pageCount = Math.max(1, Math.ceil(sortedExtensions.length / PAGE_SIZE));
const pageCount = Math.max(1, Math.ceil(sortedExtensions.length / pageSize));
const page = Math.min(currentPage, pageCount);
const visibleExtensions = sortedExtensions.slice(
(page - 1) * PAGE_SIZE,
page * PAGE_SIZE,
(page - 1) * pageSize,
page * pageSize,
);

React.useEffect(() => {
Expand Down Expand Up @@ -375,6 +382,17 @@ export function ExtensionsCatalog({
setCurrentPage(1);
}}
/>
<div className={styles.toolbarDivider} aria-hidden="true" />
<CatalogResourceSizeControl
ariaLabel="Extensions per page"
value={pageSize}
options={CATALOG_RESOURCE_SIZE_OPTIONS}
styles={styles}
onChange={(value) => {
setPageSize(value);
setCurrentPage(1);
}}
/>
</Box>
<Box className={styles.gridFrame} data-mode={colorMode}>
<Box className={styles.gridContent}>
Expand Down
28 changes: 23 additions & 5 deletions website/src/components/brand/InstructionsCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ import { useMemo, useState } from "react";

import { PageShell } from "./PageShell";
import { CatalogSortControl, CATALOG_SORT_OPTIONS } from "./CatalogSortControl";
import {
CatalogResourceSizeControl,
CATALOG_RESOURCE_SIZE_OPTIONS,
DEFAULT_CATALOG_RESOURCE_SIZE,
type CatalogResourceSize,
} from "./CatalogResourceSizeControl";
import {
daysSince,
toggleValue,
Expand Down Expand Up @@ -86,8 +92,6 @@ type FilterState = Record<FilterGroupId, string[]>;

const emptyFilters: FilterState = { pattern: [], extension: [], updated: [] };

const PAGE_SIZE = 6;

/**
* Long facets collapse behind a "Show N more" toggle, matching the prototype's
* extensions page. Real data produces far longer option lists than the
Expand All @@ -114,6 +118,9 @@ export function InstructionsCatalog({
}) {
const { colorMode } = useTheme();
const [sortMode, setSortMode] = useState<SortMode>("az");
const [pageSize, setPageSize] = useState<CatalogResourceSize>(
DEFAULT_CATALOG_RESOURCE_SIZE,
);
const [currentPage, setCurrentPage] = useState(1);
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const [filters, setFilters] = useState<FilterState>(emptyFilters);
Expand Down Expand Up @@ -190,12 +197,12 @@ export function InstructionsCatalog({
const hasActiveFilters = activeFilterCount > 0;
const pageCount = Math.max(
1,
Math.ceil(sortedInstructions.length / PAGE_SIZE),
Math.ceil(sortedInstructions.length / pageSize),
);
const page = Math.min(currentPage, pageCount);
const visibleInstructions = sortedInstructions.slice(
(page - 1) * PAGE_SIZE,
page * PAGE_SIZE,
(page - 1) * pageSize,
page * pageSize,
);

return (
Expand Down Expand Up @@ -471,6 +478,17 @@ export function InstructionsCatalog({
setCurrentPage(1);
}}
/>
<div className={styles.toolbarDivider} aria-hidden="true" />
<CatalogResourceSizeControl
ariaLabel="Instructions per page"
value={pageSize}
options={CATALOG_RESOURCE_SIZE_OPTIONS}
styles={styles}
onChange={(value) => {
setPageSize(value);
setCurrentPage(1);
}}
/>
</Box>
<Box className={styles.gridFrame} data-mode={colorMode}>
<Box className={styles.gridContent}>
Expand Down
95 changes: 58 additions & 37 deletions website/src/components/brand/LearningHubIndex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ import {
} from "@primer/react-brand";

import styles from "./styles/learning-hub-copilot-app.module.css";
import {
CatalogResourceSizeControl,
CATALOG_RESOURCE_SIZE_OPTIONS,
DEFAULT_CATALOG_RESOURCE_SIZE,
type CatalogResourceSize,
} from "./CatalogResourceSizeControl";
import { DuckIcon } from "./DuckIcon";
import { LearningHubIcon } from "./LearningHubIcon";
import { PageShell } from "./PageShell";
Expand Down Expand Up @@ -293,6 +299,9 @@ function LearningHubIndexBody({

const [filters, setFilters] = useState<FilterState>(emptyFilters);
const [sortOrder, setSortOrder] = useState<"newest" | "oldest">("newest");
const [pageSize, setPageSize] = useState<CatalogResourceSize>(
DEFAULT_CATALOG_RESOURCE_SIZE,
);
Comment thread
jhauga marked this conversation as resolved.

const filteredEntries = useMemo(() => {
const query = searchTerm.trim().toLowerCase();
Expand Down Expand Up @@ -414,7 +423,6 @@ function LearningHubIndexBody({
const activeFilterCount = filters.topic.length + filters.kind.length;
const hasActiveFilters = activeFilterCount > 0;

const pageSize = 6;
const pageCount = Math.max(1, Math.ceil(sortedEntries.length / pageSize));
const page = Math.min(currentPage, pageCount);
const visibleEntries = sortedEntries.slice(
Expand Down Expand Up @@ -677,42 +685,55 @@ function LearningHubIndexBody({
<Heading as="h2" size="5" className={styles.recommendedTitle}>
Articles
</Heading>
<div className={styles.sortControl}>
<span className={styles.sortLabel}>Sort by:</span>
<details className={styles.sortMenu}>
<summary className={styles.sortTrigger}>
{sortOrder === "newest" ? "Newest" : "Oldest"}
<ChevronDownIcon size={16} className={styles.sortChevron} />
</summary>
<div className={styles.sortOverlay} role="menu">
{(["newest", "oldest"] as const).map((value) => (
<button
key={value}
type="button"
role="menuitemradio"
aria-checked={sortOrder === value}
className={clsx(
styles.sortOption,
sortOrder === value && styles.sortOptionActive,
)}
onClick={(event) => {
setSortOrder(value);
setCurrentPage(1);
event.currentTarget
.closest("details")
?.removeAttribute("open");
}}
>
{value === "newest" ? "Newest" : "Oldest"}
<CheckIcon
size={16}
className={styles.sortOptionCheck}
aria-hidden={sortOrder !== value}
/>
</button>
))}
</div>
</details>
<div className={styles.catalogTools}>
<div className={styles.sortControl}>
<span className={styles.sortLabel}>Sort by:</span>
<details className={styles.sortMenu}>
<summary className={styles.sortTrigger}>
{sortOrder === "newest" ? "Newest" : "Oldest"}
<ChevronDownIcon size={16} className={styles.sortChevron} />
</summary>
<div className={styles.sortOverlay} role="menu">
{(["newest", "oldest"] as const).map((value) => (
<button
key={value}
type="button"
role="menuitemradio"
aria-checked={sortOrder === value}
className={clsx(
styles.sortOption,
sortOrder === value && styles.sortOptionActive,
)}
onClick={(event) => {
setSortOrder(value);
setCurrentPage(1);
event.currentTarget
.closest("details")
?.removeAttribute("open");
}}
>
{value === "newest" ? "Newest" : "Oldest"}
<CheckIcon
size={16}
className={styles.sortOptionCheck}
aria-hidden={sortOrder !== value}
/>
</button>
))}
</div>
</details>
</div>
<div className={styles.toolbarDivider} aria-hidden="true" />
<CatalogResourceSizeControl
ariaLabel="Articles per page"
value={pageSize}
options={CATALOG_RESOURCE_SIZE_OPTIONS}
styles={styles}
onChange={(value) => {
setPageSize(value);
setCurrentPage(1);
}}
/>
</div>
<button
type="button"
Expand Down
Loading
Loading