mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 02:45:07 +03:00
The page scan stops at the article wrapper, so nothing checked the top nav, sidebar nav, table-of-contents sidebar, breadcrumbs, or footer. A violation in any of those lands on every docs page. Adds a global-elements Playwright project that scans the document with the article excluded, across six pages, one per layout, at two viewports, plus one pass with the mobile menu open. It runs from a docs_components paths filter in the existing workflow, so a content-only pull request never triggers it. Rules that pass today are enforced, so they catch a newly introduced violation without failing on existing debt. Everything else annotates. Supersedes #48909, which predated e2e/shared.
168 lines
5.1 KiB
TypeScript
168 lines
5.1 KiB
TypeScript
'use client'
|
|
|
|
import { Feedback } from '~/components/Feedback'
|
|
import { useSendTelemetryEvent } from '~/lib/telemetry'
|
|
import { isFeatureEnabled } from 'common'
|
|
import { Chatgpt, Claude } from 'icons'
|
|
import { Check, Copy, Sparkles } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { usePathname } from 'next/navigation'
|
|
import { useState } from 'react'
|
|
import { cn } from 'ui'
|
|
import { ExpandableVideo } from 'ui-patterns/ExpandableVideo'
|
|
import { Toc, TOCItems, TOCScrollArea } from 'ui-patterns/Toc'
|
|
|
|
import { useTocAnchors } from '../features/docs/GuidesMdx.state'
|
|
|
|
interface TOCHeader {
|
|
id?: string
|
|
text: string
|
|
link: string
|
|
level: number
|
|
}
|
|
|
|
function AiTools({ className }: { className?: string }) {
|
|
const [copied, setCopied] = useState(false)
|
|
const path = usePathname()
|
|
const sendTelemetryEvent = useSendTelemetryEvent()
|
|
|
|
function handleAgentSetupClick() {
|
|
sendTelemetryEvent({ action: 'agent_setup_clicked' })
|
|
}
|
|
|
|
async function copyMarkdown() {
|
|
const mdUrl = `/docs/${path}.md`
|
|
|
|
try {
|
|
const res = await fetch(mdUrl)
|
|
let text: string
|
|
|
|
if (res.ok) {
|
|
text = await res.text()
|
|
} else {
|
|
// Default to HTML content within the article when no .md file is available.
|
|
text = document.getElementById('sb-docs-guide-main-article')?.innerHTML ?? ''
|
|
}
|
|
|
|
await navigator.clipboard.writeText(text)
|
|
setCopied(true)
|
|
setTimeout(() => setCopied(false), 2000)
|
|
} catch (error) {
|
|
console.error('Failed to copy markdown', error)
|
|
}
|
|
|
|
sendTelemetryEvent({
|
|
action: 'copy_as_markdown_clicked',
|
|
})
|
|
}
|
|
|
|
return (
|
|
<section className={cn(className)} aria-labelledby="ai-tools-title">
|
|
<h3
|
|
id="ai-tools-title"
|
|
className="block font-mono uppercase text-xs text-foreground-light mb-3"
|
|
>
|
|
AI Tools
|
|
</h3>
|
|
<div className="flex flex-col gap-2">
|
|
<Link
|
|
href="/guides/ai-tools"
|
|
onClick={handleAgentSetupClick}
|
|
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground transition-colors"
|
|
>
|
|
<Sparkles size={14} strokeWidth={1.5} />
|
|
Connect your AI agent
|
|
</Link>
|
|
<button
|
|
tabIndex={0}
|
|
onClick={copyMarkdown}
|
|
className="flex cursor-pointer items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground text-left transition-colors"
|
|
>
|
|
{copied ? (
|
|
<Check size={14} strokeWidth={1.5} className="text-brand" />
|
|
) : (
|
|
<Copy size={14} strokeWidth={1.5} />
|
|
)}
|
|
{copied ? 'Copied!' : 'Copy as Markdown'}
|
|
</button>
|
|
<a
|
|
href={`https://chatgpt.com/?hint=search&q=Read from https://supabase.com/docs${path} so I can ask questions about its contents`}
|
|
target="_blank"
|
|
onClick={() =>
|
|
sendTelemetryEvent({ action: 'ask_ai_clicked', properties: { agent: 'chatgpt' } })
|
|
}
|
|
rel="noreferrer noopener"
|
|
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground transition-colors"
|
|
>
|
|
<Chatgpt size={14} />
|
|
Ask ChatGPT
|
|
</a>
|
|
<a
|
|
href={`https://claude.ai/new?q=Read from https://supabase.com/docs${path} so I can ask questions about its contents`}
|
|
target="_blank"
|
|
onClick={() =>
|
|
sendTelemetryEvent({ action: 'ask_ai_clicked', properties: { agent: 'claude' } })
|
|
}
|
|
rel="noreferrer noopener"
|
|
className="flex items-center gap-1.5 text-xs text-foreground-lighter hover:text-foreground transition-colors"
|
|
>
|
|
<Claude size={14} />
|
|
Ask Claude
|
|
</a>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
const GuidesSidebar = ({
|
|
className,
|
|
video,
|
|
hideToc,
|
|
}: {
|
|
className?: string
|
|
video?: string
|
|
hideToc?: boolean
|
|
}) => {
|
|
const pathname = usePathname()
|
|
const { toc } = useTocAnchors()
|
|
const showFeedback = isFeatureEnabled('feedback:docs')
|
|
const tocVideoPreview = `https://img.youtube.com/vi/${video}/0.jpg`
|
|
|
|
return (
|
|
<div
|
|
data-testid="sb-docs-toc-sidebar"
|
|
className={cn('thin-scrollbar overflow-y-auto h-fit', 'px-px', className)}
|
|
>
|
|
<div className="w-full relative border-l flex flex-col gap-6 lg:gap-8 px-2 h-fit">
|
|
{video && (
|
|
<div className="relative pl-5">
|
|
<ExpandableVideo imgUrl={tocVideoPreview} videoId={video} />
|
|
</div>
|
|
)}
|
|
{showFeedback && (
|
|
<div className="pl-5">
|
|
<Feedback key={pathname} />
|
|
</div>
|
|
)}
|
|
<div className="pl-5">
|
|
<AiTools key={pathname} />
|
|
</div>
|
|
{!hideToc && toc.length !== 0 && (
|
|
<Toc className="-ml-[calc(0.25rem+6px)]">
|
|
<h3 className="inline-flex items-center gap-1.5 font-mono text-xs uppercase text-foreground pl-[calc(1.5rem+6px)]">
|
|
On this page
|
|
</h3>
|
|
<TOCScrollArea>
|
|
<TOCItems items={toc} />
|
|
</TOCScrollArea>
|
|
</Toc>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default GuidesSidebar
|
|
export { GuidesSidebar }
|
|
export type { TOCHeader }
|