mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 18:11:51 +08:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix / polish ## What is the current behavior? Some docs interactive controls (copy buttons, tab triggers) do not show a pointer cursor on hover, so they feel less clickable than surrounding links. Called out on https://github.com/supabase/supabase/pull/48318. ## What is the new behavior? Adds `cursor-pointer` in one sweep so related controls stay consistent: - Shared `TabsTrigger` in `packages/ui` (covers PromptPanel AI Prompt / CLI tabs, including the unselected tab) - PromptPanel copy button and Show more / Show less - Guides sidebar “Copy as Markdown” - Docs code block copy button ## To test Hover the controls below and confirm the cursor is `pointer` on both selected and unselected tabs, and on copy buttons. ### Docs Preview: https://docs-git-dnywh-docs-cursor-pointer-supabase.vercel.app/docs - Homepage setup prompt: [AI Prompt / CLI tabs + copy](https://docs-git-dnywh-docs-cursor-pointer-supabase.vercel.app/docs) - Quickstart prompt: [Show more / Show less + copy](https://docs-git-dnywh-docs-cursor-pointer-supabase.vercel.app/docs/guides/getting-started/quickstarts/nextjs) - Guide sidebar + code block: [Copy as Markdown + code copy](https://docs-git-dnywh-docs-cursor-pointer-supabase.vercel.app/docs/guides/database/tables) ### Design system Preview: https://design-system-git-dnywh-docs-cursor-pointer-supabase.vercel.app/design-system - Shared tabs demo: [Account / Password triggers](https://design-system-git-dnywh-docs-cursor-pointer-supabase.vercel.app/design-system/docs/components/tabs) ### UI library Preview: https://ui-library-git-dnywh-docs-cursor-pointer-supabase.vercel.app/ui - Install command package-manager tabs (npm / pnpm / yarn / bun) + copy: [Password-based auth](https://ui-library-git-dnywh-docs-cursor-pointer-supabase.vercel.app/ui/docs/nextjs/password-based-auth) ### Studio (staging) Preview: https://studio-staging-git-dnywh-docs-cursor-pointer-supabase.vercel.app - Auth user panel: open a project → Authentication → Users → select a user → hover Overview / Logs (and related) tabs - Connect sheet: open Connect on a project → hover the install method tabs ### Studio (self-hosted) Preview: https://studio-self-hosted-git-dnywh-docs-cursor-pointer-supabase.vercel.app - Same `TabsTrigger` callsites as Studio staging (user panel / Connect sheet) ### WWW Preview: https://zone-www-dot-com-git-dnywh-docs-cursor-pointer-supabase.vercel.app - Blog chart tabs (`PGChart` → shared `TabsTrigger`): [Latency / Number of results / Average latency / Raw data](https://zone-www-dot-com-git-dnywh-docs-cursor-pointer-supabase.vercel.app/blog/postgres-full-text-search-vs-the-rest) (scroll to the Results section) ## Additional context Split out from #48318 so the homepage prompt polish stays focused. Prefer fixing the shared tab trigger rather than only the PromptPanel copy button, otherwise the copy control would show pointer while an unselected CLI tab would not. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Style** * Improved hover feedback across the docs UI by adding a pointer cursor to “Copy as Markdown,” code block copy and word-wrap controls, prompt copy buttons, and tab selectors. * Updated cursor styling consistently so interactive controls better communicate clickability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
182 lines
5.0 KiB
TypeScript
182 lines
5.0 KiB
TypeScript
'use client'
|
|
|
|
import { ArrowRightFromLine, Check, Copy, WrapText } from 'lucide-react'
|
|
import { useCallback, useEffect, useRef, useState, type MouseEvent } from 'react'
|
|
import { type ThemedToken } from 'shiki'
|
|
import { type NodeHover } from 'twoslash'
|
|
import { cn, copyToClipboard, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
|
|
|
import { getFontStyle } from './CodeBlock.utils'
|
|
|
|
export function AnnotatedSpan({
|
|
token,
|
|
annotations,
|
|
}: {
|
|
token: ThemedToken
|
|
annotations: Array<NodeHover>
|
|
}) {
|
|
const [open, setOpen] = useState(false)
|
|
|
|
const [isTouchDevice, setIsTouchDevice] = useState(false)
|
|
useEffect(() => {
|
|
const touchDevice = !window.matchMedia('(pointer: fine)').matches
|
|
setIsTouchDevice(touchDevice)
|
|
}, [])
|
|
|
|
const handleClick = useCallback(
|
|
(evt: MouseEvent) => {
|
|
if (isTouchDevice) {
|
|
evt.preventDefault()
|
|
evt.stopPropagation()
|
|
setOpen((open) => !open)
|
|
}
|
|
},
|
|
[isTouchDevice]
|
|
)
|
|
const onOpenChange = useCallback(
|
|
(open: boolean) => {
|
|
if (!isTouchDevice || !open) {
|
|
setOpen(open)
|
|
}
|
|
},
|
|
[isTouchDevice]
|
|
)
|
|
|
|
return (
|
|
<Tooltip open={open} onOpenChange={onOpenChange}>
|
|
<TooltipTrigger asChild onClick={handleClick}>
|
|
<button
|
|
tabIndex={0}
|
|
style={token.htmlStyle}
|
|
className={cn(
|
|
isTouchDevice &&
|
|
'underline underline-offset-4 decoration-dashed decoration-[rgba(from_currentColor_r_g_b/0.5)]'
|
|
)}
|
|
>
|
|
{token.content}
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent className="max-w-[min(80vw,400px)] p-0 divide-y">
|
|
{annotations.map((annotation, idx) => (
|
|
<Annotation key={idx} annotation={annotation} />
|
|
))}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
function Annotation({ annotation }: { annotation: NodeHover }) {
|
|
const { text, docs, tags } = annotation
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
<code className={cn('block bg-200 p-2', (docs || tags) && 'border-b border-default')}>
|
|
{text}
|
|
</code>
|
|
{docs && <p className={cn('p-2', tags && 'border-b border-default')}>{docs}</p>}
|
|
{tags && (
|
|
<div className="p-2 flex flex-col">
|
|
{tags.map((tag, idx) => {
|
|
return (
|
|
<span key={idx}>
|
|
<code>@{tag[0]}</code> {tag[1]}
|
|
</span>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function CodeCopyButton({ className, content }: { className?: string; content: string }) {
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
const handleCopy = async () => {
|
|
copyToClipboard(content, () => {
|
|
setCopied(true)
|
|
})
|
|
}
|
|
|
|
const resetStatus = () => {
|
|
setCopied(false)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<span className="sr-only" aria-live="polite">
|
|
{copied ? 'Code copied' : ''}
|
|
</span>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
tabIndex={0}
|
|
onClick={handleCopy}
|
|
onBlur={resetStatus}
|
|
className={cn(
|
|
'cursor-pointer border rounded-md p-1',
|
|
copied && 'bg-selection',
|
|
'hover:bg-selection transition',
|
|
className
|
|
)}
|
|
aria-label="Copy code"
|
|
>
|
|
{copied ? (
|
|
<Check size={14} className="text-lighter" />
|
|
) : (
|
|
<Copy size={14} className="text-lighter" />
|
|
)}
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>Copy code</TooltipContent>
|
|
</Tooltip>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export function CodeBlockControls({ content }: { content: string }) {
|
|
const [isWrapped, setIsWrapped] = useState(false)
|
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
|
|
|
const toggleWrap = useCallback(() => {
|
|
setIsWrapped((prev) => {
|
|
const newValue = !prev
|
|
// Find the parent code block and toggle the wrap data attribute
|
|
const codeBlock = wrapperRef.current?.closest('.shiki')
|
|
if (codeBlock) {
|
|
if (newValue) {
|
|
codeBlock.setAttribute('data-wrapped', 'true')
|
|
} else {
|
|
codeBlock.removeAttribute('data-wrapped')
|
|
}
|
|
}
|
|
return newValue
|
|
})
|
|
}, [])
|
|
|
|
return (
|
|
<div
|
|
ref={wrapperRef}
|
|
className="opacity-0 flex group-hover:opacity-100 focus-within:opacity-100 absolute top-2 right-2 gap-1"
|
|
>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
tabIndex={0}
|
|
onClick={toggleWrap}
|
|
className={cn('cursor-pointer border rounded-md p-1', 'hover:bg-selection transition')}
|
|
aria-label={isWrapped ? 'Disable word wrap' : 'Enable word wrap'}
|
|
>
|
|
{isWrapped ? (
|
|
<ArrowRightFromLine size={14} className="text-lighter" />
|
|
) : (
|
|
<WrapText size={14} className="text-lighter" />
|
|
)}
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{isWrapped ? 'Disable word wrap' : 'Enable word wrap'}</TooltipContent>
|
|
</Tooltip>
|
|
<CodeCopyButton content={content} />
|
|
</div>
|
|
)
|
|
}
|