From b45ba41ebbfa3cf59b7ee3f84bac64be1de63cbf Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 30 Jan 2026 11:18:54 -0500 Subject: [PATCH 1/5] Sample picker as drawer --- packages/playground-website/e2e/ui.e2e.ts | 11 +- packages/playground-website/samples/build.js | 6 + .../editor-command-bar/editor-command-bar.tsx | 5 +- .../src/react/samples-drawer.module.css | 39 +++++++ .../playground/src/react/samples-drawer.tsx | 110 ++++++++++++++++++ packages/playground/src/tooling/index.ts | 1 + packages/playground/src/types.ts | 5 + 7 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 packages/playground/src/react/samples-drawer.module.css create mode 100644 packages/playground/src/react/samples-drawer.tsx diff --git a/packages/playground-website/e2e/ui.e2e.ts b/packages/playground-website/e2e/ui.e2e.ts index c5097d542db..fd754ca3971 100644 --- a/packages/playground-website/e2e/ui.e2e.ts +++ b/packages/playground-website/e2e/ui.e2e.ts @@ -8,8 +8,15 @@ test.describe("playground UI tests", () => { test("compiled http sample", async ({ page }) => { await page.goto(host); - const samplesDropDown = page.locator("_react=SamplesDropdown").locator("select"); - await samplesDropDown.selectOption({ label: "HTTP service" }); + + // Click the Samples button to open the drawer + const samplesButton = page.locator('button[aria-label="Browse samples"]'); + await samplesButton.click(); + + // Wait for the drawer to open and click on the HTTP service card + const httpServiceCard = page.locator("text=HTTP service").first(); + await httpServiceCard.click(); + const outputContainer = page.locator("_react=FileOutput"); await expect(outputContainer).toContainText(`title: Widget Service`); }); diff --git a/packages/playground-website/samples/build.js b/packages/playground-website/samples/build.js index 6ba443d8842..82c345d40b3 100644 --- a/packages/playground-website/samples/build.js +++ b/packages/playground-website/samples/build.js @@ -10,27 +10,33 @@ await buildSamples_experimental(packageRoot, resolve(__dirname, "dist/samples.ts "API versioning": { filename: "samples/versioning.tsp", preferredEmitter: "@typespec/openapi3", + description: "Learn how to version your API using TypeSpec's versioning library.", }, "Discriminated unions": { filename: "samples/unions.tsp", preferredEmitter: "@typespec/openapi3", + description: "Define discriminated unions for polymorphic types with different variants.", }, "HTTP service": { filename: "samples/http.tsp", preferredEmitter: "@typespec/openapi3", compilerOptions: { linterRuleSet: { extends: ["@typespec/http/all"] } }, + description: "Build an HTTP service with routes, parameters, and responses.", }, "REST framework": { filename: "samples/rest.tsp", preferredEmitter: "@typespec/openapi3", compilerOptions: { linterRuleSet: { extends: ["@typespec/http/all"] } }, + description: "Use the REST framework for resource-oriented API design patterns.", }, "Protobuf Kiosk": { filename: "samples/kiosk.tsp", preferredEmitter: "@typespec/protobuf", + description: "Generate Protocol Buffer definitions from TypeSpec models.", }, "Json Schema": { filename: "samples/json-schema.tsp", preferredEmitter: "@typespec/json-schema", + description: "Emit JSON Schema from TypeSpec type definitions.", }, }); diff --git a/packages/playground/src/editor-command-bar/editor-command-bar.tsx b/packages/playground/src/editor-command-bar/editor-command-bar.tsx index 05932be20ba..d608ebbf01b 100644 --- a/packages/playground/src/editor-command-bar/editor-command-bar.tsx +++ b/packages/playground/src/editor-command-bar/editor-command-bar.tsx @@ -3,7 +3,7 @@ import { Broom16Filled, Bug16Regular, Save16Regular } from "@fluentui/react-icon import type { CompilerOptions } from "@typespec/compiler"; import { useMemo, type FunctionComponent, type ReactNode } from "react"; import { EmitterDropdown } from "../react/emitter-dropdown.js"; -import { SamplesDropdown } from "../react/samples-dropdown.js"; +import { SamplesDrawerTrigger } from "../react/samples-drawer.js"; import { CompilerSettingsDialogButton } from "../react/settings/compiler-settings-dialog-button.js"; import type { BrowserHost, PlaygroundSample } from "../types.js"; import style from "./editor-command-bar.module.css"; @@ -68,9 +68,8 @@ export const EditorCommandBar: FunctionComponent = ({ {samples && ( <> -
diff --git a/packages/playground/src/react/samples-drawer.module.css b/packages/playground/src/react/samples-drawer.module.css new file mode 100644 index 00000000000..a229e006057 --- /dev/null +++ b/packages/playground/src/react/samples-drawer.module.css @@ -0,0 +1,39 @@ +.samples-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; + padding: 8px 0; +} + +.sample-card { + cursor: pointer; + padding: 16px; + transition: + box-shadow 0.2s ease, + border-color 0.2s ease; + min-height: 100px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.sample-card:hover { + box-shadow: var(--shadow8); +} + +.sample-card:focus-visible { + outline: 2px solid var(--colorBrandStroke1); + outline-offset: 2px; +} + +.sample-title { + font-size: var(--fontSizeBase400); + margin: 0; +} + +.sample-description { + font-size: var(--fontSizeBase200); + color: var(--colorNeutralForeground2); + margin: 0; + line-height: 1.4; +} diff --git a/packages/playground/src/react/samples-drawer.tsx b/packages/playground/src/react/samples-drawer.tsx new file mode 100644 index 00000000000..852c7f1eb85 --- /dev/null +++ b/packages/playground/src/react/samples-drawer.tsx @@ -0,0 +1,110 @@ +import { + Button, + Card, + DrawerBody, + DrawerHeader, + DrawerHeaderTitle, + OverlayDrawer, + Text, + ToolbarButton, + Tooltip, +} from "@fluentui/react-components"; +import { Dismiss24Regular, DocumentBulletList24Regular } from "@fluentui/react-icons"; +import { useCallback, useState, type FunctionComponent } from "react"; +import type { PlaygroundSample } from "../types.js"; +import style from "./samples-drawer.module.css"; + +export interface SamplesDrawerProps { + samples: Record; + onSelectedSampleNameChange: (sampleName: string) => void; +} + +export const SamplesDrawerTrigger: FunctionComponent = ({ + samples, + onSelectedSampleNameChange, +}) => { + const [isOpen, setIsOpen] = useState(false); + + const handleSampleSelect = useCallback( + (sampleName: string) => { + onSelectedSampleNameChange(sampleName); + setIsOpen(false); + }, + [onSelectedSampleNameChange], + ); + + return ( + <> + + } + onClick={() => setIsOpen(true)} + > + Samples + + + + setIsOpen(data.open)} + position="end" + size="large" + > + + } + onClick={() => setIsOpen(false)} + /> + } + > + Sample Gallery + + + +
+ {Object.entries(samples).map(([name, sample]) => ( + + ))} +
+
+
+ + ); +}; + +interface SampleCardProps { + name: string; + sample: PlaygroundSample; + onSelect: (name: string) => void; +} + +const SampleCard: FunctionComponent = ({ name, sample, onSelect }) => { + return ( + onSelect(name)} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onSelect(name); + } + }} + > + + {name} + + {sample.description && ( + + {sample.description} + + )} + + ); +}; diff --git a/packages/playground/src/tooling/index.ts b/packages/playground/src/tooling/index.ts index 8a1df2f1a8f..56b7a091012 100644 --- a/packages/playground/src/tooling/index.ts +++ b/packages/playground/src/tooling/index.ts @@ -20,6 +20,7 @@ export async function buildSamples_experimental( filename: config.filename, content, preferredEmitter: config.preferredEmitter, + description: config.description, compilerOptions: config.compilerOptions, }; } diff --git a/packages/playground/src/types.ts b/packages/playground/src/types.ts index 7b7dab483a2..f9431be5236 100644 --- a/packages/playground/src/types.ts +++ b/packages/playground/src/types.ts @@ -11,6 +11,11 @@ export interface PlaygroundSample { preferredEmitter?: string; content: string; + /** + * A short description of what this sample demonstrates. + */ + description?: string; + /** * Compiler options for the sample. */ From 35b2765798fbae6352154fb43631bcfe3aca9469 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 30 Jan 2026 08:24:44 -0800 Subject: [PATCH 2/5] Create playground-sample-drawer-2026-0-30-16-22-1.md --- .../changes/playground-sample-drawer-2026-0-30-16-22-1.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .chronus/changes/playground-sample-drawer-2026-0-30-16-22-1.md diff --git a/.chronus/changes/playground-sample-drawer-2026-0-30-16-22-1.md b/.chronus/changes/playground-sample-drawer-2026-0-30-16-22-1.md new file mode 100644 index 00000000000..a20ca1d1ff7 --- /dev/null +++ b/.chronus/changes/playground-sample-drawer-2026-0-30-16-22-1.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: feature +packages: + - "@typespec/playground" +--- + +Update sample dropdown to a drawer opening a sample gallery From b8964175f64e443f11cc70609e71dbc8bc4ce255 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 30 Jan 2026 11:34:02 -0500 Subject: [PATCH 3/5] generated icon --- .../src/react/samples-drawer.module.css | 42 +++++- .../playground/src/react/samples-drawer.tsx | 123 ++++++++++++++++-- 2 files changed, 153 insertions(+), 12 deletions(-) diff --git a/packages/playground/src/react/samples-drawer.module.css b/packages/playground/src/react/samples-drawer.module.css index a229e006057..c59e99d33ab 100644 --- a/packages/playground/src/react/samples-drawer.module.css +++ b/packages/playground/src/react/samples-drawer.module.css @@ -12,9 +12,6 @@ box-shadow 0.2s ease, border-color 0.2s ease; min-height: 100px; - display: flex; - flex-direction: column; - gap: 8px; } .sample-card:hover { @@ -26,6 +23,45 @@ outline-offset: 2px; } +.sample-card-content { + display: flex; + gap: 16px; + align-items: flex-start; +} + +.sample-card-text { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + min-width: 0; +} + +.sample-icon { + width: 48px; + height: 48px; + border-radius: 8px; + flex-shrink: 0; + position: relative; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.sample-icon-pattern { + position: absolute; + top: 0; + left: 0; +} + +.sample-icon-initials { + position: relative; + font-size: 16px; + font-weight: 600; + z-index: 1; +} + .sample-title { font-size: var(--fontSizeBase400); margin: 0; diff --git a/packages/playground/src/react/samples-drawer.tsx b/packages/playground/src/react/samples-drawer.tsx index 852c7f1eb85..b308172f5d0 100644 --- a/packages/playground/src/react/samples-drawer.tsx +++ b/packages/playground/src/react/samples-drawer.tsx @@ -10,10 +10,110 @@ import { Tooltip, } from "@fluentui/react-components"; import { Dismiss24Regular, DocumentBulletList24Regular } from "@fluentui/react-icons"; -import { useCallback, useState, type FunctionComponent } from "react"; +import { useCallback, useMemo, useState, type FunctionComponent } from "react"; import type { PlaygroundSample } from "../types.js"; import style from "./samples-drawer.module.css"; +/** Generate a deterministic hash from a string */ +function hashString(str: string): number { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash); +} + +/** Color palette for sample icons - using pleasant, distinct colors */ +const iconColors = [ + { bg: "#e3f2fd", fg: "#1565c0" }, // Blue + { bg: "#f3e5f5", fg: "#7b1fa2" }, // Purple + { bg: "#e8f5e9", fg: "#2e7d32" }, // Green + { bg: "#fff3e0", fg: "#e65100" }, // Orange + { bg: "#fce4ec", fg: "#c2185b" }, // Pink + { bg: "#e0f7fa", fg: "#00838f" }, // Cyan + { bg: "#fff8e1", fg: "#f9a825" }, // Amber + { bg: "#efebe9", fg: "#4e342e" }, // Brown + { bg: "#e8eaf6", fg: "#3949ab" }, // Indigo + { bg: "#e0f2f1", fg: "#00695c" }, // Teal +]; + +/** Simple geometric patterns for variety */ +type PatternType = "circle" | "squares" | "triangle" | "hexagon" | "diamond"; +const patterns: PatternType[] = ["circle", "squares", "triangle", "hexagon", "diamond"]; + +interface SampleIconProps { + name: string; +} + +const SampleIcon: FunctionComponent = ({ name }) => { + const { color, pattern, initials } = useMemo(() => { + const hash = hashString(name); + const colorIndex = hash % iconColors.length; + const patternIndex = (hash >> 4) % patterns.length; + // Get first letter of first two words, or first two letters + const words = name.split(/\s+/); + const init = + words.length >= 2 + ? (words[0][0] + words[1][0]).toUpperCase() + : name.slice(0, 2).toUpperCase(); + return { + color: iconColors[colorIndex], + pattern: patterns[patternIndex], + initials: init, + }; + }, [name]); + + const renderPattern = () => { + const size = 48; + const half = size / 2; + + switch (pattern) { + case "circle": + return ; + case "squares": + return ( + <> + + + + ); + case "triangle": + return ( + + ); + case "hexagon": + return ( + + ); + case "diamond": + return ( + + ); + } + }; + + return ( + + ); +}; + export interface SamplesDrawerProps { samples: Record; onSelectedSampleNameChange: (sampleName: string) => void; @@ -97,14 +197,19 @@ const SampleCard: FunctionComponent = ({ name, sample, onSelect } }} > - - {name} - - {sample.description && ( - - {sample.description} - - )} +
+ +
+ + {name} + + {sample.description && ( + + {sample.description} + + )} +
+
); }; From e527cb43230debecc5096202c145f43edf5629ad Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 30 Jan 2026 11:40:32 -0500 Subject: [PATCH 4/5] use fluentui colors --- .../src/react/samples-drawer.module.css | 1 + .../playground/src/react/samples-drawer.tsx | 55 ++++++++++--------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/packages/playground/src/react/samples-drawer.module.css b/packages/playground/src/react/samples-drawer.module.css index c59e99d33ab..bfdf344f597 100644 --- a/packages/playground/src/react/samples-drawer.module.css +++ b/packages/playground/src/react/samples-drawer.module.css @@ -47,6 +47,7 @@ align-items: center; justify-content: center; overflow: hidden; + user-select: none; } .sample-icon-pattern { diff --git a/packages/playground/src/react/samples-drawer.tsx b/packages/playground/src/react/samples-drawer.tsx index b308172f5d0..08a201e4be2 100644 --- a/packages/playground/src/react/samples-drawer.tsx +++ b/packages/playground/src/react/samples-drawer.tsx @@ -6,6 +6,7 @@ import { DrawerHeaderTitle, OverlayDrawer, Text, + tokens, ToolbarButton, Tooltip, } from "@fluentui/react-components"; @@ -25,18 +26,18 @@ function hashString(str: string): number { return Math.abs(hash); } -/** Color palette for sample icons - using pleasant, distinct colors */ +/** Color palette using FluentUI tokens - using Background2 for light bg */ const iconColors = [ - { bg: "#e3f2fd", fg: "#1565c0" }, // Blue - { bg: "#f3e5f5", fg: "#7b1fa2" }, // Purple - { bg: "#e8f5e9", fg: "#2e7d32" }, // Green - { bg: "#fff3e0", fg: "#e65100" }, // Orange - { bg: "#fce4ec", fg: "#c2185b" }, // Pink - { bg: "#e0f7fa", fg: "#00838f" }, // Cyan - { bg: "#fff8e1", fg: "#f9a825" }, // Amber - { bg: "#efebe9", fg: "#4e342e" }, // Brown - { bg: "#e8eaf6", fg: "#3949ab" }, // Indigo - { bg: "#e0f2f1", fg: "#00695c" }, // Teal + { bg: tokens.colorPaletteBlueBackground2, fg: tokens.colorPaletteBlueForeground2 }, + { bg: tokens.colorPaletteGrapeBackground2, fg: tokens.colorPaletteGrapeForeground2 }, + { bg: tokens.colorPaletteForestBackground2, fg: tokens.colorPaletteForestForeground2 }, + { bg: tokens.colorPalettePumpkinBackground2, fg: tokens.colorPalettePumpkinForeground2 }, + { bg: tokens.colorPaletteMagentaBackground2, fg: tokens.colorPaletteMagentaForeground2 }, + { bg: tokens.colorPaletteTealBackground2, fg: tokens.colorPaletteTealForeground2 }, + { bg: tokens.colorPaletteGoldBackground2, fg: tokens.colorPaletteGoldForeground2 }, + { bg: tokens.colorPalettePlumBackground2, fg: tokens.colorPalettePlumForeground2 }, + { bg: tokens.colorPaletteLavenderBackground2, fg: tokens.colorPaletteLavenderForeground2 }, + { bg: tokens.colorPaletteSteelBackground2, fg: tokens.colorPaletteSteelForeground2 }, ]; /** Simple geometric patterns for variety */ @@ -48,7 +49,7 @@ interface SampleIconProps { } const SampleIcon: FunctionComponent = ({ name }) => { - const { color, pattern, initials } = useMemo(() => { + const { colors, pattern, initials } = useMemo(() => { const hash = hashString(name); const colorIndex = hash % iconColors.length; const patternIndex = (hash >> 4) % patterns.length; @@ -59,7 +60,7 @@ const SampleIcon: FunctionComponent = ({ name }) => { ? (words[0][0] + words[1][0]).toUpperCase() : name.slice(0, 2).toUpperCase(); return { - color: iconColors[colorIndex], + colors: iconColors[colorIndex], pattern: patterns[patternIndex], initials: init, }; @@ -71,43 +72,47 @@ const SampleIcon: FunctionComponent = ({ name }) => { switch (pattern) { case "circle": - return ; + return ; case "squares": return ( <> - - + + ); case "triangle": return ( - + ); case "hexagon": return ( ); case "diamond": return ( - + ); } }; return ( - ); }; - -export interface SamplesDrawerProps { - samples: Record; - onSelectedSampleNameChange: (sampleName: string) => void; -} - -export const SamplesDrawerTrigger: FunctionComponent = ({ - samples, - onSelectedSampleNameChange, -}) => { - const [isOpen, setIsOpen] = useState(false); - - const handleSampleSelect = useCallback( - (sampleName: string) => { - onSelectedSampleNameChange(sampleName); - setIsOpen(false); - }, - [onSelectedSampleNameChange], - ); - - return ( - <> - - } - onClick={() => setIsOpen(true)} - > - Samples - - - - setIsOpen(data.open)} - position="end" - size="large" - > - - } - onClick={() => setIsOpen(false)} - /> - } - > - Sample Gallery - - - -
- {Object.entries(samples).map(([name, sample]) => ( - - ))} -
-
-
- - ); -}; - -interface SampleCardProps { - name: string; - sample: PlaygroundSample; - onSelect: (name: string) => void; -} - -const SampleCard: FunctionComponent = ({ name, sample, onSelect }) => { - return ( - onSelect(name)} - role="button" - tabIndex={0} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onSelect(name); - } - }} - > -
- -
- - {name} - - {sample.description && ( - - {sample.description} - - )} -
-
-
- ); -}; diff --git a/packages/playground/src/react/samples-drawer/samples-drawer-trigger.tsx b/packages/playground/src/react/samples-drawer/samples-drawer-trigger.tsx new file mode 100644 index 00000000000..68db54f0b37 --- /dev/null +++ b/packages/playground/src/react/samples-drawer/samples-drawer-trigger.tsx @@ -0,0 +1,77 @@ +import { + Button, + DrawerBody, + DrawerHeader, + DrawerHeaderTitle, + OverlayDrawer, + ToolbarButton, + Tooltip, +} from "@fluentui/react-components"; +import { Dismiss24Regular, DocumentBulletList24Regular } from "@fluentui/react-icons"; +import { useCallback, useState, type FunctionComponent } from "react"; +import type { PlaygroundSample } from "../../types.js"; +import { SampleCard } from "./sample-card.js"; +import style from "./samples-drawer.module.css"; + +export interface SamplesDrawerProps { + samples: Record; + onSelectedSampleNameChange: (sampleName: string) => void; +} + +export const SamplesDrawerTrigger: FunctionComponent = ({ + samples, + onSelectedSampleNameChange, +}) => { + const [isOpen, setIsOpen] = useState(false); + + const handleSampleSelect = useCallback( + (sampleName: string) => { + onSelectedSampleNameChange(sampleName); + setIsOpen(false); + }, + [onSelectedSampleNameChange], + ); + + return ( + <> + + } + onClick={() => setIsOpen(true)} + > + Samples + + + + setIsOpen(data.open)} + position="end" + size="large" + > + + } + onClick={() => setIsOpen(false)} + /> + } + > + Sample Gallery + + + +
+ {Object.entries(samples).map(([name, sample]) => ( + + ))} +
+
+
+ + ); +}; diff --git a/packages/playground/src/react/samples-drawer.module.css b/packages/playground/src/react/samples-drawer/samples-drawer.module.css similarity index 100% rename from packages/playground/src/react/samples-drawer.module.css rename to packages/playground/src/react/samples-drawer/samples-drawer.module.css diff --git a/packages/playground/src/react/samples-dropdown.tsx b/packages/playground/src/react/samples-dropdown.tsx deleted file mode 100644 index dade744f3e9..00000000000 --- a/packages/playground/src/react/samples-dropdown.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Select } from "@fluentui/react-components"; -import { useCallback, type FunctionComponent } from "react"; -import type { PlaygroundSample } from "../types.js"; - -export interface SamplesDropdownProps { - samples: Record; - selectedSampleName: string; - onSelectedSampleNameChange: (sampleName: string) => void; -} - -export const SamplesDropdown: FunctionComponent = ({ - samples, - selectedSampleName, - onSelectedSampleNameChange, -}) => { - const options = Object.keys(samples).map((sample) => { - return ; - }); - - const handleSelected = useCallback( - (evt: any) => { - if (samples[evt.target.value]) { - onSelectedSampleNameChange(evt.target.value); - } - }, - [onSelectedSampleNameChange, samples], - ); - return ( - - ); -};