Cleanup the registry.

This commit is contained in:
Ivan Vasilov committed 2025-03-20 13:06:13 +01:00
1 parent c81cee3ad7
commit bc07855ff6
58 files changed
+2070 -3497

No files matched your search

-6
View File
@@ -1,6 +0,0 @@
{
"$schema": "https://json.schemastore.org/eslintrc",
"rules": {
"react/no-unescaped-entities": "off"
}
}
+26
View File
@@ -0,0 +1,26 @@
import { type Registry } from 'shadcn/registry'
import { clients } from './clients'
import dropzone from './default/blocks/dropzone/registry-item.json' assert { type: 'json' }
import passwordBasedAuthNextjs from './default/blocks/password-based-auth-nextjs/registry-item.json' assert { type: 'json' }
import passwordBasedAuthReact from './default/blocks/password-based-auth-react/registry-item.json' assert { type: 'json' }
import realtimeCursor from './default/blocks/realtime-cursor/registry-item.json' assert { type: 'json' }
import { registryItemAppend } from './utils'
const combine = (component: Registry['items'][number]) => {
return clients.flatMap((client) => {
return registryItemAppend(
{
...component,
name: `${component.name}-${client.name.replace('supabase-client-', '')}`,
},
[client]
)
})
}
export const blocks = [
passwordBasedAuthNextjs,
passwordBasedAuthReact,
...combine(dropzone as Registry['items'][number]),
...combine(realtimeCursor as Registry['items'][number]),
] as Registry['items']
+7
View File
@@ -0,0 +1,7 @@
import { type Registry } from 'shadcn/registry'
import nextjs from './default/clients/nextjs/registry-item.json' assert { type: 'json' }
import react from './default/clients/react/registry-item.json' assert { type: 'json' }
import remix from './default/clients/remix/registry-item.json' assert { type: 'json' }
import tanstack from './default/clients/tanstack/registry-item.json' assert { type: 'json' }
export const clients = [nextjs, react, remix, tanstack] as Registry['items']
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,166 @@
'use client'
import { cn } from '@/lib/utils'
import { UseSupabaseUploadReturn } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload'
import { Button } from '@/registry/default/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/registry/default/components/ui/tooltip'
import { CheckCircle, CircleAlert, File, Loader2, Trash, Upload } from 'lucide-react'
import { createContext, PropsWithChildren, useCallback, useContext } from 'react'
type DropzoneContextType = Omit<UseSupabaseUploadReturn, 'getRootProps' | 'getInputProps'>
const DropzoneContext = createContext<DropzoneContextType | undefined>(undefined)
type DropzoneProps = UseSupabaseUploadReturn & {
className?: string
}
const Dropzone = ({
className,
children,
getRootProps,
getInputProps,
...restProps
}: PropsWithChildren<DropzoneProps>) => {
const isActive = restProps.isDragActive
const isInvalid =
(restProps.isDragActive && restProps.isDragReject) ||
restProps.error ||
restProps.files.some((file) => file.errors.length !== 0)
return (
<DropzoneContext.Provider value={{ ...restProps }}>
<div
{...getRootProps({
className: cn(
'border-2 border-dashed border-gray-300 rounded-lg p-6 text-center bg-card transition-colors duration-300',
className,
isActive && 'border-primary bg-primary/10',
isInvalid && 'border-destructive bg-destructive/10'
),
})}
>
<input {...getInputProps()} />
{children}
</div>
</DropzoneContext.Provider>
)
}
const DropzoneContent = ({ className }: { className?: string }) => {
const { error, files, setFiles, onUpload, loading, success } = useDropzoneContext()
const handleRemoveFile = useCallback(
(fileName: string) => {
setFiles(files.filter((file) => file.name !== fileName))
},
[files, setFiles]
)
if (success) {
return (
<div className={cn('flex flex-row items-center gap-1 justify-center', className)}>
<CheckCircle className="text-primary text-lg font-medium" />
<p className="text-primary text-lg font-medium">Files uploaded successfully</p>
</div>
)
}
return (
<div className={cn('flex flex-col', className)}>
{files.map((file) => {
return (
<div
key={file.name}
className="flex items-center gap-2 border-b p-2 first:mt-4 last:mb-4 "
>
{file.type.startsWith('image/') ? (
<div className="h-12 w-12 rounded border overflow-hidden flex-shrink-0 bg-muted">
<img src={file.preview} alt={file.name} className="object-cover" />
</div>
) : (
<div className="h-12 w-12 rounded border bg-muted flex items-center justify-center">
<File />
</div>
)}
<div className="grow flex items-center gap-2 justify-start">
<span className="text-sm">{file.name}</span>
{file.errors.length !== 0 && (
<Tooltip>
<TooltipTrigger asChild>
<CircleAlert className="text-destructive" />
</TooltipTrigger>
<TooltipContent>{file.errors.map((e) => e.message).join(', ')}</TooltipContent>
</Tooltip>
)}
</div>
<Button
size="icon"
variant="destructive"
className="justify-self-end"
onClick={() => handleRemoveFile(file.name)}
>
<Trash />
</Button>
</div>
)
})}
{files.length > 0 && (
<div className="mt-2">
<Button
variant="default"
onClick={onUpload}
disabled={files.some((file) => file.errors.length !== 0) || loading}
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Uploading...
</>
) : (
<>Upload</>
)}
</Button>
</div>
)}
{error && <p className="text-destructive text-lg font-medium">{error}</p>}
</div>
)
}
const DropzoneEmptyState = ({ className }: { className?: string }) => {
const { files, maxFiles, maxFileSize, inputRef, success } = useDropzoneContext()
if ((maxFiles !== 0 && files.length >= maxFiles) || success) {
return null
}
return (
<div className={cn('flex flex-col items-center', className)}>
<div className="rounded-full bg-primary text-primary-foreground p-2">
<Upload />
</div>
<p className="text-lg font-medium">
Add or{' '}
<a onClick={() => inputRef.current?.click()} className="underline cursor-pointer">
drop {maxFiles === 1 ? `file` : 'files'}
</a>
</p>
{maxFileSize !== Number.POSITIVE_INFINITY && (
<p className="text-sm text-muted-foreground mt-2">Maximum file size: {maxFileSize} MB</p>
)}
</div>
)
}
const useDropzoneContext = () => {
const context = useContext(DropzoneContext)
if (!context) {
throw new Error('useDropzoneContext must be used within a Dropzone')
}
return context
}
export { Dropzone, DropzoneContent, DropzoneEmptyState, useDropzoneContext }
@@ -0,0 +1,122 @@
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { useCallback, useEffect, useState } from 'react'
import { FileError, FileRejection, useDropzone } from 'react-dropzone'
const supabase = createClient()
interface FileWithPreview extends File {
preview?: string
errors: readonly FileError[]
}
type UseSupabaseUploadOptions = {
bucketName: string
path: string
allowedMimeTypes?: string[]
maxFileSize?: number
maxFiles?: number
}
type UseSupabaseUploadReturn = ReturnType<typeof useSupabaseUpload>
const useSupabaseUpload = (options: UseSupabaseUploadOptions) => {
const {
bucketName,
path,
allowedMimeTypes = [],
maxFileSize = Number.POSITIVE_INFINITY,
maxFiles = 0,
} = options
const [files, setFiles] = useState<FileWithPreview[]>([])
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState(false)
const onDrop = useCallback(
(acceptedFiles: File[], fileRejections: FileRejection[]) => {
const validFiles = acceptedFiles.map((file) => {
;(file as FileWithPreview).preview = URL.createObjectURL(file)
;(file as FileWithPreview).errors = []
return file as FileWithPreview
})
const invalidFiles = fileRejections.map(({ file, errors }) => {
;(file as FileWithPreview).preview = URL.createObjectURL(file)
;(file as FileWithPreview).errors = errors
return file as FileWithPreview
})
const newFiles = [...files, ...validFiles, ...invalidFiles]
setFiles(newFiles)
},
[files, setFiles]
)
const dropzoneProps = useDropzone({
onDrop,
noClick: true,
accept: allowedMimeTypes.reduce((acc, type) => ({ ...acc, [type]: [] }), {}),
maxSize: maxFileSize,
maxFiles: maxFiles,
multiple: maxFiles !== 1,
})
const onUpload = useCallback(async () => {
setError(null)
setLoading(true)
await Promise.all(
files.map(async (file) => {
const { error } = await supabase.storage
.from(bucketName)
.upload(`${path}/${file.name}`, file, {
cacheControl: '3600',
upsert: false,
})
if (error) {
setError(error.message)
} else {
setSuccess(true)
}
})
)
setLoading(false)
}, [files, path, bucketName, setError, setLoading])
useEffect(() => {
if (files.length === 0) {
setError(null)
}
// If the number of files doesn't exceed the maxFiles parameter, remove the error 'Too many files' from each file
if (files.length <= maxFiles) {
let changed = false
const newFiles = files.map((file) => {
if (file.errors.some((e) => e.code === 'too-many-files')) {
file.errors = file.errors.filter((e) => e.code !== 'too-many-files')
changed = true
}
return file
})
if (changed) {
setFiles(newFiles)
}
}
}, [files.length, setFiles, maxFiles])
return {
files,
setFiles,
success,
loading,
error,
setError,
onUpload,
maxFileSize: maxFileSize,
maxFiles: maxFiles,
...dropzoneProps,
}
}
export { useSupabaseUpload, type UseSupabaseUploadOptions, type UseSupabaseUploadReturn }
@@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "dropzone",
"type": "registry:component",
"title": "Dropzone (File Upload)",
"description": "Displays a control for easier uploading of files directly to Supabase Storage.",
"registryDependencies": ["button", "tooltip", "progress"],
"dependencies": ["react-dropzone", "lucide-react"],
"files": [
{
"path": "registry/default/blocks/dropzone/components/dropzone.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/dropzone/hooks/use-supabase-upload.ts",
"type": "registry:hook"
}
]
}
@@ -0,0 +1,31 @@
import { type EmailOtpType } from '@supabase/supabase-js'
import { type NextRequest } from 'next/server'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'
import { redirect } from 'next/navigation'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const token_hash = searchParams.get('token_hash')
const type = searchParams.get('type') as EmailOtpType | null
const next = searchParams.get('next') ?? '/'
if (token_hash && type) {
const supabase = await createClient()
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
})
if (!error) {
// redirect user to specified redirect URL or root of app
redirect(next)
} else {
// redirect the user to an error page with some instructions
redirect(`/auth/error?error=${error?.message}`)
}
}
// redirect the user to an error page with some instructions
redirect(`/auth/error?error=No token hash or type`)
}
@@ -0,0 +1,3 @@
export default function Page({ searchParams }: { searchParams: { error: string } }) {
return <div>Sorry, something went wrong. Code error: {searchParams.error}</div>
}
@@ -0,0 +1,11 @@
import { ForgotPasswordForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/forgot-password-form'
export default function Page() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<ForgotPasswordForm />
</div>
</div>
)
}
@@ -0,0 +1,11 @@
import { LoginForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/login-form'
export default function Page() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<LoginForm />
</div>
</div>
)
}
@@ -0,0 +1,14 @@
import { redirect } from 'next/navigation'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/server'
export default async function ProtectedPage() {
const supabase = await createClient()
const { data, error } = await supabase.auth.getUser()
if (error || !data?.user) {
redirect('/login')
}
return <p>Hello {data.user.email}</p>
}
@@ -0,0 +1,3 @@
export default function Page() {
return <div>Sign up success</div>
}
@@ -0,0 +1,11 @@
import { SignUpForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/sign-up-form'
export default function Page() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<SignUpForm />
</div>
</div>
)
}
@@ -0,0 +1,11 @@
import { UpdatePasswordForm } from '@/registry/default/blocks/password-based-auth-nextjs/components/update-password-form'
export default function Page() {
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<UpdatePasswordForm />
</div>
</div>
)
}
@@ -0,0 +1,81 @@
'use client'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import Link from 'next/link'
import { useState } from 'react'
export function ForgotPasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const supabase = createClient()
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
try {
// The url which will be included in the email. This URL needs to be configured in your redirect URLs in the Supabase dashboard at https://supabase.com/dashboard/project/_/auth/url-configuration
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: 'http://localhost:3000/update-password',
})
if (error) throw error
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Reset Your Password</CardTitle>
<CardDescription>
Type in your email and we&apos;ll send you a link to reset your password
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleForgotPassword}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Sending...' : 'Send reset email'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<Link href="/login" className="underline underline-offset-4">
Login
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,102 @@
'use client'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const supabase = createClient()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
router.push('/protected')
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>Enter your email below to login to your account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Link
href="/forgot-password"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</Link>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{' '}
<Link href="/sign-up" className="underline underline-offset-4">
Sign up
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,114 @@
'use client'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [repeatPassword, setRepeatPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const supabase = createClient()
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
if (password !== repeatPassword) {
setError('Passwords do not match')
setIsLoading(false)
return
}
try {
const { error } = await supabase.auth.signUp({
email,
password,
})
if (error) throw error
router.push('/sign-up-success')
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Sign up</CardTitle>
<CardDescription>Create a new account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSignUp}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="repeat-password">Repeat Password</Label>
</div>
<Input
id="repeat-password"
type="password"
required
value={repeatPassword}
onChange={(e) => setRepeatPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Creating an account...' : 'Sign up'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<Link href="/login" className="underline underline-offset-4">
Login
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,79 @@
'use client'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
export function UpdatePasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()
const supabase = createClient()
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.updateUser({ password })
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
router.push('/protected')
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Reset Your Password</CardTitle>
<CardDescription>Please enter your new password below.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleForgotPassword}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="password">New password</Label>
<Input
id="password"
type="password"
placeholder="New password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save new password'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<a href="/login" className="underline underline-offset-4">
Login
</a>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,19 @@
import { updateSession } from '@/registry/default/clients/nextjs/lib/supabase/middleware'
import { type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
@@ -0,0 +1,71 @@
{
"name": "password-based-auth-nextjs",
"type": "registry:block",
"title": "Password Based Auth flow for Nextjs and Supabase",
"description": "Password Based Auth flow for Nextjs and Supabase",
"registryDependencies": ["button", "card", "input", "label"],
"dependencies": ["@supabase/ssr@latest"],
"files": [
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/login/page.tsx",
"type": "registry:page",
"target": "app/login/page.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/error/page.tsx",
"type": "registry:page",
"target": "app/auth/error/page.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/protected/page.tsx",
"type": "registry:page",
"target": "app/protected/page.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/confirm/route.ts",
"type": "registry:page",
"target": "app/auth/confirm/route.ts"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/middleware.ts",
"type": "registry:file",
"target": "middleware.ts"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/sign-up/page.tsx",
"type": "registry:page",
"target": "app/sign-up/page.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/sign-up-success/page.tsx",
"type": "registry:page",
"target": "app/sign-up-success/page.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/components/sign-up-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/forgot-password/page.tsx",
"type": "registry:page",
"target": "app/forgot-password/page.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/app/update-password/page.tsx",
"type": "registry:page",
"target": "app/update-password/page.tsx"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/components/forgot-password-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-nextjs/components/update-password-form.tsx",
"type": "registry:component"
}
]
}
@@ -0,0 +1,82 @@
'use client'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { useState } from 'react'
export function ForgotPasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [success, setSuccess] = useState(false)
const supabase = createClient()
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
try {
// The url which will be included in the email. This URL needs to be configured in your redirect URLs in the Supabase dashboard at https://supabase.com/dashboard/project/_/auth/url-configuration
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: 'http://localhost:3000/update-password',
})
if (error) throw error
setSuccess(true)
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Reset Your Password</CardTitle>
<CardDescription>
Type in your email and we&apos;ll send you a link to reset your password
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleForgotPassword}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Sending...' : 'Send reset email'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<a href="/login" className="underline underline-offset-4">
Login
</a>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,99 @@
'use client'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { useState } from 'react'
export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const supabase = createClient()
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Login</CardTitle>
<CardDescription>Enter your email below to login to your account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleLogin}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<a
href="/forgot-password"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</a>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{' '}
<a href="/sign-up" className="underline underline-offset-4">
Sign up
</a>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,111 @@
'use client'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { useState } from 'react'
export function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [repeatPassword, setRepeatPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const supabase = createClient()
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
if (password !== repeatPassword) {
setError('Passwords do not match')
setIsLoading(false)
return
}
try {
const { error } = await supabase.auth.signUp({
email,
password,
})
if (error) throw error
// TODO: Redirect to sign-up success page
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Sign up</CardTitle>
<CardDescription>Create a new account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSignUp}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="repeat-password">Repeat Password</Label>
</div>
<Input
id="repeat-password"
type="password"
required
value={repeatPassword}
onChange={(e) => setRepeatPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Creating an account...' : 'Sign up'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<a href="#" className="underline underline-offset-4">
Login
</a>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,77 @@
'use client'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/react/lib/supabase/client'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { useState } from 'react'
export function UpdatePasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const supabase = createClient()
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.updateUser({ password })
if (error) throw error
// TODO:Update this route to redirect to an authenticated route. The user already has an active session.
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">Reset Your Password</CardTitle>
<CardDescription>Please enter your new password below.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleForgotPassword}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="password">New password</Label>
<Input
id="password"
type="password"
placeholder="New password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save new password'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<a href="#" className="underline underline-offset-4">
Login
</a>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
@@ -0,0 +1,30 @@
{
"name": "password-based-auth-react",
"type": "registry:block",
"title": "Password Based Auth flow for React and Supabase",
"description": "Password Based Auth flow for React and Supabase",
"registryDependencies": ["button", "card", "input", "label"],
"files": [
{
"path": "registry/default/blocks/password-based-auth-react/components/login-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-react/components/sign-up-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-react/components/forgot-password-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/password-based-auth-react/components/update-password-form.tsx",
"type": "registry:component"
},
{
"path": "registry/default/clients/react/lib/supabase/client.ts",
"type": "registry:lib"
}
],
"dependencies": ["@supabase/supabase-js@latest"]
}
@@ -0,0 +1,27 @@
import { cn } from '@/lib/utils'
import { MousePointer2 } from 'lucide-react'
export const Cursor = ({
className,
style,
color,
name,
}: {
className?: string
style?: React.CSSProperties
color: string
name: string
}) => {
return (
<div className={cn('pointer-events-none', className)} style={style}>
<MousePointer2 color={color} fill={color} size={30} />
<div
className="mt-1 px-2 py-1 rounded text-xs font-bold text-white text-center"
style={{ backgroundColor: color }}
>
{name}
</div>
</div>
)
}
@@ -0,0 +1,26 @@
'use client'
import { Cursor } from '@/registry/default/blocks/realtime-cursor/components/cursor'
import { useRealtimeCursors } from '@/registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors'
export const RealtimeCursors = ({ roomName, username }: { roomName: string; username: string }) => {
const { cursors } = useRealtimeCursors({ roomName, username })
return (
<div>
{Object.keys(cursors).map((id) => (
<Cursor
key={id}
className="absolute transform ease-linear duration-70 z-50"
style={{
top: 0,
left: 0,
transform: `translate(${cursors[id].position.x}px, ${cursors[id].position.y}px)`,
}}
color={cursors[id].color}
name={cursors[id].user.name}
/>
))}
</div>
)
}
@@ -0,0 +1,104 @@
import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'
import { RealtimeChannel } from '@supabase/supabase-js'
import { useCallback, useEffect, useRef, useState } from 'react'
const supabase = createClient()
const generateRandomColor = () => `hsl(${Math.floor(Math.random() * 360)}, 100%, 70%)`
const generateRandomNumber = () => Math.floor(Math.random() * 100)
const EVENT_NAME = 'realtime-cursor-move'
type CursorEventPayload = {
position: {
x: number
y: number
}
user: {
id: number
name: string
}
color: string
timestamp: number
}
export const useRealtimeCursors = ({
roomName,
username,
}: {
roomName: string
username: string
}) => {
const [color] = useState(generateRandomColor())
const [userId] = useState(generateRandomNumber())
const [cursors, setCursors] = useState<Record<string, CursorEventPayload>>({})
const channelRef = useRef<RealtimeChannel | null>(null)
const handleMouseMove = useCallback(
(event: MouseEvent) => {
const { clientX, clientY } = event
const payload: CursorEventPayload = {
position: {
x: clientX,
y: clientY,
},
user: {
id: userId,
name: username,
},
color: color,
timestamp: new Date().getTime(),
}
channelRef.current?.send({
type: 'broadcast',
event: EVENT_NAME,
payload: payload,
})
},
[color, userId, username]
)
useEffect(() => {
const channel = supabase.channel(roomName)
channelRef.current = channel
channel
.on('broadcast', { event: EVENT_NAME }, (data: { payload: CursorEventPayload }) => {
const { user } = data.payload
// Don't render your own cursor
if (user.id === userId) return
setCursors((prev) => {
if (prev[userId]) {
delete prev[userId]
}
return {
...prev,
[user.id]: data.payload,
}
})
})
.subscribe()
return () => {
channel.unsubscribe()
}
}, [])
useEffect(() => {
// Add event listener for mousemove
window.addEventListener('mousemove', handleMouseMove)
// Cleanup on unmount
return () => {
window.removeEventListener('mousemove', handleMouseMove)
}
}, [handleMouseMove])
return { cursors }
}
@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "realtime-cursor",
"type": "registry:component",
"title": "Realtime Cursor",
"description": "Component which renders realtime cursors from other users in a room.",
"registryDependencies": [],
"dependencies": ["lucide-react"],
"files": [
{
"path": "registry/default/blocks/realtime-cursor/components/cursor.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-cursor/components/realtime-cursors.tsx",
"type": "registry:component"
},
{
"path": "registry/default/blocks/realtime-cursor/hooks/use-realtime-cursors.ts",
"type": "registry:hook"
}
]
}
@@ -0,0 +1,8 @@
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
@@ -0,0 +1,65 @@
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// Do not run code between createServerClient and
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
// issues with users being randomly logged out.
// IMPORTANT: DO NOT REMOVE auth.getUser()
const {
data: { user },
} = await supabase.auth.getUser()
if (
!user &&
!request.nextUrl.pathname.startsWith('/login') &&
!request.nextUrl.pathname.startsWith('/auth')
) {
// no user, potentially respond by redirecting the user to the login page
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
// IMPORTANT: You *must* return the supabaseResponse object as it is.
// If you're creating a new response object with NextResponse.next() make sure to:
// 1. Pass the request in it, like so:
// const myNewResponse = NextResponse.next({ request })
// 2. Copy over the cookies, like so:
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
// 3. Change the myNewResponse object to fit your needs, but avoid changing
// the cookies!
// 4. Finally:
// return myNewResponse
// If this is not done, you may be causing the browser and server to go out
// of sync and terminate the user's session prematurely!
return supabaseResponse
}
@@ -0,0 +1,29 @@
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}
@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "supabase-client-nextjs",
"type": "registry:lib",
"title": "Supabase Client for Next.js",
"description": "",
"registryDependencies": [],
"dependencies": ["@supabase/ssr@latest"],
"files": [
{
"path": "registry/default/clients/nextjs/lib/supabase/client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/nextjs/lib/supabase/server.ts",
"type": "registry:lib"
}
]
}
@@ -0,0 +1,8 @@
import { createClient as createSupabaseClient } from '@supabase/supabase-js'
export function createClient() {
return createSupabaseClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
@@ -0,0 +1,15 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "supabase-client-react",
"type": "registry:lib",
"title": "Supabase Client for React",
"description": "",
"registryDependencies": [],
"dependencies": ["@supabase/supabase-js@latest"],
"files": [
{
"path": "registry/default/clients/react/lib/supabase/client.ts",
"type": "registry:lib"
}
]
}
@@ -0,0 +1,7 @@
import { createBrowserClient } from '@supabase/ssr'
// Use this function to create a client for the browser. You should pass the env variables through a loader and use them
// to instantiate the client.
export function createClient(supabaseUrl: string, supabaseAnonKey: string) {
return createBrowserClient(supabaseUrl, supabaseAnonKey)
}
@@ -0,0 +1,23 @@
import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'
export function createClient(request: Request) {
const headers = new Headers()
const supabase = createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, {
cookies: {
getAll() {
return parseCookieHeader(request.headers.get('Cookie') ?? '') as {
name: string
value: string
}[]
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
headers.append('Set-Cookie', serializeCookieHeader(name, value, options))
)
},
},
})
return { supabase, headers }
}
@@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "supabase-client-remix",
"type": "registry:lib",
"title": "Supabase Client for Remix",
"description": "",
"registryDependencies": [],
"dependencies": ["@supabase/ssr@latest"],
"files": [
{
"path": "registry/default/clients/remix/lib/supabase.client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/remix/lib/supabase.server.ts",
"type": "registry:lib"
}
]
}
@@ -0,0 +1,5 @@
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(process.env.VITE_SUPABASE_URL!, process.env.VITE_SUPABASE_ANON_KEY!)
}
@@ -0,0 +1,23 @@
import { createServerClient } from '@supabase/ssr'
import { parseCookies, setCookie } from '@tanstack/react-start/server'
export function getSupabaseServerClient() {
return createServerClient(process.env.VITE_SUPABASE_URL!, process.env.VITE_SUPABASE_ANON_KEY!, {
cookies: {
getAll() {
return Object.entries(parseCookies()).map(
([name, value]) =>
({
name,
value,
}) as { name: string; value: string }
)
},
setAll(cookies) {
cookies.forEach((cookie) => {
setCookie(cookie.name, cookie.value)
})
},
},
})
}
@@ -0,0 +1,19 @@
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "supabase-client-tanstack",
"type": "registry:lib",
"title": "Supabase Client for Tanstack Start",
"description": "",
"registryDependencies": [],
"dependencies": ["@supabase/ssr@latest"],
"files": [
{
"path": "registry/default/clients/tanstack/lib/supabase/client.ts",
"type": "registry:lib"
},
{
"path": "registry/default/clients/tanstack/lib/supabase/server.ts",
"type": "registry:lib"
}
]
}
@@ -0,0 +1,50 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
)
}
)
Button.displayName = 'Button'
export { Button, buttonVariants }
@@ -0,0 +1,55 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('rounded-xl border bg-card text-card-foreground shadow', className)}
{...props}
/>
)
)
Card.displayName = 'Card'
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
)
)
CardHeader.displayName = 'CardHeader'
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('font-semibold leading-none tracking-tight', className)}
{...props}
/>
)
)
CardTitle.displayName = 'CardTitle'
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
)
)
CardDescription.displayName = 'CardDescription'
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
)
)
CardContent.displayName = 'CardContent'
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
)
)
CardFooter.displayName = 'CardFooter'
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
@@ -0,0 +1,22 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export { Input }
@@ -0,0 +1,21 @@
'use client'
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }
@@ -0,0 +1,28 @@
'use client'
import * as React from 'react'
import * as ProgressPrimitive from '@radix-ui/react-progress'
import { cn } from '@/lib/utils'
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn('bg-primary/20 relative h-2 w-full overflow-hidden rounded-full', className)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }
@@ -0,0 +1,21 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Textarea = React.forwardRef<HTMLTextAreaElement, React.ComponentProps<'textarea'>>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = 'Textarea'
export { Textarea }
@@ -0,0 +1,32 @@
'use client'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import * as React from 'react'
import { cn } from '@/lib/utils'
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]',
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }
@@ -0,0 +1,28 @@
'use client'
import {
Dropzone,
DropzoneContent,
DropzoneEmptyState,
} from '@/registry/default/blocks/dropzone/components/dropzone'
import { useSupabaseUpload } from '@/registry/default/blocks/dropzone/hooks/use-supabase-upload'
const FileUploadDemo = () => {
const props = useSupabaseUpload({
bucketName: 'test',
path: 'test',
allowedMimeTypes: ['image/*'],
maxFiles: 2,
})
return (
<div>
<Dropzone {...props}>
<DropzoneEmptyState />
<DropzoneContent />
</Dropzone>
</div>
)
}
export default FileUploadDemo
@@ -0,0 +1,26 @@
'use client'
import { RealtimeCursors } from '@/registry/default/blocks/realtime-cursor/components/realtime-cursors'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
import { useEffect, useState } from 'react'
const names = ['Eren', 'Armin', 'Mikasa', 'Reiner', 'Levi', 'Bertholdt']
const RealtimeCursorDemo = () => {
const [username, setUsername] = useState('')
useEffect(() => {
setUsername(names[Math.floor(Math.random() * names.length)])
}, [])
return (
<div className="flex flex-col w-40">
<Label>Username</Label>
<Input value={username} onChange={(e) => setUsername(e.target.value)} />
<RealtimeCursors roomName="realtime-cursor-example" username={username} />
</div>
)
}
export default RealtimeCursorDemo
File diff suppressed because it is too large. Load diff
+17
View File
@@ -0,0 +1,17 @@
import { type Registry } from 'shadcn/registry'
import { examples } from '@/registry/examples'
import { blocks } from './blocks'
import { clients } from './clients'
export const registry = {
name: 'Supabase UI Library',
homepage: 'https://supabase.com/ui',
items: [
...blocks,
...clients,
// Internal use only.
...examples,
],
} satisfies Registry
-6
View File
@@ -1,6 +0,0 @@
import { Registry } from '@/registry/schema'
import { examples } from '@/registry//examples'
import { fragments } from '@/registry/fragments'
import { charts } from '@/registry/charts'
export const registry: Registry = [...fragments, ...examples, ...charts]
-68
View File
@@ -1,68 +0,0 @@
import { z } from 'zod'
export const blockChunkSchema = z.object({
name: z.string(),
description: z.string(),
component: z.any(),
file: z.string(),
code: z.string().optional(),
container: z
.object({
className: z.string().nullish(),
})
.optional(),
})
const backendProviderValues = ['next-auth', 'supabase', 'auth0']
const isBackendProvider = (value: unknown): value is (typeof backendProviderValues)[number] =>
backendProviderValues.includes(value as string)
export const registryEntrySchema = z.object({
name: z.string(),
description: z.string().optional(),
dependencies: z.array(z.string()).optional(),
devDependencies: z.array(z.string()).optional(),
registryDependencies: z.array(z.string()).optional(),
files: z.array(z.string()),
source: z.string().optional(),
type: z.enum([
'components:ui',
'components:fragment',
'components:component',
'components:example',
'components:block',
'docs:example',
]),
category: z.string().optional(),
backendProviders: z.array(z.string()).optional(),
subcategory: z.string().optional(),
chunks: z.array(blockChunkSchema).optional(),
// Optional path of file that is exported from a package
optionalPath: z.string().optional(),
})
export const registrySchema = z.array(registryEntrySchema)
export type RegistryEntry = z.infer<typeof registryEntrySchema>
export type Registry = z.infer<typeof registrySchema>
export const blockSchema = registryEntrySchema.extend({
type: z.literal('components:block'),
style: z.enum(['default', 'new-york']),
component: z.any(),
container: z
.object({
height: z.string().optional(),
className: z.string().nullish(),
})
.optional(),
code: z.string(),
backendProvidersCode: z.any().optional(),
highlightedCode: z.string(),
})
export type Block = z.infer<typeof blockSchema>
export type BlockChunk = z.infer<typeof blockChunkSchema>
-4
View File
@@ -3,10 +3,6 @@ export const styles = [
name: 'default',
label: 'Default',
},
// {
// name: 'new-york',
// label: 'New York',
// },
] as const
export type Style = (typeof styles)[number]
-636
View File
@@ -1,636 +0,0 @@
export const themes = [
{
name: 'zinc',
label: 'Zinc',
activeColor: {
light: '240 5.9% 10%',
dark: '240 5.2% 33.9%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '240 10% 3.9%',
card: '0 0% 100%',
'card-foreground': '240 10% 3.9%',
popover: '0 0% 100%',
'popover-foreground': '240 10% 3.9%',
primary: '240 5.9% 10%',
'primary-foreground': '0 0% 98%',
secondary: '240 4.8% 95.9%',
'secondary-foreground': '240 5.9% 10%',
muted: '240 4.8% 95.9%',
'muted-foreground': '240 3.8% 46.1%',
accent: '240 4.8% 95.9%',
'accent-foreground': '240 5.9% 10%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '0 0% 98%',
border: '240 5.9% 90%',
input: '240 5.9% 90%',
ring: '240 5.9% 10%',
radius: '0.5rem',
},
dark: {
background: '240 10% 3.9%',
foreground: '0 0% 98%',
card: '240 10% 3.9%',
'card-foreground': '0 0% 98%',
popover: '240 10% 3.9%',
'popover-foreground': '0 0% 98%',
primary: '0 0% 98%',
'primary-foreground': '240 5.9% 10%',
secondary: '240 3.7% 15.9%',
'secondary-foreground': '0 0% 98%',
muted: '240 3.7% 15.9%',
'muted-foreground': '240 5% 64.9%',
accent: '240 3.7% 15.9%',
'accent-foreground': '0 0% 98%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '0 0% 98%',
border: '240 3.7% 15.9%',
input: '240 3.7% 15.9%',
ring: '240 4.9% 83.9%',
},
},
},
{
name: 'slate',
label: 'Slate',
activeColor: {
light: '215.4 16.3% 46.9%',
dark: '215.3 19.3% 34.5%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '222.2 84% 4.9%',
card: '0 0% 100%',
'card-foreground': '222.2 84% 4.9%',
popover: '0 0% 100%',
'popover-foreground': '222.2 84% 4.9%',
primary: '222.2 47.4% 11.2%',
'primary-foreground': '210 40% 98%',
secondary: '210 40% 96.1%',
'secondary-foreground': '222.2 47.4% 11.2%',
muted: '210 40% 96.1%',
'muted-foreground': '215.4 16.3% 46.9%',
accent: '210 40% 96.1%',
'accent-foreground': '222.2 47.4% 11.2%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '210 40% 98%',
border: '214.3 31.8% 91.4%',
input: '214.3 31.8% 91.4%',
ring: '222.2 84% 4.9%',
radius: '0.5rem',
},
dark: {
background: '222.2 84% 4.9%',
foreground: '210 40% 98%',
card: '222.2 84% 4.9%',
'card-foreground': '210 40% 98%',
popover: '222.2 84% 4.9%',
'popover-foreground': '210 40% 98%',
primary: '210 40% 98%',
'primary-foreground': '222.2 47.4% 11.2%',
secondary: '217.2 32.6% 17.5%',
'secondary-foreground': '210 40% 98%',
muted: '217.2 32.6% 17.5%',
'muted-foreground': '215 20.2% 65.1%',
accent: '217.2 32.6% 17.5%',
'accent-foreground': '210 40% 98%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '210 40% 98%',
border: '217.2 32.6% 17.5%',
input: '217.2 32.6% 17.5%',
ring: '212.7 26.8% 83.9',
},
},
},
{
name: 'stone',
label: 'Stone',
activeColor: {
light: '25 5.3% 44.7%',
dark: '33.3 5.5% 32.4%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '20 14.3% 4.1%',
card: '0 0% 100%',
'card-foreground': '20 14.3% 4.1%',
popover: '0 0% 100%',
'popover-foreground': '20 14.3% 4.1%',
primary: '24 9.8% 10%',
'primary-foreground': '60 9.1% 97.8%',
secondary: '60 4.8% 95.9%',
'secondary-foreground': '24 9.8% 10%',
muted: '60 4.8% 95.9%',
'muted-foreground': '25 5.3% 44.7%',
accent: '60 4.8% 95.9%',
'accent-foreground': '24 9.8% 10%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '60 9.1% 97.8%',
border: '20 5.9% 90%',
input: '20 5.9% 90%',
ring: '20 14.3% 4.1%',
radius: '0.95rem',
},
dark: {
background: '20 14.3% 4.1%',
foreground: '60 9.1% 97.8%',
card: '20 14.3% 4.1%',
'card-foreground': '60 9.1% 97.8%',
popover: '20 14.3% 4.1%',
'popover-foreground': '60 9.1% 97.8%',
primary: '60 9.1% 97.8%',
'primary-foreground': '24 9.8% 10%',
secondary: '12 6.5% 15.1%',
'secondary-foreground': '60 9.1% 97.8%',
muted: '12 6.5% 15.1%',
'muted-foreground': '24 5.4% 63.9%',
accent: '12 6.5% 15.1%',
'accent-foreground': '60 9.1% 97.8%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '60 9.1% 97.8%',
border: '12 6.5% 15.1%',
input: '12 6.5% 15.1%',
ring: '24 5.7% 82.9%',
},
},
},
{
name: 'gray',
label: 'Gray',
activeColor: {
light: '220 8.9% 46.1%',
dark: '215 13.8% 34.1%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '224 71.4% 4.1%',
card: '0 0% 100%',
'card-foreground': '224 71.4% 4.1%',
popover: '0 0% 100%',
'popover-foreground': '224 71.4% 4.1%',
primary: '220.9 39.3% 11%',
'primary-foreground': '210 20% 98%',
secondary: '220 14.3% 95.9%',
'secondary-foreground': '220.9 39.3% 11%',
muted: '220 14.3% 95.9%',
'muted-foreground': '220 8.9% 46.1%',
accent: '220 14.3% 95.9%',
'accent-foreground': '220.9 39.3% 11%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '210 20% 98%',
border: '220 13% 91%',
input: '220 13% 91%',
ring: '224 71.4% 4.1%',
radius: '0.35rem',
},
dark: {
background: '224 71.4% 4.1%',
foreground: '210 20% 98%',
card: '224 71.4% 4.1%',
'card-foreground': '210 20% 98%',
popover: '224 71.4% 4.1%',
'popover-foreground': '210 20% 98%',
primary: '210 20% 98%',
'primary-foreground': '220.9 39.3% 11%',
secondary: '215 27.9% 16.9%',
'secondary-foreground': '210 20% 98%',
muted: '215 27.9% 16.9%',
'muted-foreground': '217.9 10.6% 64.9%',
accent: '215 27.9% 16.9%',
'accent-foreground': '210 20% 98%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '210 20% 98%',
border: '215 27.9% 16.9%',
input: '215 27.9% 16.9%',
ring: '216 12.2% 83.9%',
},
},
},
{
name: 'neutral',
label: 'Neutral',
activeColor: {
light: '0 0% 45.1%',
dark: '0 0% 32.2%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '0 0% 3.9%',
card: '0 0% 100%',
'card-foreground': '0 0% 3.9%',
popover: '0 0% 100%',
'popover-foreground': '0 0% 3.9%',
primary: '0 0% 9%',
'primary-foreground': '0 0% 98%',
secondary: '0 0% 96.1%',
'secondary-foreground': '0 0% 9%',
muted: '0 0% 96.1%',
'muted-foreground': '0 0% 45.1%',
accent: '0 0% 96.1%',
'accent-foreground': '0 0% 9%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '0 0% 98%',
border: '0 0% 89.8%',
input: '0 0% 89.8%',
ring: '0 0% 3.9%',
},
dark: {
background: '0 0% 3.9%',
foreground: '0 0% 98%',
card: '0 0% 3.9%',
'card-foreground': '0 0% 98%',
popover: '0 0% 3.9%',
'popover-foreground': '0 0% 98%',
primary: '0 0% 98%',
'primary-foreground': '0 0% 9%',
secondary: '0 0% 14.9%',
'secondary-foreground': '0 0% 98%',
muted: '0 0% 14.9%',
'muted-foreground': '0 0% 63.9%',
accent: '0 0% 14.9%',
'accent-foreground': '0 0% 98%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '0 0% 98%',
border: '0 0% 14.9%',
input: '0 0% 14.9%',
ring: '0 0% 83.1%',
},
},
},
{
name: 'red',
label: 'Red',
activeColor: {
light: '0 72.2% 50.6%',
dark: '0 72.2% 50.6%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '0 0% 3.9%',
card: '0 0% 100%',
'card-foreground': '0 0% 3.9%',
popover: '0 0% 100%',
'popover-foreground': '0 0% 3.9%',
primary: '0 72.2% 50.6%',
'primary-foreground': '0 85.7% 97.3%',
secondary: '0 0% 96.1%',
'secondary-foreground': '0 0% 9%',
muted: '0 0% 96.1%',
'muted-foreground': '0 0% 45.1%',
accent: '0 0% 96.1%',
'accent-foreground': '0 0% 9%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '0 0% 98%',
border: '0 0% 89.8%',
input: '0 0% 89.8%',
ring: '0 72.2% 50.6%',
radius: '0.4rem',
},
dark: {
background: '0 0% 3.9%',
foreground: '0 0% 98%',
card: '0 0% 3.9%',
'card-foreground': '0 0% 98%',
popover: '0 0% 3.9%',
'popover-foreground': '0 0% 98%',
primary: '0 72.2% 50.6%',
'primary-foreground': '0 85.7% 97.3%',
secondary: '0 0% 14.9%',
'secondary-foreground': '0 0% 98%',
muted: '0 0% 14.9%',
'muted-foreground': '0 0% 63.9%',
accent: '0 0% 14.9%',
'accent-foreground': '0 0% 98%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '0 0% 98%',
border: '0 0% 14.9%',
input: '0 0% 14.9%',
ring: '0 72.2% 50.6%',
},
},
},
{
name: 'rose',
label: 'Rose',
activeColor: {
light: '346.8 77.2% 49.8%',
dark: '346.8 77.2% 49.8%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '240 10% 3.9%',
card: '0 0% 100%',
'card-foreground': '240 10% 3.9%',
popover: '0 0% 100%',
'popover-foreground': '240 10% 3.9%',
primary: '346.8 77.2% 49.8%',
'primary-foreground': '355.7 100% 97.3%',
secondary: '240 4.8% 95.9%',
'secondary-foreground': '240 5.9% 10%',
muted: '240 4.8% 95.9%',
'muted-foreground': '240 3.8% 46.1%',
accent: '240 4.8% 95.9%',
'accent-foreground': '240 5.9% 10%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '0 0% 98%',
border: '240 5.9% 90%',
input: '240 5.9% 90%',
ring: '346.8 77.2% 49.8%',
radius: '0.5rem',
},
dark: {
background: '20 14.3% 4.1%',
foreground: '0 0% 95%',
popover: '0 0% 9%',
'popover-foreground': '0 0% 95%',
card: '24 9.8% 10%',
'card-foreground': '0 0% 95%',
primary: '346.8 77.2% 49.8%',
'primary-foreground': '355.7 100% 97.3%',
secondary: '240 3.7% 15.9%',
'secondary-foreground': '0 0% 98%',
muted: '0 0% 15%',
'muted-foreground': '240 5% 64.9%',
accent: '12 6.5% 15.1%',
'accent-foreground': '0 0% 98%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '0 85.7% 97.3%',
border: '240 3.7% 15.9%',
input: '240 3.7% 15.9%',
ring: '346.8 77.2% 49.8%',
},
},
},
{
name: 'orange',
label: 'Orange',
activeColor: {
light: '24.6 95% 53.1%',
dark: '20.5 90.2% 48.2%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '20 14.3% 4.1%',
card: '0 0% 100%',
'card-foreground': '20 14.3% 4.1%',
popover: '0 0% 100%',
'popover-foreground': '20 14.3% 4.1%',
primary: '24.6 95% 53.1%',
'primary-foreground': '60 9.1% 97.8%',
secondary: '60 4.8% 95.9%',
'secondary-foreground': '24 9.8% 10%',
muted: '60 4.8% 95.9%',
'muted-foreground': '25 5.3% 44.7%',
accent: '60 4.8% 95.9%',
'accent-foreground': '24 9.8% 10%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '60 9.1% 97.8%',
border: '20 5.9% 90%',
input: '20 5.9% 90%',
ring: '24.6 95% 53.1%',
radius: '0.95rem',
},
dark: {
background: '20 14.3% 4.1%',
foreground: '60 9.1% 97.8%',
card: '20 14.3% 4.1%',
'card-foreground': '60 9.1% 97.8%',
popover: '20 14.3% 4.1%',
'popover-foreground': '60 9.1% 97.8%',
primary: '20.5 90.2% 48.2%',
'primary-foreground': '60 9.1% 97.8%',
secondary: '12 6.5% 15.1%',
'secondary-foreground': '60 9.1% 97.8%',
muted: '12 6.5% 15.1%',
'muted-foreground': '24 5.4% 63.9%',
accent: '12 6.5% 15.1%',
'accent-foreground': '60 9.1% 97.8%',
destructive: '0 72.2% 50.6%',
'destructive-foreground': '60 9.1% 97.8%',
border: '12 6.5% 15.1%',
input: '12 6.5% 15.1%',
ring: '20.5 90.2% 48.2%',
},
},
},
{
name: 'green',
label: 'Green',
activeColor: {
light: '142.1 76.2% 36.3%',
dark: '142.1 70.6% 45.3%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '240 10% 3.9%',
card: '0 0% 100%',
'card-foreground': '240 10% 3.9%',
popover: '0 0% 100%',
'popover-foreground': '240 10% 3.9%',
primary: '142.1 76.2% 36.3%',
'primary-foreground': '355.7 100% 97.3%',
secondary: '240 4.8% 95.9%',
'secondary-foreground': '240 5.9% 10%',
muted: '240 4.8% 95.9%',
'muted-foreground': '240 3.8% 46.1%',
accent: '240 4.8% 95.9%',
'accent-foreground': '240 5.9% 10%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '0 0% 98%',
border: '240 5.9% 90%',
input: '240 5.9% 90%',
ring: '142.1 76.2% 36.3%',
},
dark: {
background: '20 14.3% 4.1%',
foreground: '0 0% 95%',
popover: '0 0% 9%',
'popover-foreground': '0 0% 95%',
card: '24 9.8% 10%',
'card-foreground': '0 0% 95%',
primary: '142.1 70.6% 45.3%',
'primary-foreground': '144.9 80.4% 10%',
secondary: '240 3.7% 15.9%',
'secondary-foreground': '0 0% 98%',
muted: '0 0% 15%',
'muted-foreground': '240 5% 64.9%',
accent: '12 6.5% 15.1%',
'accent-foreground': '0 0% 98%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '0 85.7% 97.3%',
border: '240 3.7% 15.9%',
input: '240 3.7% 15.9%',
ring: '142.4 71.8% 29.2%',
},
},
},
{
name: 'blue',
label: 'Blue',
activeColor: {
light: '221.2 83.2% 53.3%',
dark: '217.2 91.2% 59.8%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '222.2 84% 4.9%',
card: '0 0% 100%',
'card-foreground': '222.2 84% 4.9%',
popover: '0 0% 100%',
'popover-foreground': '222.2 84% 4.9%',
primary: '221.2 83.2% 53.3%',
'primary-foreground': '210 40% 98%',
secondary: '210 40% 96.1%',
'secondary-foreground': '222.2 47.4% 11.2%',
muted: '210 40% 96.1%',
'muted-foreground': '215.4 16.3% 46.9%',
accent: '210 40% 96.1%',
'accent-foreground': '222.2 47.4% 11.2%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '210 40% 98%',
border: '214.3 31.8% 91.4%',
input: '214.3 31.8% 91.4%',
ring: '221.2 83.2% 53.3%',
},
dark: {
background: '222.2 84% 4.9%',
foreground: '210 40% 98%',
card: '222.2 84% 4.9%',
'card-foreground': '210 40% 98%',
popover: '222.2 84% 4.9%',
'popover-foreground': '210 40% 98%',
primary: '217.2 91.2% 59.8%',
'primary-foreground': '222.2 47.4% 11.2%',
secondary: '217.2 32.6% 17.5%',
'secondary-foreground': '210 40% 98%',
muted: '217.2 32.6% 17.5%',
'muted-foreground': '215 20.2% 65.1%',
accent: '217.2 32.6% 17.5%',
'accent-foreground': '210 40% 98%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '210 40% 98%',
border: '217.2 32.6% 17.5%',
input: '217.2 32.6% 17.5%',
ring: '224.3 76.3% 48%',
},
},
},
{
name: 'yellow',
label: 'Yellow',
activeColor: {
light: '47.9 95.8% 53.1%',
dark: '47.9 95.8% 53.1%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '20 14.3% 4.1%',
card: '0 0% 100%',
'card-foreground': '20 14.3% 4.1%',
popover: '0 0% 100%',
'popover-foreground': '20 14.3% 4.1%',
primary: '47.9 95.8% 53.1%',
'primary-foreground': '26 83.3% 14.1%',
secondary: '60 4.8% 95.9%',
'secondary-foreground': '24 9.8% 10%',
muted: '60 4.8% 95.9%',
'muted-foreground': '25 5.3% 44.7%',
accent: '60 4.8% 95.9%',
'accent-foreground': '24 9.8% 10%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '60 9.1% 97.8%',
border: '20 5.9% 90%',
input: '20 5.9% 90%',
ring: '20 14.3% 4.1%',
radius: '0.95rem',
},
dark: {
background: '20 14.3% 4.1%',
foreground: '60 9.1% 97.8%',
card: '20 14.3% 4.1%',
'card-foreground': '60 9.1% 97.8%',
popover: '20 14.3% 4.1%',
'popover-foreground': '60 9.1% 97.8%',
primary: '47.9 95.8% 53.1%',
'primary-foreground': '26 83.3% 14.1%',
secondary: '12 6.5% 15.1%',
'secondary-foreground': '60 9.1% 97.8%',
muted: '12 6.5% 15.1%',
'muted-foreground': '24 5.4% 63.9%',
accent: '12 6.5% 15.1%',
'accent-foreground': '60 9.1% 97.8%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '60 9.1% 97.8%',
border: '12 6.5% 15.1%',
input: '12 6.5% 15.1%',
ring: '35.5 91.7% 32.9%',
},
},
},
{
name: 'violet',
label: 'Violet',
activeColor: {
light: '262.1 83.3% 57.8%',
dark: '263.4 70% 50.4%',
},
cssVars: {
light: {
background: '0 0% 100%',
foreground: '224 71.4% 4.1%',
card: '0 0% 100%',
'card-foreground': '224 71.4% 4.1%',
popover: '0 0% 100%',
'popover-foreground': '224 71.4% 4.1%',
primary: '262.1 83.3% 57.8%',
'primary-foreground': '210 20% 98%',
secondary: '220 14.3% 95.9%',
'secondary-foreground': '220.9 39.3% 11%',
muted: '220 14.3% 95.9%',
'muted-foreground': '220 8.9% 46.1%',
accent: '220 14.3% 95.9%',
'accent-foreground': '220.9 39.3% 11%',
destructive: '0 84.2% 60.2%',
'destructive-foreground': '210 20% 98%',
border: '220 13% 91%',
input: '220 13% 91%',
ring: '262.1 83.3% 57.8%',
},
dark: {
background: '224 71.4% 4.1%',
foreground: '210 20% 98%',
card: '224 71.4% 4.1%',
'card-foreground': '210 20% 98%',
popover: '224 71.4% 4.1%',
'popover-foreground': '210 20% 98%',
primary: '263.4 70% 50.4%',
'primary-foreground': '210 20% 98%',
secondary: '215 27.9% 16.9%',
'secondary-foreground': '210 20% 98%',
muted: '215 27.9% 16.9%',
'muted-foreground': '217.9 10.6% 64.9%',
accent: '215 27.9% 16.9%',
'accent-foreground': '210 20% 98%',
destructive: '0 62.8% 30.6%',
'destructive-foreground': '210 20% 98%',
border: '215 27.9% 16.9%',
input: '215 27.9% 16.9%',
ring: '263.4 70% 50.4%',
},
},
},
] as const
export type Theme = (typeof themes)[number]
+23
View File
@@ -0,0 +1,23 @@
import lodash from 'lodash'
import { Registry } from 'shadcn/registry'
const { uniq, uniqBy } = lodash
const registryItemAppend = (item: Registry['items'][number], items: Registry['items']) => {
const neededRegDependencies = [
...(item.registryDependencies || []),
...items.flatMap((i) => i.registryDependencies),
]
const neededDependencies = [...(item.dependencies || []), ...items.flatMap((i) => i.dependencies)]
const neededFiles = [...(item.files || []), ...items.flatMap((i) => i.files)]
const registryBlock = {
...item,
registryDependencies: uniq(neededRegDependencies),
dependencies: uniq(neededDependencies),
files: uniqBy(neededFiles, (file) => file?.path),
}
return registryBlock
}
export { registryItemAppend }