mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## What kind of change does this PR introduce? Refactor. Follow-up to #49871. ## What is the current behavior? Branded (green) text still uses the `text-brand` classname while the colour comes from `--primary`. ## What is the new behavior? **Rename-only:** `text-brand` → `text-primary` across callsites and docs. Leftover `bg-brand` / `border-brand` alias to `brand-default`. No intentional colour changes in this PR. This better matches how we treat our green in other components and props, like `variant="primary"` for green buttons. ## To test On light mode: smoke-test that branded text still looks like #49871 (readable green, not the bright fill). - [Homepage](https://zone-www-dot-com-git-dnywh-depr-316-text-brand-de2380-supabase.vercel.app/): “Scale to millions” uses `text-primary` - [Docs homepage](https://docs-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/docs): branded links like “More on self-hosting” are still readable - [Typography docs](https://design-system-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/design-system/docs/typography): documents `text-primary` - [Colour usage](https://design-system-git-dnywh-depr-316-text-brand-to-primary-supabase.vercel.app/design-system/docs/color-usage): `text-primary` is visibly darker than `bg-brand-default` _on light mode_ - [Studio auth providers](https://studio-staging-git-dnywh-depr-316-text-brand-to-60fa6c-supabase.vercel.app/dashboard/project/_/auth/providers): enabled provider badge text readable; status dot stays bright green <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Style** * Updated branded text, link hover states, icons, badges, indicators, and highlighted content across the Design System, Docs, Studio, Learn, UI Library, and marketing experiences to use the primary theme color. * Updated syntax highlighting and table-of-contents styling for consistent primary-color presentation. * Refined brand color fallback behavior for bright fills and borders. * **Documentation** * Updated color-usage and typography guidance to recommend the primary text utility. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
163 lines
5.3 KiB
TypeScript
163 lines
5.3 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"], [data-connect-prompt-ignore]'
|
|
)
|
|
.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) => {
|
|
if (element.closest('[data-connect-prompt-ignore]')) return undefined
|
|
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(getSnippet)
|
|
.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 = getSnippet(code)
|
|
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
|
|
icon={showCopied ? <Check strokeWidth={2} className="text-primary" /> : <Copy />}
|
|
onClick={() => {
|
|
const textToCopy = customPrompt ?? buildConnectPrompt(stepsContainerRef.current)
|
|
copyToClipboard(textToCopy, () => setShowCopied(true))
|
|
}}
|
|
tooltip={{
|
|
content: {
|
|
side: 'left',
|
|
text: 'Copy these steps for your coding agent',
|
|
},
|
|
}}
|
|
>
|
|
{showCopied ? 'Copied' : 'Copy prompt'}
|
|
</ButtonTooltip>
|
|
<span className="sr-only" role="status" aria-live="polite">
|
|
{showCopied ? 'Copied' : ''}
|
|
</span>
|
|
</>
|
|
)
|
|
}
|