mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 22:59:11 +03:00
562 lines
17 KiB
TypeScript
562 lines
17 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import {
|
|
SandpackProvider,
|
|
SandpackLayout,
|
|
SandpackCodeEditor,
|
|
SandpackPreview,
|
|
SandpackFileExplorer,
|
|
} from '@codesandbox/sandpack-react'
|
|
import { cn } from 'ui'
|
|
import {
|
|
Tabs_Shadcn_ as Tabs,
|
|
TabsContent_Shadcn_ as TabsContent,
|
|
TabsList_Shadcn_ as TabsList,
|
|
TabsTrigger_Shadcn_ as TabsTrigger,
|
|
} from 'ui'
|
|
import { Terminal } from 'lucide-react'
|
|
|
|
interface SandpackToggleProps {
|
|
files: Record<string, string>
|
|
theme: 'dark' | 'light'
|
|
showPreview?: boolean
|
|
dependencies?: Record<string, string>
|
|
installCommand?: string
|
|
}
|
|
|
|
export function SandpackToggle({
|
|
files,
|
|
theme,
|
|
showPreview = true,
|
|
dependencies = {},
|
|
installCommand,
|
|
}: SandpackToggleProps) {
|
|
const [activeView, setActiveView] = useState<'code' | 'preview'>(showPreview ? 'preview' : 'code')
|
|
|
|
const defaultFiles = {
|
|
...files,
|
|
'/tsconfig.json': {
|
|
code: `{
|
|
"compilerOptions": {
|
|
"target": "es5",
|
|
"lib": ["dom", "dom.iterable", "esnext"],
|
|
"allowJs": true,
|
|
"skipLibCheck": true,
|
|
"strict": true,
|
|
"forceConsistentCasingInFileNames": true,
|
|
"noEmit": true,
|
|
"esModuleInterop": true,
|
|
"module": "esnext",
|
|
"moduleResolution": "node",
|
|
"resolveJsonModule": true,
|
|
"isolatedModules": true,
|
|
"jsx": "preserve",
|
|
"incremental": true,
|
|
"plugins": [
|
|
{
|
|
"name": "next"
|
|
}
|
|
],
|
|
"paths": {
|
|
"@/*": ["./*"]
|
|
}
|
|
},
|
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
|
"exclude": ["node_modules"]
|
|
}`,
|
|
},
|
|
'/next-env.d.ts': {
|
|
code: `/// <reference types="next" />
|
|
/// <reference types="next/image-types/global" />`,
|
|
},
|
|
'/components/ui/button.tsx': {
|
|
code: `import * as React from "react"
|
|
import { Slot } from "@radix-ui/react-slot"
|
|
import { cn } from "../../lib/utils"
|
|
|
|
export interface ButtonProps
|
|
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
asChild?: boolean
|
|
}
|
|
|
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
({ className, asChild = false, ...props }, ref) => {
|
|
const Comp = asChild ? Slot : "button"
|
|
return (
|
|
<Comp
|
|
className={cn(
|
|
"inline-flex items-center justify-center 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",
|
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
className
|
|
)}
|
|
ref={ref}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
)
|
|
Button.displayName = "Button"
|
|
|
|
export { Button }`,
|
|
},
|
|
'/components/ui/dropdown-menu.tsx': {
|
|
code: `"use client"
|
|
|
|
import * as React from "react"
|
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
import { Check, ChevronRight, Circle } from "lucide-react"
|
|
|
|
import { cn } from "../../lib/utils"
|
|
|
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
|
|
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
|
|
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
|
|
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|
|
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
|
|
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
|
|
|
const DropdownMenuSubTrigger = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
inset?: boolean
|
|
}
|
|
>(({ className, inset, children, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.SubTrigger
|
|
ref={ref}
|
|
className={cn(
|
|
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
inset && "pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
<ChevronRight className="ml-auto" />
|
|
</DropdownMenuPrimitive.SubTrigger>
|
|
))
|
|
DropdownMenuSubTrigger.displayName =
|
|
DropdownMenuPrimitive.SubTrigger.displayName
|
|
|
|
const DropdownMenuSubContent = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
>(({ className, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.SubContent
|
|
ref={ref}
|
|
className={cn(
|
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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-dropdown-menu-content-transform-origin]",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
DropdownMenuSubContent.displayName =
|
|
DropdownMenuPrimitive.SubContent.displayName
|
|
|
|
const DropdownMenuContent = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.Portal>
|
|
<DropdownMenuPrimitive.Content
|
|
ref={ref}
|
|
sideOffset={sideOffset}
|
|
className={cn(
|
|
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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-dropdown-menu-content-transform-origin]",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
</DropdownMenuPrimitive.Portal>
|
|
))
|
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
|
|
|
const DropdownMenuItem = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
inset?: boolean
|
|
}
|
|
>(({ className, inset, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.Item
|
|
ref={ref}
|
|
className={cn(
|
|
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
|
|
inset && "pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
|
|
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
>(({ className, children, checked, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.CheckboxItem
|
|
ref={ref}
|
|
className={cn(
|
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
className
|
|
)}
|
|
checked={checked}
|
|
{...props}
|
|
>
|
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
<Check className="h-4 w-4" />
|
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
</span>
|
|
{children}
|
|
</DropdownMenuPrimitive.CheckboxItem>
|
|
))
|
|
DropdownMenuCheckboxItem.displayName =
|
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
|
|
|
const DropdownMenuRadioItem = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
>(({ className, children, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.RadioItem
|
|
ref={ref}
|
|
className={cn(
|
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
<Circle className="h-2 w-2 fill-current" />
|
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
</span>
|
|
{children}
|
|
</DropdownMenuPrimitive.RadioItem>
|
|
))
|
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
|
|
|
const DropdownMenuLabel = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
inset?: boolean
|
|
}
|
|
>(({ className, inset, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.Label
|
|
ref={ref}
|
|
className={cn(
|
|
"px-2 py-1.5 text-sm font-semibold",
|
|
inset && "pl-8",
|
|
className
|
|
)}
|
|
{...props}
|
|
/>
|
|
))
|
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
|
|
|
const DropdownMenuSeparator = React.forwardRef<
|
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
>(({ className, ...props }, ref) => (
|
|
<DropdownMenuPrimitive.Separator
|
|
ref={ref}
|
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
{...props}
|
|
/>
|
|
))
|
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
|
|
|
const DropdownMenuShortcut = ({
|
|
className,
|
|
...props
|
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
return (
|
|
<span
|
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
|
{...props}
|
|
/>
|
|
)
|
|
}
|
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
|
|
|
export {
|
|
DropdownMenu,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuCheckboxItem,
|
|
DropdownMenuRadioItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuShortcut,
|
|
DropdownMenuGroup,
|
|
DropdownMenuPortal,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuSubTrigger,
|
|
DropdownMenuRadioGroup,
|
|
}`,
|
|
},
|
|
'/lib/utils.ts': {
|
|
code: `import { type ClassValue, clsx } from "clsx"
|
|
import { twMerge } from "tailwind-merge"
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs))
|
|
}`,
|
|
},
|
|
'/pages/index.js': {
|
|
code: `"use client"
|
|
|
|
import { Button } from "../components/ui/button"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "../components/ui/dropdown-menu"
|
|
|
|
export default function Home() {
|
|
return (
|
|
<div className="p-8">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button>Open Menu</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem>Profile</DropdownMenuItem>
|
|
<DropdownMenuItem>Billing</DropdownMenuItem>
|
|
<DropdownMenuItem>Team</DropdownMenuItem>
|
|
<DropdownMenuItem>Subscription</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
)
|
|
}`,
|
|
active: true,
|
|
},
|
|
'/pages/_app.js': {
|
|
code: `import '../styles.css'
|
|
|
|
export default function MyApp({ Component, pageProps }) {
|
|
return <Component {...pageProps} />
|
|
}`,
|
|
active: true,
|
|
},
|
|
'/tailwind.config.js': {
|
|
code: `/** @type {import('tailwindcss').Config} */
|
|
module.exports = {
|
|
content: [
|
|
"./pages/**/*.{js,ts,jsx,tsx}",
|
|
"./components/**/*.{js,ts,jsx,tsx}",
|
|
],
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
border: "var(--border)",
|
|
input: "var(--input)",
|
|
ring: "var(--ring)",
|
|
background: "var(--background)",
|
|
foreground: "var(--foreground)",
|
|
primary: {
|
|
DEFAULT: "var(--primary)",
|
|
foreground: "var(--primary-foreground)",
|
|
},
|
|
secondary: {
|
|
DEFAULT: "var(--secondary)",
|
|
foreground: "var(--secondary-foreground)",
|
|
},
|
|
destructive: {
|
|
DEFAULT: "var(--destructive)",
|
|
foreground: "var(--destructive-foreground)",
|
|
},
|
|
muted: {
|
|
DEFAULT: "var(--muted)",
|
|
foreground: "var(--muted-foreground)",
|
|
},
|
|
accent: {
|
|
DEFAULT: "var(--accent)",
|
|
foreground: "var(--accent-foreground)",
|
|
},
|
|
popover: {
|
|
DEFAULT: "var(--popover)",
|
|
foreground: "var(--popover-foreground)",
|
|
},
|
|
card: {
|
|
DEFAULT: "var(--card)",
|
|
foreground: "var(--card-foreground)",
|
|
},
|
|
},
|
|
borderRadius: {
|
|
lg: "var(--radius)",
|
|
md: "calc(var(--radius) - 2px)",
|
|
sm: "calc(var(--radius) - 4px)",
|
|
},
|
|
},
|
|
},
|
|
plugins: [],
|
|
}`,
|
|
},
|
|
'/postcss.config.js': {
|
|
code: `module.exports = {
|
|
plugins: {
|
|
tailwindcss: {},
|
|
autoprefixer: {},
|
|
},
|
|
}`,
|
|
},
|
|
'/styles.css': {
|
|
code: `:root {
|
|
--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%;
|
|
}
|
|
|
|
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;`,
|
|
hidden: true,
|
|
},
|
|
}
|
|
|
|
return (
|
|
<div className="lg:col-span-12">
|
|
<SandpackProvider
|
|
theme={theme}
|
|
template="nextjs"
|
|
files={defaultFiles}
|
|
customSetup={{
|
|
dependencies: {
|
|
typescript: '^5.0.0',
|
|
'@types/node': '^20.0.0',
|
|
'@types/react': '^18.0.0',
|
|
'@types/react-dom': '^18.0.0',
|
|
tailwindcss: '^3.3.0',
|
|
postcss: '^8.4.31',
|
|
autoprefixer: '^10.4.14',
|
|
clsx: 'latest',
|
|
'tailwind-merge': 'latest',
|
|
'@radix-ui/react-dropdown-menu': 'latest',
|
|
'@radix-ui/react-icons': 'latest',
|
|
'lucide-react': 'latest',
|
|
'@radix-ui/react-slot': 'latest',
|
|
'class-variance-authority': 'latest',
|
|
'@radix-ui/react-checkbox': 'latest',
|
|
'@radix-ui/react-radio-group': 'latest',
|
|
'@radix-ui/react-label': 'latest',
|
|
'@radix-ui/react-select': 'latest',
|
|
'@radix-ui/react-tabs': 'latest',
|
|
'@radix-ui/react-dialog': 'latest',
|
|
'@radix-ui/react-toggle-group': 'latest',
|
|
'@radix-ui/react-switch': 'latest',
|
|
...dependencies,
|
|
},
|
|
}}
|
|
options={{
|
|
bundlerTimeOut: 60000,
|
|
autorun: true,
|
|
recompileMode: 'immediate',
|
|
}}
|
|
>
|
|
<div className="mb-4 flex items-center justify-between gap-2">
|
|
<Tabs
|
|
value={activeView}
|
|
onValueChange={(value) => setActiveView(value as 'code' | 'preview')}
|
|
>
|
|
<TabsList className="gap-0 border-none bg-muted rounded p-1 h-auto">
|
|
{showPreview && (
|
|
<TabsTrigger
|
|
className="px-3 py-1 rounded data-[state=active]:bg-background border-none"
|
|
value="preview"
|
|
>
|
|
Preview
|
|
</TabsTrigger>
|
|
)}
|
|
<TabsTrigger
|
|
className="px-3 py-1 rounded data-[state=active]:bg-background border-none"
|
|
value="code"
|
|
>
|
|
Code
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
</Tabs>
|
|
{installCommand && (
|
|
<div className="bg-muted rounded p-3 py-2 font-mono text-xs shrink-0 flex items-center gap-2">
|
|
<Terminal className="w-4 h-4 mr-2" />
|
|
{installCommand}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<SandpackLayout style={{ height: '500px' }}>
|
|
<SandpackFileExplorer
|
|
className={cn('!h-full', activeView === 'code' ? 'block' : '!hidden')}
|
|
/>
|
|
|
|
<SandpackCodeEditor
|
|
className={cn('!h-full', activeView === 'code' ? 'block' : '!hidden')}
|
|
showTabs={false}
|
|
showLineNumbers
|
|
/>
|
|
{showPreview && (
|
|
<SandpackPreview
|
|
className={cn(
|
|
'!h-full w-full',
|
|
activeView === 'preview' ? 'opacity-100 relative' : '!hidden'
|
|
)}
|
|
/>
|
|
)}
|
|
</SandpackLayout>
|
|
</SandpackProvider>
|
|
</div>
|
|
)
|
|
}
|