Skip to content

Commit 34fbe57

Browse files
Add search and category to the gallery (#10256)
<img width="942" height="895" alt="image" src="https://github.com/user-attachments/assets/09f1b001-7a64-4942-80af-29e131ba2c60" />
1 parent c84f756 commit 34fbe57

4 files changed

Lines changed: 134 additions & 6 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
3+
changeKind: feature
4+
packages:
5+
- "@typespec/playground"
6+
---
7+
8+
Add search and category grouping to samples

packages/playground/src/react/samples-drawer/samples-drawer-trigger.tsx

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import {
44
DrawerHeader,
55
DrawerHeaderTitle,
66
OverlayDrawer,
7+
SearchBox,
8+
Text,
79
ToolbarButton,
810
Tooltip,
911
} from "@fluentui/react-components";
1012
import { Dismiss24Regular, DocumentBulletList24Regular } from "@fluentui/react-icons";
11-
import { useCallback, useState, type FunctionComponent } from "react";
13+
import { useCallback, useMemo, useState, type FunctionComponent } from "react";
1214
import type { PlaygroundSample } from "../../types.js";
1315
import { SampleCard } from "./sample-card.js";
1416
import style from "./samples-drawer.module.css";
@@ -23,13 +25,49 @@ export interface SamplesDrawerOverlayProps extends SamplesDrawerProps {
2325
onOpenChange: (open: boolean) => void;
2426
}
2527

28+
interface SampleCategory {
29+
name: string;
30+
entries: [string, PlaygroundSample][];
31+
}
32+
33+
function groupAndFilterSamples(
34+
samples: Record<string, PlaygroundSample>,
35+
searchQuery: string,
36+
): SampleCategory[] {
37+
const query = searchQuery.toLowerCase().trim();
38+
const categoryMap = new Map<string, [string, PlaygroundSample][]>();
39+
40+
for (const [name, sample] of Object.entries(samples)) {
41+
if (query) {
42+
const matchesName = name.toLowerCase().includes(query);
43+
const matchesDescription = sample.description?.toLowerCase().includes(query);
44+
const matchesCategory = sample.category?.toLowerCase().includes(query);
45+
if (!matchesName && !matchesDescription && !matchesCategory) continue;
46+
}
47+
48+
const category = sample.category ?? "Other";
49+
let entries = categoryMap.get(category);
50+
if (!entries) {
51+
entries = [];
52+
categoryMap.set(category, entries);
53+
}
54+
entries.push([name, sample]);
55+
}
56+
57+
return Array.from(categoryMap.entries())
58+
.map(([name, entries]) => ({ name, entries }))
59+
.sort((a, b) => a.name.localeCompare(b.name));
60+
}
61+
2662
/** The overlay drawer showing the sample gallery. Controlled via open/onOpenChange. */
2763
export const SamplesDrawerOverlay: FunctionComponent<SamplesDrawerOverlayProps> = ({
2864
samples,
2965
onSelectedSampleNameChange,
3066
open,
3167
onOpenChange,
3268
}) => {
69+
const [searchQuery, setSearchQuery] = useState("");
70+
3371
const handleSampleSelect = useCallback(
3472
(sampleName: string) => {
3573
onSelectedSampleNameChange(sampleName);
@@ -38,10 +76,23 @@ export const SamplesDrawerOverlay: FunctionComponent<SamplesDrawerOverlayProps>
3876
[onSelectedSampleNameChange, onOpenChange],
3977
);
4078

79+
const categories = useMemo(
80+
() => groupAndFilterSamples(samples, searchQuery),
81+
[samples, searchQuery],
82+
);
83+
const hasCategories = useMemo(() => Object.values(samples).some((s) => s.category), [samples]);
84+
const totalFiltered = useMemo(
85+
() => categories.reduce((sum, c) => sum + c.entries.length, 0),
86+
[categories],
87+
);
88+
4189
return (
4290
<OverlayDrawer
4391
open={open}
44-
onOpenChange={(_, data) => onOpenChange(data.open)}
92+
onOpenChange={(_, data) => {
93+
onOpenChange(data.open);
94+
if (!data.open) setSearchQuery("");
95+
}}
4596
position="end"
4697
size="large"
4798
>
@@ -60,11 +111,46 @@ export const SamplesDrawerOverlay: FunctionComponent<SamplesDrawerOverlayProps>
60111
</DrawerHeaderTitle>
61112
</DrawerHeader>
62113
<DrawerBody>
63-
<div className={style["samples-grid"]}>
64-
{Object.entries(samples).map(([name, sample]) => (
65-
<SampleCard key={name} name={name} sample={sample} onSelect={handleSampleSelect} />
66-
))}
114+
<div className={style["samples-search"]}>
115+
<SearchBox
116+
placeholder="Search samples..."
117+
value={searchQuery}
118+
onChange={(_, data) => setSearchQuery(data.value)}
119+
className={style["search-input"]}
120+
/>
67121
</div>
122+
123+
{totalFiltered === 0 ? (
124+
<div className={style["samples-empty"]}>
125+
<Text>No samples match your search.</Text>
126+
</div>
127+
) : hasCategories ? (
128+
categories.map((category) => (
129+
<div key={category.name} className={style["samples-category"]}>
130+
<Text as="h3" weight="semibold" className={style["category-title"]}>
131+
{category.name}
132+
</Text>
133+
<div className={style["samples-grid"]}>
134+
{category.entries.map(([name, sample]) => (
135+
<SampleCard
136+
key={name}
137+
name={name}
138+
sample={sample}
139+
onSelect={handleSampleSelect}
140+
/>
141+
))}
142+
</div>
143+
</div>
144+
))
145+
) : (
146+
<div className={style["samples-grid"]}>
147+
{categories.flatMap((c) =>
148+
c.entries.map(([name, sample]) => (
149+
<SampleCard key={name} name={name} sample={sample} onSelect={handleSampleSelect} />
150+
)),
151+
)}
152+
</div>
153+
)}
68154
</DrawerBody>
69155
</OverlayDrawer>
70156
);

packages/playground/src/react/samples-drawer/samples-drawer.module.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
1+
.samples-search {
2+
padding: 0 0 16px 0;
3+
position: sticky;
4+
top: 0;
5+
z-index: 1;
6+
background: var(--colorNeutralBackground1);
7+
}
8+
9+
.search-input {
10+
width: 100%;
11+
}
12+
13+
.samples-category {
14+
margin-bottom: 24px;
15+
}
16+
17+
.category-title {
18+
font-size: var(--fontSizeBase400);
19+
margin: 0 0 12px 0;
20+
color: var(--colorNeutralForeground1);
21+
}
22+
23+
.samples-empty {
24+
display: flex;
25+
justify-content: center;
26+
padding: 48px 16px;
27+
color: var(--colorNeutralForeground3);
28+
}
29+
130
.samples-grid {
231
display: grid;
332
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));

packages/playground/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export interface PlaygroundSample {
1616
*/
1717
description?: string;
1818

19+
/**
20+
* Category for grouping samples in the sample gallery.
21+
*/
22+
category?: string;
23+
1924
/**
2025
* Compiler options for the sample.
2126
*/

0 commit comments

Comments
 (0)