mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
## What kind of change does this PR introduce? UI polish for the Connect sheet: clearer mode selection, wider sheet layout, and step/content chrome across Direct, Server, MCP, and shadcn flows. ## What is the current behavior? - Connect modes use a weak selected state and an awkward grid layout. - The sheet can jump width below the `lg` breakpoint when switching modes. - Direct connection chrome is noisy (reset in a footer, Title Case / mono pooler labels, mismatched copy-button sizes). - Several steps use admonitions or extra tips that repeat footer guidance. - Case-sensitive import of `InlineLink` breaks Linux/Vercel builds. ## What is the new behavior? ### Mode selector and sheet - Stronger selected/hover treatment; comfortable single row that wraps via `@container`. - Empty odd slots use a sunk placeholder cell. - Sheet uses `size="lg"` with `max-w-4xl` and `w-full min-w-0` so width stays stable when switching modes. ### Steps chrome - “Follow these steps” header with a copy-prompt action for coding agents. - Optional steps labelled `(optional)`. - Shared `CodeBlock` for install snippets; MCP feature groups preselect all except Storage. - Server / shadcn tips folded into footers; IPv4 add-on admonition is responsive with an inline Learn more link and a single Enable action. ### Direct connection - Connection string and connection parameters stay one step (same credentials, two formats). - Reset database password lives in the string card title row beside Shared/Dedicated pooler. - Card titles use sans + sentence case (`Shared pooler`, `Connection parameters`); `.env` stays mono. - Icon-only copy buttons match CodeBlock square sizing; row actions sit slightly closer to the right edge (`pr-2`). - Shared pooler toggle copy clarified. | Before | After | | --- | --- | | <img width="390" height="763" alt="API Keys Settings Chisel Toolshed Supabase" src="https://github.com/user-attachments/assets/adca3cc5-94f8-47e5-a4a2-2831790f430a" /> | <img width="390" height="763" alt="API Keys Settings Chisel Toolshed Supabase" src="https://github.com/user-attachments/assets/f03afe58-e654-435e-a821-835f6243ca95" /> | | <img width="1718" height="1323" alt="API Keys Settings Chisel Toolshed Supabase" src="https://github.com/user-attachments/assets/79f08620-7e1e-4246-a70f-801606c0f499" /> | <img width="1718" height="1323" alt="API Keys Settings Chisel Toolshed Supabase" src="https://github.com/user-attachments/assets/fb45e851-955e-46c2-90f1-afecb93d6ac4" /> | | <img width="1718" height="1323" alt="API Keys Settings Chisel Toolshed Supabase" src="https://github.com/user-attachments/assets/eda36d21-bba7-46ab-ad48-134acf93b471" /> | <img width="1718" height="1323" alt="API Keys Settings Chisel Toolshed Supabase" src="https://github.com/user-attachments/assets/b7b728c6-fc92-46a7-8e3f-2f182c56ece7" /> | ### Test plan - [ ] Open **Connect** and confirm mode cells select/hover clearly; narrow the sheet and confirm wrap + stable width. - [ ] Direct: switch Direct / Transaction / Session; confirm pooler title, reset in title row, parameters table, and percent-encode note. - [ ] Toggle IPv4 shared pooler on Transaction; confirm string updates and admonition/Learn more behaviour when on IPv4-only paths. - [ ] Server: `.env` Copy all / row copy sizing; install command copy. - [ ] MCP / shadcn / Framework: steps still resolve and copy prompt still builds a useful agent prompt. - [ ] Spot-check light/dark and a Linux/Vercel build (InlineLink import casing).
157 lines
5.1 KiB
TypeScript
157 lines
5.1 KiB
TypeScript
import { Check, Copy } from 'lucide-react'
|
|
import { useEffect, useState, type RefObject } from 'react'
|
|
import { copyToClipboard } from 'ui'
|
|
|
|
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
|
|
|
interface CopyPromptButtonProps {
|
|
stepsContainerRef: RefObject<HTMLDivElement | null>
|
|
/** When set, the Copy prompt button uses this verbatim instead of scraping the steps. */
|
|
customPrompt?: string
|
|
}
|
|
|
|
const normalizeTextLines = (value: string) => {
|
|
return value
|
|
.split('\n')
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
.join('\n')
|
|
}
|
|
|
|
const getStepTextContent = (contentElement: HTMLElement) => {
|
|
const clone = contentElement.cloneNode(true) as HTMLElement
|
|
clone
|
|
.querySelectorAll('pre, button, svg, input, textarea, select, [aria-hidden="true"]')
|
|
.forEach((element) => {
|
|
element.remove()
|
|
})
|
|
|
|
clone.querySelectorAll('p, div').forEach((el) => {
|
|
el.appendChild(document.createTextNode('\n'))
|
|
})
|
|
|
|
const text = clone.textContent ?? ''
|
|
return normalizeTextLines(text)
|
|
}
|
|
|
|
const getStepCodeSnippets = (contentElement: HTMLElement) => {
|
|
const snippets: Array<{ label: string; snippet: string }> = []
|
|
const seen = new Set<string>()
|
|
|
|
const addSnippet = (label: string, snippet: string) => {
|
|
if (!snippet || seen.has(snippet)) return
|
|
seen.add(snippet)
|
|
snippets.push({ label, snippet })
|
|
}
|
|
|
|
const getSnippet = (element: Element) => {
|
|
const copyValueElement = element.closest('[data-connect-copy-value]') as HTMLElement | null
|
|
return copyValueElement?.dataset.connectCopyValue?.trim() || element.textContent?.trim()
|
|
}
|
|
|
|
const tabContents = Array.from(
|
|
contentElement.querySelectorAll('[data-connect-tab-content]')
|
|
) as HTMLElement[]
|
|
|
|
tabContents.forEach((tabContent) => {
|
|
const label = tabContent.getAttribute('data-tab-label') || 'Code'
|
|
const tabSnippets = Array.from(tabContent.querySelectorAll('pre'))
|
|
.map(getSnippet)
|
|
.filter((snippet): snippet is string => Boolean(snippet))
|
|
|
|
if (tabSnippets.length === 0) {
|
|
const inlineSnippets = Array.from(tabContent.querySelectorAll('code'))
|
|
.filter((code) => !code.closest('pre') && code.closest('.font-mono'))
|
|
.map((code) => code.textContent?.trim())
|
|
.filter((snippet): snippet is string => Boolean(snippet))
|
|
inlineSnippets.forEach((snippet, index) => {
|
|
const inlineLabel = inlineSnippets.length > 1 ? `${label} (part ${index + 1})` : label
|
|
addSnippet(inlineLabel, snippet)
|
|
})
|
|
return
|
|
}
|
|
|
|
tabSnippets.forEach((snippet, index) => {
|
|
const tabLabel = tabSnippets.length > 1 ? `${label} (part ${index + 1})` : label
|
|
addSnippet(tabLabel, snippet)
|
|
})
|
|
})
|
|
|
|
contentElement.querySelectorAll('pre').forEach((pre) => {
|
|
if (pre.closest('[data-connect-tab-content]')) return
|
|
const snippet = getSnippet(pre)
|
|
if (snippet) addSnippet('Code', snippet)
|
|
})
|
|
|
|
contentElement.querySelectorAll('code').forEach((code) => {
|
|
if (code.closest('pre')) return
|
|
if (code.closest('[data-connect-tab-content]')) return
|
|
if (!code.closest('.font-mono')) return
|
|
const snippet = code.textContent?.trim()
|
|
if (snippet) addSnippet('Code', snippet)
|
|
})
|
|
|
|
return snippets
|
|
}
|
|
|
|
export const buildConnectPrompt = (stepsContainer: HTMLElement | null) => {
|
|
const stepElements = stepsContainer?.querySelectorAll('[data-connect-step]')
|
|
if (!stepElements?.length) return ''
|
|
|
|
const promptContent = Array.from(stepElements)
|
|
.map((stepElement, index) => {
|
|
const title = stepElement.getAttribute('data-step-title') ?? `Step ${index + 1}`
|
|
const description = stepElement.getAttribute('data-step-description') ?? ''
|
|
const contentElement = stepElement.querySelector('[data-step-content]') as HTMLElement | null
|
|
|
|
const details = contentElement ? getStepTextContent(contentElement) : ''
|
|
const codeSnippets = contentElement ? getStepCodeSnippets(contentElement) : []
|
|
|
|
const sections = [
|
|
`${index + 1}. ${title}`,
|
|
description,
|
|
details ? `Details:\n${details}` : null,
|
|
codeSnippets.length
|
|
? `Code:\n${codeSnippets
|
|
.map(({ label, snippet }) => `File: ${label}\n\`\`\`\n${snippet}\n\`\`\``)
|
|
.join('\n\n')}`
|
|
: null,
|
|
].filter(Boolean)
|
|
|
|
return sections.join('\n')
|
|
})
|
|
.join('\n\n')
|
|
|
|
return promptContent
|
|
}
|
|
|
|
export function CopyPromptButton({ stepsContainerRef, customPrompt }: CopyPromptButtonProps) {
|
|
const [showCopied, setShowCopied] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!showCopied) return
|
|
const timer = setTimeout(() => setShowCopied(false), 2000)
|
|
return () => clearTimeout(timer)
|
|
}, [showCopied])
|
|
|
|
return (
|
|
<ButtonTooltip
|
|
variant="default"
|
|
icon={showCopied ? <Check strokeWidth={2} className="text-brand" /> : <Copy />}
|
|
onClick={() => {
|
|
const textToCopy = customPrompt ?? buildConnectPrompt(stepsContainerRef.current)
|
|
setShowCopied(true)
|
|
copyToClipboard(textToCopy)
|
|
}}
|
|
tooltip={{
|
|
content: {
|
|
side: 'left',
|
|
text: 'Copy these steps for your coding agent',
|
|
},
|
|
}}
|
|
>
|
|
{showCopied ? 'Copied' : 'Copy prompt'}
|
|
</ButtonTooltip>
|
|
)
|
|
}
|