This commit is contained in:
Saxon Fletcher committed 2026-09-11 11:29:59 +10:00
1 parent f6730bfd4b
commit d0767cd76e
8 files changed
+194 -138

No files matched your search

+2 -6
View File
@@ -1,6 +1,6 @@
'use client'
import { CircleIcon, LaptopIcon, MoonIcon, SunIcon } from 'lucide-react'
import { LaptopIcon, MoonIcon, SunIcon } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/navigation'
import * as React from 'react'
@@ -79,7 +79,7 @@ export function CommandMenu({ ...props }: DialogProps) {
Find guides, blocks, starter apps, and theme settings.
</DialogDescription>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandList className="min-h-0 max-h-[min(400px,calc(100dvh-8rem))] overscroll-contain [&_[cmdk-group]]:p-2! [&_[cmdk-item]]:rounded-md [&_[cmdk-item]]:p-2!">
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup key="pages" heading="Pages">
{gettingStarted.items.map((navItem) => (
@@ -88,9 +88,6 @@ export function CommandMenu({ ...props }: DialogProps) {
value={navItem.title}
onSelect={() => runCommand(() => router.push(navItem.href as string))}
>
<div className="mr-2 flex h-4 w-4 items-center justify-center">
<CircleIcon className="h-3 w-3" strokeWidth={1} />
</div>
{navItem.title}
</CommandItem>
))}
@@ -111,7 +108,6 @@ export function CommandMenu({ ...props }: DialogProps) {
})
}
>
<CircleIcon className="mr-2 h-3 w-3" strokeWidth={1} />
{block.title}
</CommandItem>
))}
@@ -46,7 +46,7 @@ export function CopyDocPrompt({ title, pagePath, command }: CopyDocPromptProps)
}
onClick={copyPrompt}
>
{status === 'copied' ? 'Prompt copied' : 'Copy prompt'}
{status === 'copied' ? 'Prompt copied' : 'Copy agent prompt'}
</Button>
<span
role="status"
@@ -1,13 +1,12 @@
'use client'
import { CommandCopyButton } from './command-copy-button'
import { TerminalCode } from './terminal-code'
export function InstallCommand({ command }: { command: string }) {
return (
<div className="flex items-start gap-3 rounded-lg border bg-surface-75 p-4">
<pre className="min-w-0 flex-1 overflow-x-auto whitespace-pre-wrap break-words font-mono text-xs leading-6">
<code>{command}</code>
</pre>
<TerminalCode command={command} />
<CommandCopyButton command={command} />
</div>
)
@@ -4,6 +4,7 @@ import { Check, Copy } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ui'
import { TerminalCode } from './terminal-code'
import type { PackageManager } from '@/lib/install-command'
interface InstallationPanelProps {
@@ -72,9 +73,7 @@ export function InstallationPanel({ title, pagePath, commands }: InstallationPan
</div>
{packageManagers.map((manager) => (
<TabsContent key={manager} value={manager} className="m-0 p-5">
<pre className="min-w-0 flex-1 overflow-x-auto whitespace-pre-wrap break-words font-mono text-xs leading-6 text-foreground">
<code>{commands[manager]}</code>
</pre>
<TerminalCode command={commands[manager] ?? ''} />
</TabsContent>
))}
<TabsContent value="prompt" className="m-0 p-5">
+106 -124
View File
@@ -1,10 +1,9 @@
'use client'
import supabaseLogoIcon from 'common/assets/images/supabase-logo-icon.svg'
import { ArrowUpRight, ChevronRight, Code2 } from 'lucide-react'
import { ArrowUpRight, ChevronRight, Code2, Grid2X2 } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useState } from 'react'
import {
Button,
@@ -16,7 +15,6 @@ import {
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
Tabs,
TabsContent,
TabsList,
@@ -28,21 +26,23 @@ import { Sidebar } from '@/components/sidebar'
import { ThemeSwitcherDropdown } from '@/components/theme-switcher-dropdown'
import { getLibraryBlockHref, libraryBlocks, libraryCategories } from '@/config/library'
import { useFramework } from '@/context/framework-context'
import { useMenuAim } from '@/hooks/use-menu-aim'
export function SiteHeader() {
const pathname = usePathname()
const { framework } = useFramework()
const [activeCategory, setActiveCategory] = useState<string>(libraryCategories[0].name)
const menuAim = useMenuAim(activeCategory, setActiveCategory)
return (
<header className="sticky top-0 z-40 border-b bg-background/90 backdrop-blur-lg">
<header className="sticky top-0 z-40 before:pointer-events-none before:absolute before:inset-x-0 before:top-0 before:h-24 before:bg-gradient-to-b before:from-background before:via-background/90 before:via-60% before:to-transparent">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:bg-background focus:p-3"
>
Skip to content
</a>
<div className="mx-auto flex h-14 max-w-site items-center justify-between gap-4 px-4 md:px-8">
<div className="relative mx-auto flex h-14 max-w-site items-center justify-between gap-4 px-4 md:px-8">
<div className="flex items-center gap-4 lg:gap-8">
<Link
href="/"
@@ -50,46 +50,48 @@ export function SiteHeader() {
aria-label="Supabase Library home"
>
<Image src={supabaseLogoIcon} alt="" width={24} height={24} />
<span aria-hidden="true" className="hidden text-foreground-lighter sm:inline">
<span aria-hidden="true" className="hidden text-border sm:inline">
/
</span>
<span className="hidden text-foreground-light sm:inline">Library</span>
</Link>
<nav aria-label="Main navigation">
<NavigationMenu
delayDuration={0}
className="hidden h-14 md:flex md:flex-none"
viewportClassName="rounded-xl bg-background data-[state=open]:slide-in-from-right-0!"
>
<NavigationMenuList className="flex-none gap-1">
<NavigationMenuItem>
<NavigationMenuTrigger
className={cn(
buttonVariants({ variant: 'text', size: 'small' }),
'bg-transparent! hover:text-brand-link data-open:text-brand-link! focus-ring focus-visible:text-foreground px-2 h-auto',
pathname === '/' && 'text-foreground'
)}
</div>
<nav aria-label="Main navigation" className="absolute left-1/2 -translate-x-1/2">
<NavigationMenu
delayDuration={0}
className="hidden h-14 md:flex md:flex-none [&>div:last-child]:left-1/2 [&>div:last-child]:-translate-x-1/2"
viewportClassName="rounded-xl bg-background data-[state=open]:slide-in-from-right-0!"
>
<NavigationMenuList className="flex-none gap-1">
<NavigationMenuItem>
<NavigationMenuTrigger
className={cn(
buttonVariants({ variant: 'text', size: 'small' }),
'h-8 rounded-full px-3 text-foreground-light hover:text-foreground data-[state=open]:text-foreground'
)}
>
<Grid2X2 aria-hidden="true" className="size-4 text-muted-foreground" />
<span className="font-medium text-foreground">Explore</span>
</NavigationMenuTrigger>
<NavigationMenuContent>
<Tabs
value={activeCategory}
onValueChange={menuAim.selectCategory}
onPointerMove={menuAim.onPointerMove}
onPointerLeave={menuAim.onPointerLeave}
orientation="vertical"
className="flex w-[640px] max-w-[calc(100vw-4rem)] max-h-[calc(100dvh-5rem)] overflow-y-auto"
>
Explore
</NavigationMenuTrigger>
<NavigationMenuContent>
<Tabs
value={activeCategory}
onValueChange={setActiveCategory}
orientation="vertical"
className="flex w-[640px] max-w-[calc(100vw-4rem)] max-h-[calc(100dvh-5rem)] overflow-y-auto"
>
<div className="flex w-60 shrink-0 flex-col border-r bg-surface-75 py-2">
<TabsList
aria-label="Block categories"
className="h-auto w-60 shrink-0 flex-col items-stretch justify-start border-b-0 border-r bg-surface-75 py-2"
className="h-auto flex-col items-stretch justify-start border-b-0"
>
{libraryCategories.map((category) => (
<TabsTrigger
key={category.name}
value={category.name}
onPointerEnter={(event) => {
if (event.pointerType === 'mouse') setActiveCategory(category.name)
}}
data-menu-category={category.name}
className="justify-between gap-3 whitespace-normal border-0 px-4 py-2.5 text-left data-[state=active]:bg-surface-200 data-[state=active]:shadow-none"
>
{category.name}
@@ -99,96 +101,76 @@ export function SiteHeader() {
/>
</TabsTrigger>
))}
<NavigationMenuLink asChild>
<Link
href="/"
className="mx-4 mt-5 border-t pt-4 text-xs text-foreground-light hover:text-foreground"
>
Browse all blocks
</Link>
</NavigationMenuLink>
<NavigationMenuLink asChild>
<Link
href="/docs/getting-started/introduction"
className="px-4 py-3 text-xs text-foreground-light hover:text-foreground"
>
Introduction
</Link>
</NavigationMenuLink>
</TabsList>
{libraryCategories.map((category) => {
const blocks = libraryBlocks.filter(
(block) => block.category === category.name
)
return (
<TabsContent
key={category.name}
value={category.name}
className="m-0 min-w-0 flex-1 p-6"
>
{blocks.length > 0 ? (
<ul className="flex flex-col gap-4">
{blocks.map((block) => (
<li key={block.slug}>
<NavigationMenuLink asChild>
<Link
href={getLibraryBlockHref(block, framework)}
className="group/menu-item flex h-fit flex-col gap-1 rounded-md text-sm text-foreground-light hover:text-foreground select-none leading-none no-underline focus-ring focus-visible:text-foreground"
>
<span className="flex items-center gap-1">
<span className="text-sm leading-snug text-foreground">
{block.title}
</span>
<ChevronRight
strokeWidth={2}
className="h-3 w-3 text-foreground transition-all -translate-x-1 opacity-0 group-hover/menu-item:translate-x-0 group-hover/menu-item:opacity-100"
/>
<ul className="mx-4 mt-5 flex flex-col border-t pt-2.5">
{[
{ href: '/', label: 'Browse all blocks' },
{ href: '/docs/getting-started/quickstart', label: 'Quickstart' },
{ href: '/docs/getting-started/faq', label: 'FAQ' },
].map(({ href, label }) => (
<li key={href}>
<NavigationMenuLink asChild>
<Link
href={href}
className="block py-1.5 text-xs text-foreground-light hover:text-foreground"
>
{label}
</Link>
</NavigationMenuLink>
</li>
))}
</ul>
</div>
{libraryCategories.map((category) => {
const blocks = libraryBlocks.filter(
(block) => block.category === category.name
)
return (
<TabsContent
key={category.name}
value={category.name}
className="m-0 min-w-0 flex-1 p-6"
>
{blocks.length > 0 ? (
<ul className="flex flex-col gap-4">
{blocks.map((block) => (
<li key={block.slug}>
<NavigationMenuLink asChild>
<Link
href={getLibraryBlockHref(block, framework)}
className="group/menu-item flex h-fit flex-col gap-1 rounded-md text-sm text-foreground-light hover:text-foreground select-none leading-none no-underline focus-ring focus-visible:text-foreground"
>
<span className="flex items-center gap-1">
<span className="text-sm leading-snug text-foreground">
{block.title}
</span>
<span className="text-xs leading-snug text-foreground-lighter group-hover/menu-item:text-foreground-light group-focus-visible/menu-item:text-foreground-light">
{block.description}
</span>
</Link>
</NavigationMenuLink>
</li>
))}
</ul>
) : (
<p className="text-base text-foreground-light">
No blocks in this category yet.
</p>
)}
</TabsContent>
)
})}
</Tabs>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
asChild
className={cn(
navigationMenuTriggerStyle(),
'h-9 bg-transparent px-3 text-sm text-foreground-light hover:bg-transparent hover:text-foreground'
)}
>
<Link href="/docs/getting-started/quickstart">Quickstart</Link>
</NavigationMenuLink>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
asChild
className={cn(
navigationMenuTriggerStyle(),
'h-9 bg-transparent px-3 text-sm text-foreground-light hover:bg-transparent hover:text-foreground'
)}
>
<Link href="/docs/getting-started/faq">FAQ</Link>
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</nav>
</div>
<ChevronRight
strokeWidth={2}
className="h-3 w-3 text-foreground transition-all -translate-x-1 opacity-0 group-hover/menu-item:translate-x-0 group-hover/menu-item:opacity-100"
/>
</span>
<span className="text-xs leading-snug text-foreground-lighter group-hover/menu-item:text-foreground-light group-focus-visible/menu-item:text-foreground-light">
{block.description}
</span>
</Link>
</NavigationMenuLink>
</li>
))}
</ul>
) : (
<p className="text-base text-foreground-light">
No blocks in this category yet.
</p>
)}
</TabsContent>
)
})}
</Tabs>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</nav>
<div className="flex items-center gap-2">
<div className="w-20 sm:w-28 lg:w-36 xl:w-44">
<CommandMenu />
@@ -197,7 +179,7 @@ export function SiteHeader() {
asChild
variant="text"
size="tiny"
className="hidden px-2 sm:inline-flex"
className="hidden h-8 w-8 p-0 sm:inline-flex"
aria-label="View source on GitHub"
>
<a href="https://github.com/supabase/supabase/tree/master/apps/ui-library">
@@ -208,7 +190,7 @@ export function SiteHeader() {
asChild
variant="text"
size="small"
className="hidden px-3 text-sm sm:inline-flex"
className="hidden h-8 px-3 text-sm sm:inline-flex"
iconRight={<ArrowUpRight size={14} />}
>
<a href="https://supabase.com/docs">Docs</a>
@@ -0,0 +1,21 @@
'use client'
import dynamic from 'next/dynamic'
const CodeBlock = dynamic(() => import('ui-patterns/CodeBlock').then((module) => module.CodeBlock))
export function TerminalCode({ command }: { command: string }) {
return (
<CodeBlock
language="bash"
hideCopy
hideLineNumbers
wrapLongLines
focusable={false}
wrapperClassName="min-w-0 max-w-none flex-1"
className="w-full max-w-none rounded-none border-0 bg-transparent! p-0 font-mono text-xs! leading-6! text-foreground!"
>
{command}
</CodeBlock>
)
}
@@ -58,7 +58,7 @@ const ThemeSwitcherDropdown = () => {
variant="text"
size="tiny"
aria-label="Choose a theme"
className="px-1 group"
className="group h-8 w-8 p-0"
icon={
resolvedTheme?.includes('light') ? (
<Sun className={iconClasses} />
+59
View File
@@ -0,0 +1,59 @@
'use client'
import { useEffect, useRef, type PointerEvent } from 'react'
// Keep the selected category while the pointer travels toward its content panel.
export function useMenuAim(activeCategory: string, onCategoryChange: (category: string) => void) {
const previousPoint = useRef<{ x: number; y: number } | null>(null)
const timeout = useRef<ReturnType<typeof setTimeout> | null>(null)
function cancelPending() {
if (timeout.current !== null) clearTimeout(timeout.current)
timeout.current = null
}
function reset() {
cancelPending()
previousPoint.current = null
}
useEffect(() => {
return () => {
if (timeout.current !== null) clearTimeout(timeout.current)
}
}, [])
function onPointerMove(event: PointerEvent<HTMLDivElement>) {
if (event.pointerType !== 'mouse') return
const previous = previousPoint.current
const point = { x: event.clientX, y: event.clientY }
previousPoint.current = point
const target = event.target instanceof Element ? event.target : null
const tab = target?.closest<HTMLElement>('[data-menu-category]')
const category = tab?.dataset.menuCategory
cancelPending()
if (!category || category === activeCategory) return
const list = tab?.closest('[role="tablist"]')?.getBoundingClientRect()
const panel = event.currentTarget.getBoundingClientRect()
if (previous && list && previous.x < point.x && point.x < list.right) {
const progress = (point.x - previous.x) / (list.right - previous.x)
const top = previous.y + (panel.top - previous.y) * progress
const bottom = previous.y + (panel.bottom - previous.y) * progress
if (point.y >= top && point.y <= bottom) {
timeout.current = setTimeout(() => onCategoryChange(category), 300)
return
}
}
onCategoryChange(category)
}
function selectCategory(category: string) {
reset()
onCategoryChange(category)
}
return { onPointerMove, onPointerLeave: reset, selectCategory }
}