feat(docs): agent prompt block (#50565)

## What kind of change does this PR introduce?

docs ui polish for the prompt panel on the docs homepage and in the
quickstarts

## What is the current behavior?

agent prompt renders as one run on paragraph steps read as "1. ... 2.
... 3. ..." inline making it hard to read

## What is the new behavior?

- sets prompt bodies as markdown for easier readability
- adds long prompts collapse
- removes panel tab icons
- uses panel code block ui
- makes active tab has an underline on first paint
 
 `home`
| state | preview |
| -------|------|
| before | <img width="709" height="344" alt="image"
src="https://github.com/user-attachments/assets/ba3a0948-efff-4144-8eba-9065e1f140d9"
/> |
| after | <img width="709" height="344" alt="image"
src="https://github.com/user-attachments/assets/e63e71e9-86d2-4d9a-ac2d-925e197d69f9"
/> |

`quickstart`
| state | preview |
| -------|------|
| before | <img width="862" height="344" alt="image"
src="https://github.com/user-attachments/assets/96faf012-a9f1-4ee9-8a98-3ec6de24d2de"
/> |
| after | <img width="862" height="344" alt="image"
src="https://github.com/user-attachments/assets/7a41a259-e181-4a32-949f-de095d8bb66c"
/> |

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Prompts now support Markdown formatting, inline code chips, expandable
content, and improved hover-reveal behavior.
* Code-copy controls support customizable labels and clearer
screen-reader announcements.
* Prompt tabs and panels have updated styling, spacing, and
active-content handling.

* **UI Improvements**
* Setup and agent prompts now use consistent Markdown-rendered
presentation.
  * Prompt headings now display “Agent Prompt.”
  * Home page setup code blocks use a more compact layout.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Anthony Lio authored and GitHub committed 2026-09-18 21:54:31 +03:00
1 parent 986931184c
commit 5918a69398
9 files changed
+140 -115

No files matched your search

@@ -6,4 +6,8 @@ export const setupCommand = {
export const setupCommands = [setupCommand.installCli, setupCommand.installPlugin].join('\n')
export const setupPrompt = `Help me get set up with Supabase. Do the following: 1. Install the Supabase CLI as a project dev dependency with \`${setupCommand.installCli}\`, so the version is pinned per project. 2. Install the Supabase Plugin with \`${setupCommand.installPlugin}\`. 3. Review my project and determine whether Supabase is already initialized. If it is not initialized, run \`${setupCommand.initialize}\`. 4. Suggest the most relevant next steps.`
export const setupPrompt = `Help me get set up with Supabase. Do the following:
1. Install the Supabase CLI as a project dev dependency with \`${setupCommand.installCli}\`, so the version is pinned per project.
2. Install the Supabase Plugin with \`${setupCommand.installPlugin}\`.
3. Review my project and determine whether Supabase is already initialized. If it is not initialized, run \`${setupCommand.initialize}\`.
4. Suggest the most relevant next steps.`
+8 -22
View File
@@ -4,45 +4,31 @@ import {
Prompt,
PromptContent,
PromptCopy,
PromptMarkdown,
PromptPanel,
PromptTitle,
} from '~/features/ui/PromptPanel'
import { isFeatureEnabled } from 'common'
import { Sparkles, Terminal } from 'lucide-react'
import { type ReactNode } from 'react'
import { getCustomContent } from '../lib/custom-content/getCustomContent'
import DocsCoverLogo from './DocsCoverLogo'
import { setupCommand, setupCommands, setupPrompt } from './HomePageCover.constants'
import { setupCommands, setupPrompt } from './HomePageCover.constants'
const fullGettingStartedEnabled = isFeatureEnabled('docs:full_getting_started')
function SetupPrompt({ cliCode }: { cliCode: ReactNode }) {
return (
<PromptPanel>
<Prompt value="prompt">
<PromptTitle icon={<Sparkles />}>AI Prompt</PromptTitle>
<Prompt value="prompt" expandable>
<PromptTitle>Agent Prompt</PromptTitle>
<PromptCopy>{setupPrompt}</PromptCopy>
<PromptContent>
Help me get set up with Supabase. Do the following: 1. Install the Supabase CLI as a
project dev dependency with{' '}
<code className="shimmer-none rounded bg-surface-200 px-1 py-0.5 font-mono text-xs text-foreground">
{setupCommand.installCli}
</code>
, so the version is pinned per project. 2. Install the Supabase Plugin with{' '}
<code className="shimmer-none rounded bg-surface-200 px-1 py-0.5 font-mono text-xs text-foreground">
{setupCommand.installPlugin}
</code>
. 3. Review my project and determine whether Supabase is already initialized. If it is not
initialized, run{' '}
<code className="shimmer-none rounded bg-surface-200 px-1 py-0.5 font-mono text-xs text-foreground">
{setupCommand.initialize}
</code>
. 4. Suggest the most relevant next steps.
<PromptMarkdown>{setupPrompt}</PromptMarkdown>
</PromptContent>
</Prompt>
<Prompt value="cli">
<PromptTitle icon={<Terminal />}>CLI</PromptTitle>
<PromptTitle>CLI</PromptTitle>
<PromptCopy>{setupCommands}</PromptCopy>
<PromptContent shimmer={false}>{cliCode}</PromptContent>
</Prompt>
@@ -56,8 +42,8 @@ const HomePageCover = ({ title, cliCode }: { title: string; cliCode: ReactNode }
return (
<div className="w-full border-b bg-muted/10">
<div className="mx-auto max-w-7xl px-6 py-16">
<div className="flex flex-col gap-10 lg:flex-row lg:items-center lg:gap-12 xl:gap-16">
<div className="flex w-full min-w-0 flex-1 items-center gap-4 sm:gap-8">
<div className="flex flex-col gap-10 lg:flex-row lg:items-start lg:gap-12 xl:gap-16">
<div className="flex w-full min-w-0 flex-1 items-center gap-4 sm:gap-8 mt-10">
<DocsCoverLogo aria-hidden="true" className="w-12 shrink-0 sm:w-[60px] md:w-[100px]" />
<div className="flex min-w-0 flex-col">
<h1 className="m-0 text-3xl text-foreground sm:text-4xl">
+2 -5
View File
@@ -12,6 +12,7 @@ import ReactMarkdown from 'react-markdown'
import { ConnectionIcon } from 'ui-patterns/McpUrlBuilder'
import { AiPrompt } from './AiPrompt'
import { PromptCode } from './PromptPanel'
import { TabPanel, Tabs } from './Tabs'
type AgentSetupProps = {
@@ -33,11 +34,7 @@ const markdownComponents = {
</a>
)
},
code: ({ children }: { children?: ReactNode }) => (
<code className="rounded bg-surface-200 px-1 py-0.5 font-mono text-xs text-foreground">
{children}
</code>
),
code: PromptCode,
}
function HarnessBody({ harness }: { harness: MonitoringAgentHarnessSetup }) {
+12 -4
View File
@@ -1,9 +1,15 @@
'use client'
import { aiPrompts, type AiPromptId } from '~/data/ai-prompts.data'
import { Sparkles } from 'lucide-react'
import { Prompt, PromptContent, PromptCopy, PromptPanel, PromptTitle } from './PromptPanel'
import {
Prompt,
PromptContent,
PromptCopy,
PromptMarkdown,
PromptPanel,
PromptTitle,
} from './PromptPanel'
type AiPromptProps = {
/** Looks up prompt text from `aiPrompts`. */
@@ -30,9 +36,11 @@ function AiPrompt({ id }: AiPromptProps) {
return (
<PromptPanel>
<Prompt value="prompt" expandable>
<PromptTitle icon={<Sparkles />}>AI Prompt</PromptTitle>
<PromptTitle>Agent Prompt</PromptTitle>
<PromptCopy>{prompt}</PromptCopy>
<PromptContent>{prompt}</PromptContent>
<PromptContent>
<PromptMarkdown>{prompt}</PromptMarkdown>
</PromptContent>
</Prompt>
</PromptPanel>
)
@@ -180,7 +180,17 @@ function CrossfadeIcon({
)
}
export function CodeCopyButton({ className, content }: { className?: string; content: string }) {
export function CodeCopyButton({
className,
content,
label = 'Copy code',
copiedLabel = 'Code copied',
}: {
className?: string
content: string
label?: string
copiedLabel?: string
}) {
const [copied, setCopied] = useState(false)
useEffect(() => {
@@ -203,7 +213,7 @@ export function CodeCopyButton({ className, content }: { className?: string; con
return (
<>
<span className="sr-only" aria-live="polite">
{copied ? 'Code copied' : ''}
{copied ? copiedLabel : ''}
</span>
<Tooltip>
<TooltipTrigger asChild>
@@ -218,14 +228,14 @@ export function CodeCopyButton({ className, content }: { className?: string; con
'hover:bg-[var(--btn-active)]',
className
)}
aria-label="Copy code"
aria-label={label}
// Tooltip repeats the label; the description would read the name twice
aria-describedby={undefined}
>
<CrossfadeIcon active={copied} activeIcon={Check} inactiveIcon={Copy} />
</Button>
</TooltipTrigger>
<TooltipContent>Copy code</TooltipContent>
<TooltipContent>{label}</TooltipContent>
</Tooltip>
</>
)
+95 -76
View File
@@ -1,21 +1,15 @@
'use client'
import { Check, Copy } from 'lucide-react'
import {
Children,
isValidElement,
useEffect,
useId,
useState,
type ReactElement,
type ReactNode,
} from 'react'
import { ChevronDown } from 'lucide-react'
import { Children, isValidElement, useId, useState, type ReactElement, type ReactNode } from 'react'
import ReactMarkdown from 'react-markdown'
// shadcn tabs from packages/ui/src/components/shadcn/ui/tabs.tsx
import { cn, copyToClipboard, Tabs, TabsContent, TabsIndicator, TabsList, TabsTrigger } from 'ui'
import { cn, Tabs, TabsContent, TabsIndicator, TabsList, TabsTrigger } from 'ui'
import { CodeCopyButton } from './CodeBlock/CodeBlock.client'
type PromptTitleProps = {
children: ReactNode
icon?: ReactNode
}
type PromptCopyProps = {
@@ -44,7 +38,6 @@ type PromptPanelProps = {
type CollectedPrompt = {
value: string
title: ReactNode
icon?: ReactNode
copyValue: string
content: ReactNode
expandable: boolean
@@ -82,16 +75,13 @@ function isElementOfType(child: ReactNode, type: { displayName?: string }) {
function collectPrompt(prompt: ReactElement<PromptProps>, index: number): CollectedPrompt {
let title: ReactNode = `Prompt ${index + 1}`
let icon: ReactNode | undefined
let copyValue = ''
let content: ReactNode = null
let shimmer = true
Children.forEach(prompt.props.children, (child) => {
if (isElementOfType(child, PromptTitle)) {
const props = (child as ReactElement<PromptTitleProps>).props
title = props.children
icon = props.icon
title = (child as ReactElement<PromptTitleProps>).props.children
} else if (isElementOfType(child, PromptCopy)) {
copyValue = (child as ReactElement<PromptCopyProps>).props.children
} else if (isElementOfType(child, PromptContent)) {
@@ -104,7 +94,6 @@ function collectPrompt(prompt: ReactElement<PromptProps>, index: number): Collec
return {
value: prompt.props.value ?? `prompt-${index}`,
title,
icon,
copyValue,
content,
expandable: Boolean(prompt.props.expandable),
@@ -120,24 +109,59 @@ function collectPrompts(children: ReactNode): CollectedPrompt[] {
function ExpandableContent({ children }: { children: ReactNode }) {
const [isExpanded, setIsExpanded] = useState(false)
const [isHoverRevealPaused, setIsHoverRevealPaused] = useState(false)
const handleReelPointerLeave = () => setIsHoverRevealPaused(false)
const handleToggle = () => {
if (isExpanded) setIsHoverRevealPaused(true)
setIsExpanded((expanded) => !expanded)
}
return (
<div>
<div className="relative">
<div className={cn(!isExpanded && 'max-h-28 overflow-hidden')}>{children}</div>
{!isExpanded && (
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-12 bg-gradient-to-t from-background to-transparent" />
<div
className="group/expand relative h-full px-4 py-3.5"
onPointerLeave={handleReelPointerLeave}
>
<div
className={cn(
'overflow-hidden [interpolate-size:allow-keywords] transition-[max-height] duration-300 ease-out motion-reduce:transition-none',
isExpanded ? 'max-h-max' : 'max-h-28'
)}
</div>
<button
tabIndex={0}
type="button"
onClick={() => setIsExpanded((expanded) => !expanded)}
className="mt-2 cursor-pointer text-sm text-brand-link transition-colors hover:text-foreground focus-ring"
aria-expanded={isExpanded}
>
{isExpanded ? 'Show less' : 'Show more'}
</button>
{children}
</div>
<div
className={cn(
'pointer-events-none absolute inset-x-4 bottom-3.5 h-16 bg-gradient-to-t from-background-200 to-transparent',
'transition-opacity duration-300 ease-out motion-reduce:transition-none',
isExpanded && 'opacity-0'
)}
/>
<div
className={cn('flex justify-center', isExpanded ? 'mt-3' : 'absolute inset-x-0 bottom-3.5')}
>
<button
key={String(isExpanded)}
tabIndex={0}
type="button"
onClick={handleToggle}
className={cn(
'inline-flex cursor-pointer items-center gap-1.5 rounded-full pl-2.5 pr-3 py-1 text-xs text-foreground',
'bg-surface-200/90 shadow-sm ring-1 ring-foreground/10 backdrop-blur-sm',
'transition-[opacity,translate,background-color,scale] duration-200 ease-out',
'hover:bg-surface-300 active:scale-[0.96] focus-ring',
!isExpanded &&
'translate-y-1 opacity-0 focus-visible:translate-y-0 focus-visible:opacity-100 pointer-coarse:translate-y-0 pointer-coarse:opacity-100 motion-reduce:translate-y-0',
!isExpanded &&
!isHoverRevealPaused &&
'group-hover/expand:translate-y-0 group-hover/expand:opacity-100'
)}
aria-expanded={isExpanded}
>
<ChevronDown aria-hidden="true" size={14} className={cn(isExpanded && 'rotate-180')} />
{isExpanded ? 'Show less' : 'Show more'}
</button>
</div>
</div>
)
}
@@ -167,54 +191,42 @@ function PromptBody({
)
return (
<div className="px-4 py-3.5 text-sm leading-6 text-foreground-light font-normal">
<div
className={cn(
'h-full bg-200 text-sm leading-6 text-foreground-light font-normal',
!prompt.expandable && 'px-4 py-3.5'
)}
>
{prompt.expandable ? <ExpandableContent>{content}</ExpandableContent> : content}
</div>
)
}
function CopyButton({ label, value }: { label: string; value: string }) {
const [copied, setCopied] = useState(false)
useEffect(() => {
if (!copied) return
const timeout = window.setTimeout(() => setCopied(false), 2000)
return () => window.clearTimeout(timeout)
}, [copied])
/** Renders a Markdown prompt string as the panel body, with backticks as code chips. */
function PromptMarkdown({ children }: { children: string }) {
return (
<button
tabIndex={0}
type="button"
onClick={() => {
copyToClipboard(value, () => {
setCopied(true)
})
}}
className="cursor-pointer rounded-sm p-1.5 text-foreground-muted transition-colors hover:bg-surface-200 hover:text-foreground focus-ring"
aria-label={copied ? `${label} copied` : `Copy ${label}`}
title={copied ? 'Copied' : 'Copy to clipboard'}
>
{copied ? <Check size={14} /> : <Copy size={14} />}
</button>
<div className="not-prose flex flex-col gap-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_ul]:list-disc [&_ul]:pl-5 [&_li+li]:mt-1">
<ReactMarkdown components={MARKDOWN_COMPONENTS}>{children}</ReactMarkdown>
</div>
)
}
function TabLabel({ icon, children }: { icon?: ReactNode; children: ReactNode }) {
/** Inline code chip for prompt bodies; opts out of the text shimmer. */
function PromptCode({ children }: { children?: ReactNode }) {
return (
<span className="inline-flex items-center gap-1.5">
{icon && (
<span aria-hidden="true" className="text-tertiary-foreground [&_svg]:size-3.5">
{icon}
</span>
)}
<code className="shimmer-none rounded bg-surface-200 px-1 py-0.5 font-mono text-xs text-foreground">
{children}
</span>
</code>
)
}
const tabTriggerClassName = 'h-full px-0 py-0 text-xs shadow-none data-[state=active]:shadow-none'
const MARKDOWN_COMPONENTS = { code: PromptCode }
const tabTriggerClassName = cn(
'h-full px-0 py-0 text-xs shadow-none data-[state=active]:shadow-none',
'group-has-[[data-tab-indicator]]/list:border-b',
'group-data-[tab-indicator-ready]/list:data-[state=active]:border-b-transparent'
)
/**
* Copyable prompt card. Compose one or more `<Prompt>` children; multiple
@@ -225,7 +237,7 @@ const tabTriggerClassName = 'h-full px-0 py-0 text-xs shadow-none data-[state=ac
* ```tsx
* <PromptPanel>
* <Prompt value="prompt" expandable>
* <PromptTitle icon={<Sparkles />}>AI Prompt</PromptTitle>
* <PromptTitle>Agent Prompt</PromptTitle>
* <PromptCopy>Plain text copied to the clipboard</PromptCopy>
* <PromptContent>Rich content shown in the panel</PromptContent>
* </Prompt>
@@ -245,13 +257,14 @@ function PromptPanel({ children, className }: PromptPanelProps) {
const hasTabs = prompts.length > 1
const dismissShimmer = () => setShimmerEnabled(false)
const copyTarget = typeof activePrompt.title === 'string' ? activePrompt.title : 'content'
const header = (
<div className="flex h-11 items-center justify-between border-b bg-surface-75 px-4">
<div className="flex h-11 items-center justify-between pl-4 pr-2 shadow-[inset_0_-1px_0_0_var(--border-default)] [--btn-active:color-mix(in_srgb,var(--foreground)_4%,var(--background-200))]">
{hasTabs ? (
<TabsList className="h-full gap-5 border-0 [--tab-track:transparent]">
{prompts.map((prompt) => (
<TabsTrigger key={prompt.value} value={prompt.value} className={tabTriggerClassName}>
<TabLabel icon={prompt.icon}>{prompt.title}</TabLabel>
{prompt.title}
</TabsTrigger>
))}
<TabsIndicator />
@@ -264,13 +277,14 @@ function PromptPanel({ children, className }: PromptPanelProps) {
tabTriggerClassName
)}
>
<TabLabel icon={prompts[0].icon}>{prompts[0].title}</TabLabel>
{prompts[0].title}
</span>
)}
<CopyButton
<CodeCopyButton
key={activePrompt.value}
label={typeof activePrompt.title === 'string' ? activePrompt.title : 'content'}
value={activePrompt.copyValue}
content={activePrompt.copyValue}
label={`Copy ${copyTarget}`}
copiedLabel={`${copyTarget} copied`}
/>
</div>
)
@@ -283,7 +297,7 @@ function PromptPanel({ children, className }: PromptPanelProps) {
onFocusCapture={dismissShimmer}
onPointerEnter={dismissShimmer}
className={cn(
'w-full overflow-hidden rounded-lg border bg-background shadow-sm',
'w-full overflow-hidden rounded-lg border border-default shadow-codeblock',
className
)}
>
@@ -301,7 +315,10 @@ function PromptPanel({ children, className }: PromptPanelProps) {
activationMode="manual"
onFocusCapture={dismissShimmer}
onPointerEnter={dismissShimmer}
className={cn('w-full overflow-hidden rounded-lg border bg-background shadow-sm', className)}
className={cn(
'w-full overflow-hidden rounded-lg border border-default shadow-codeblock',
className
)}
>
{header}
{/* Stack panes in one grid cell so the panel keeps the tallest tab's height. */}
@@ -314,6 +331,8 @@ function PromptPanel({ children, className }: PromptPanelProps) {
key={prompt.value}
value={prompt.value}
forceMount
// pane's own controls remain reachable
tabIndex={-1}
inert={!isActive}
aria-hidden={!isActive}
className="col-start-1 row-start-1 m-0 data-[state=inactive]:invisible data-[state=inactive]:pointer-events-none"
@@ -327,5 +346,5 @@ function PromptPanel({ children, className }: PromptPanelProps) {
)
}
export { Prompt, PromptContent, PromptCopy, PromptPanel, PromptTitle }
export { Prompt, PromptCode, PromptContent, PromptCopy, PromptMarkdown, PromptPanel, PromptTitle }
export type { PromptContentProps, PromptCopyProps, PromptPanelProps, PromptProps, PromptTitleProps }
@@ -14,7 +14,7 @@ describe('AiPrompt markdown schema', () => {
props: { id: 'nextjs', includeInMarkdown: true },
})
expect(markdown).toContain('**AI Prompt**')
expect(markdown).toContain('**Agent Prompt**')
expect(markdown).toContain('Help me add Supabase to my Next.js project.')
expect(markdown).toContain('```text')
})
@@ -17,5 +17,5 @@ export function AiPrompt({ props }: HandlerContext): string {
throw new Error(`Unknown AiPrompt id: ${id}`)
}
return `**AI Prompt**\n\n${toMarkdown({ type: 'code', lang: 'text', value: prompt }).trimEnd()}`
return `**Agent Prompt**\n\n${toMarkdown({ type: 'code', lang: 'text', value: prompt }).trimEnd()}`
}
+2 -1
View File
@@ -18,7 +18,8 @@ const HomeLayout = ({ children }: PropsWithChildren) => {
lang="bash"
lineNumbers={false}
hideControls
className="!-mx-4 !-my-3.5 !border-0 !rounded-none !bg-transparent [&_.code-content]:!px-4 [&_.code-content]:!py-3.5"
compact
className="-mx-4 w-auto bg-transparent [&_.code-content]:px-4 [&_.code-content]:py-0"
/>
}
/>