Chore/move framework (#34426)

* init

* Update component-preview.tsx

* move framework selector

* simplified framework selection

* moaar

* remove

* Update framework-selector.tsx

* side menu item fix

---------

Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
This commit is contained in:
Saxon FletcherandJonathan Summers-Muir authored and GitHub committed 2025-03-27 17:39:01 +08:00
1 parent c8fad32371
commit e8698432bc
10 files changed
+233 -176

No files matched your search

@@ -1,3 +1,4 @@
import { FrameworkSelector } from '@/components/framework-selector'
import { Mdx } from '@/components/mdx-components'
import { SourcePanel } from '@/components/source-panel'
import { DashboardTableOfContents } from '@/components/toc'
@@ -87,13 +88,16 @@ export default async function DocPage({ params }: DocPageProps) {
<ChevronRight className="h-4 w-4 text-foreground-muted" />
<div className="text-foreground-lighter">{doc.title}</div>
</div>
<div className="space-y-2 mb-5">
<h1 className={cn('scroll-m-20 text-4xl tracking-tight')}>{doc.title}</h1>
{doc.description && (
<p className="text-lg text-foreground-light">
<Balancer>{doc.description}</Balancer>
</p>
)}
<div className="flex items-end justify-between mb-5">
<div className="space-y-2 mb-5">
<h1 className={cn('scroll-m-20 text-4xl tracking-tight')}>{doc.title}</h1>
{doc.description && (
<p className="text-lg text-foreground-light">
<Balancer>{doc.description}</Balancer>
</p>
)}
</div>
<FrameworkSelector />
</div>
<Separator className="mb-6" />
<SourcePanel doc={doc} />
+4 -1
View File
@@ -5,12 +5,15 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { ThemeProviderProps } from 'next-themes/dist/types'
import { TooltipProvider } from 'ui'
import { FrameworkProvider } from '@/context/framework-context'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<JotaiProvider>
<NextThemesProvider {...props}>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
<FrameworkProvider>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
</FrameworkProvider>
</NextThemesProvider>
</JotaiProvider>
)
@@ -0,0 +1,78 @@
'use client'
import { usePathname, useRouter } from 'next/navigation'
import { componentPages, frameworkTitles } from '@/config/docs'
import { useFramework } from '@/context/framework-context'
import { SelectValue } from '@ui/components/shadcn/ui/select'
import {
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
} from 'ui'
const frameworks = Object.keys(frameworkTitles)
export function FrameworkSelector() {
const pathname = usePathname()
// Extract framework and docTitle from pathname
// Example: /ui/docs/nextjs/password-based-auth
const pathParts = pathname.split('/')
const docTitle = pathParts[pathParts.length - 1]
const framework = pathParts[pathParts.length - 2]
const router = useRouter()
const { setFramework: setPreferredFramework } = useFramework()
const selectedFramework = frameworks.includes(framework) ? framework : 'nextjs'
if (!framework) {
return null
}
// Get the supported frameworks for this component
const supportedFrameworks = componentPages[docTitle]?.supportedFrameworks || []
// Don't show selector if component only supports one framework
if (supportedFrameworks.length <= 1) {
return null
}
const onSelect = (value: string) => {
// Get current path parts
const currentPathParts = [...pathParts]
// Replace the framework part (second to last) with the new framework
currentPathParts[currentPathParts.length - 2] = value
// Build the new path
const newPath = currentPathParts.join('/')
router.push(newPath)
// Also update the context/localStorage for future use
setPreferredFramework(value as keyof typeof frameworkTitles)
}
const options = supportedFrameworks.map((f) => ({
label: frameworkTitles[f],
value: f,
}))
return (
<Select_Shadcn_ value={selectedFramework} onValueChange={onSelect}>
<SelectTrigger_Shadcn_ className="w-[180px]">
<SelectValue />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{options.map((f) => (
<SelectItem_Shadcn_ key={f.value} value={f.value}>
{f.label}
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
)
}
@@ -8,12 +8,27 @@ import React from 'react'
const NavigationItem: React.FC<{ item: SidebarNavItem }> = React.memo(({ item }) => {
const pathname = usePathname()
const pathParts = pathname.split('/')
const slug = pathParts[pathParts.length - 1]
const framework = pathParts[pathParts.length - 2]
const isActive = pathname === item.href
// Build URL with framework param if provided
let href = item.href
// Handle component items with slug and framework
if (!href && slug) {
if (framework) {
href = `/docs/${framework}/${slug}`
} else {
href = `/docs/${slug}`
}
}
const isActive = pathname === href
return (
<Link
href={`${item.href}`}
href={href || '#'}
className={cn(
'relative',
'flex',
@@ -30,7 +45,6 @@ const NavigationItem: React.FC<{ item: SidebarNavItem }> = React.memo(({ item })
className={cn(
'transition',
'absolute left-0 w-1 h-full bg-foreground',
isActive ? 'opacity-100' : 'opacity-0'
)}
></div>
+28 -45
View File
@@ -1,38 +1,36 @@
'use client'
import NavigationItem from '@/components/side-navigation-item'
import { aiEditorsRules, frameworkPages, gettingStarted } from '@/config/docs'
import { SelectValue } from '@ui/components/shadcn/ui/select'
import { usePathname, useRouter } from 'next/navigation'
import {
Select_Shadcn_,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
} from 'ui'
const frameworks = Object.keys(frameworkPages)
import { aiEditorsRules, componentPages, gettingStarted } from '@/config/docs'
import { useFramework } from '@/context/framework-context'
function SideNavigation() {
const router = useRouter()
const pathname = usePathname()
const { framework: preferredFramework } = useFramework()
const pathRegex = /\/docs\/([^/]+)/
const match = pathname?.match(pathRegex)
const selectedFramework = frameworks.includes(match?.[1] ?? '') ? match?.[1]! : 'nextjs'
const onSelect = (value: string) => {
const firstUrl = frameworkPages[value].items[0].href
router.push(firstUrl as string)
// Create a function to build URLs
const buildUrl = (slug: string, framework?: string) => {
if (!framework) return `/docs/${slug}`
return `/docs/${framework}/${slug}`
}
const options = frameworks.map((f) => ({
label: frameworkPages[f].title,
value: f,
}))
// Get all component pages
const componentItems = Object.entries(componentPages).map(([slug, component]) => {
let frameworkToUse = undefined
const selectedFrameworkConfig = frameworkPages[selectedFramework]
// Add framework param only if needed
if (preferredFramework && component.supportedFrameworks.includes(preferredFramework)) {
frameworkToUse = preferredFramework
} else if (component.supportedFrameworks.length > 0) {
frameworkToUse = component.supportedFrameworks[0]
}
return {
title: component.title,
href: buildUrl(slug, frameworkToUse),
items: [],
commandItemLabel: component.commandItemLabel,
}
})
return (
<nav className="min-w-[220px]">
@@ -46,27 +44,12 @@ function SideNavigation() {
</div>
<div className="pb-10 space-y-2">
<div className="font-mono uppercase text-xs text-foreground-lighter/75 mb-2 px-6 tracking-widest">
Select a framework
</div>
<div className="px-6">
<Select_Shadcn_ value={selectedFramework} onValueChange={onSelect}>
<SelectTrigger_Shadcn_ className="w-[180px]">
<SelectValue />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{options.map((f) => (
<SelectItem_Shadcn_ key={f.value} value={f.value}>
{f.label}
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
Blocks
</div>
<div className="space-y-0.5">
{selectedFrameworkConfig.items.map((item, i) => (
<NavigationItem item={item} key={`${item.href}-${i}`} />
{/* Render items based on component definitions */}
{componentItems.map((item, i) => (
<NavigationItem item={item} key={`${item.href?.toString() || item.title}-${i}`} />
))}
</div>
</div>
+42 -113
View File
@@ -41,116 +41,47 @@ export const aiEditorsRules: SidebarNavGroup = {
],
}
export const frameworkPages: Record<string, SidebarNavGroup> = {
nextjs: {
title: 'Next.js',
items: [
{
title: 'Client',
href: '/docs/nextjs/client',
items: [],
commandItemLabel: 'Supabase Client for Next.js',
},
{
title: 'Password-Based Auth',
href: '/docs/nextjs/password-based-auth',
items: [],
commandItemLabel: 'Password-Based Auth for Next.js',
},
{
title: 'Dropzone',
href: '/docs/nextjs/dropzone',
items: [],
commandItemLabel: 'Dropzone for Next.js',
},
{
title: 'Realtime Cursor',
href: '/docs/nextjs/realtime-cursor',
items: [],
commandItemLabel: 'Realtime Cursor for Next.js',
},
],
// Framework titles for display
export const frameworkTitles: Record<string, string> = {
nextjs: 'Next.js',
'react-router': 'React Router',
tanstack: 'Tanstack Start',
react: 'React SPA',
}
// Component definitions with supported frameworks
export const componentPages: Record<
string,
{
title: string
supportedFrameworks: string[]
commandItemLabel: string
href: string
}
> = {
client: {
title: 'Client',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
commandItemLabel: 'Supabase Client',
href: '/docs/client',
},
'react-router': {
title: 'React Router',
items: [
{
title: 'Client',
href: '/docs/react-router/client',
items: [],
commandItemLabel: 'Supabase Client for React Router',
},
{
title: 'Dropzone',
href: '/docs/react-router/dropzone',
items: [],
commandItemLabel: 'Dropzone for React Router',
},
{
title: 'Realtime Cursor',
href: '/docs/react-router/realtime-cursor',
items: [],
commandItemLabel: 'Realtime Cursor for React Router',
},
],
'password-based-auth': {
title: 'Password-Based Auth',
supportedFrameworks: ['nextjs', 'tanstack', 'react'],
commandItemLabel: 'Password-Based Auth',
href: '/docs/password-based-auth',
},
tanstack: {
title: 'Tanstack Start',
items: [
{
title: 'Client',
href: '/docs/tanstack/client',
items: [],
commandItemLabel: 'Supabase Client for Tanstack Start',
},
{
title: 'Password-Based Auth',
href: '/docs/tanstack/password-based-auth',
items: [],
commandItemLabel: 'Password-Based Auth for Tanstack Start',
},
{
title: 'Dropzone',
href: '/docs/tanstack/dropzone',
items: [],
commandItemLabel: 'Dropzone for Tanstack Start',
},
{
title: 'Realtime Cursor',
href: '/docs/tanstack/realtime-cursor',
items: [],
commandItemLabel: 'Realtime Cursor for Tanstack Start',
},
],
dropzone: {
title: 'Dropzone',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
commandItemLabel: 'Dropzone (File Upload)',
href: '/docs/dropzone',
},
react: {
title: 'React SPA',
items: [
{
title: 'Client',
href: '/docs/react/client',
items: [],
commandItemLabel: 'Supabase Client for React SPA',
},
{
title: 'Password-Based Auth',
href: '/docs/react/password-based-auth',
items: [],
commandItemLabel: 'Password-Based Auth for React',
},
{
title: 'Dropzone',
href: '/docs/react/dropzone',
items: [],
commandItemLabel: 'Dropzone for React',
},
{
title: 'Realtime Cursor',
href: '/docs/react/realtime-cursor',
items: [],
commandItemLabel: 'Realtime Cursor for React',
},
],
'realtime-cursor': {
title: 'Realtime Cursor',
supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'],
commandItemLabel: 'Realtime Cursor',
href: '/docs/realtime-cursor',
},
}
@@ -163,10 +94,8 @@ export const COMMAND_ITEMS = [
label: item.commandItemLabel,
href: item.href,
})),
...Object.values(frameworkPages).flatMap((group) =>
group.items.map((item) => ({
label: item.commandItemLabel,
href: item.href,
}))
),
...Object.entries(componentPages).map(([_, component]) => ({
label: component.commandItemLabel,
href: component.href,
})),
]
@@ -0,0 +1,45 @@
'use client'
import { frameworkTitles } from '@/config/docs'
import { createContext, useContext, useEffect, useState } from 'react'
type Framework = keyof typeof frameworkTitles
type FrameworkContextType = {
framework: Framework
setFramework: (framework: Framework) => void
}
const FrameworkContext = createContext<FrameworkContextType | undefined>(undefined)
export function FrameworkProvider({ children }: { children: React.ReactNode }) {
const [framework, setFrameworkState] = useState<Framework>('nextjs')
// Initialize from localStorage on mount (client-side only)
useEffect(() => {
const storedFramework = localStorage.getItem('preferredFramework')
if (storedFramework && Object.keys(frameworkTitles).includes(storedFramework)) {
setFrameworkState(storedFramework as Framework)
}
}, [])
// Update localStorage when framework changes
const setFramework = (newFramework: Framework) => {
setFrameworkState(newFramework)
localStorage.setItem('preferredFramework', newFramework)
}
return (
<FrameworkContext.Provider value={{ framework, setFramework }}>
{children}
</FrameworkContext.Provider>
)
}
// Custom hook to use the framework context
export function useFramework() {
const context = useContext(FrameworkContext)
if (context === undefined) {
throw new Error('useFramework must be used within a FrameworkProvider')
}
return context
}
+6 -6
View File
@@ -1,5 +1,5 @@
# Supabase UI Library
Last updated: 2025-03-23T14:40:10.160Z
Last updated: 2025-03-27T07:03:14.930Z
## Overview
Library of components for your project. The components integrate with Supabase and are shadcn compatible.
@@ -10,7 +10,7 @@ Library of components for your project. The components integrate with Supabase a
- [FAQ](https://supabase.com/ui/docs/getting-started/faq)
- Frequently asked questions
- [Introduction](https://supabase.com/ui/docs/getting-started/introduction)
- Components
- A flexible, open-source UI component library built on shadcn/ui, designed to simplify Supabase-powered projects with pre-built Auth, Storage, and Realtime features.
- [Quick Start](https://supabase.com/ui/docs/getting-started/quickstart)
- Components
- [Next.js](https://supabase.com/ui/docs/nextjs/client)
@@ -27,12 +27,12 @@ Library of components for your project. The components integrate with Supabase a
- Displays a control for easier uploading of files directly to Supabase Storage
- [Realtime Cursor](https://supabase.com/ui/docs/react-router/realtime-cursor)
- Real-time cursor sharing for collaborative applications
- [React](https://supabase.com/ui/docs/react/client)
- Supabase client for React
- [React Single Page Applications](https://supabase.com/ui/docs/react/client)
- Supabase client for React Single Page Applications
- [Dropzone (File Upload)](https://supabase.com/ui/docs/react/dropzone)
- Displays a control for easier uploading of files directly to Supabase Storage
- [Password-based Auth (React)](https://supabase.com/ui/docs/react/password-based-auth)
- Password-based Auth block for React
- [Password-based Authentication](https://supabase.com/ui/docs/react/password-based-auth)
- Password-based authentication block for React Single Page Applications
- [Realtime Cursor](https://supabase.com/ui/docs/react/realtime-cursor)
- Real-time cursor sharing for collaborative applications
- [Tanstack Start](https://supabase.com/ui/docs/tanstack/client)
@@ -40,7 +40,7 @@
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx",
"content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const router = useRouter()\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n router.push('/protected')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Login</CardTitle>\n <CardDescription>Enter your email below to login to your account</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleLogin}>\n <div className=\"flex flex-col gap-6\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"m@example.com\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n />\n </div>\n <div className=\"grid gap-2\">\n <div className=\"flex items-center\">\n <Label htmlFor=\"password\">Password</Label>\n <Link\n href=\"/forgot-password\"\n className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n >\n Forgot your password?\n </Link>\n </div>\n <Input\n id=\"password\"\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Logging in...' : 'Login'}\n </Button>\n </div>\n <div className=\"mt-4 text-center text-sm\">\n Don&apos;t have an account?{' '}\n <Link href=\"/sign-up\" className=\"underline underline-offset-4\">\n Sign up\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport Link from 'next/link'\nimport { useRouter } from 'next/navigation'\nimport { useState } from 'react'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(false)\n const router = useRouter()\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n console.log('login successful')\n // Update this route to redirect to an authenticated route. The user already has an active session.\n router.push('/protected')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <Card>\n <CardHeader>\n <CardTitle className=\"text-2xl\">Login</CardTitle>\n <CardDescription>Enter your email below to login to your account</CardDescription>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleLogin}>\n <div className=\"flex flex-col gap-6\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"email\">Email</Label>\n <Input\n id=\"email\"\n type=\"email\"\n placeholder=\"m@example.com\"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n />\n </div>\n <div className=\"grid gap-2\">\n <div className=\"flex items-center\">\n <Label htmlFor=\"password\">Password</Label>\n <Link\n href=\"/forgot-password\"\n className=\"ml-auto inline-block text-sm underline-offset-4 hover:underline\"\n >\n Forgot your password?\n </Link>\n </div>\n <Input\n id=\"password\"\n type=\"password\"\n required\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n />\n </div>\n {error && <p className=\"text-sm text-red-500\">{error}</p>}\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading ? 'Logging in...' : 'Login'}\n </Button>\n </div>\n <div className=\"mt-4 text-center text-sm\">\n Don&apos;t have an account?{' '}\n <Link href=\"/sign-up\" className=\"underline underline-offset-4\">\n Sign up\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n )\n}\n",
"type": "registry:component"
},
{
@@ -35,6 +35,7 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe
password,
})
if (error) throw error
console.log('login successful')
// Update this route to redirect to an authenticated route. The user already has an active session.
router.push('/example/password-based-auth/protected')
} catch (error: unknown) {