diff --git a/.github/workflows/docs-sync-auto-troubleshooting.yml b/.github/workflows/docs-sync-auto-troubleshooting.yml index bb315d0607b7c..333f14f173fb5 100644 --- a/.github/workflows/docs-sync-auto-troubleshooting.yml +++ b/.github/workflows/docs-sync-auto-troubleshooting.yml @@ -59,9 +59,16 @@ jobs: repository: supabase/troubleshooting path: troubleshooting-upstream + - name: Generate PR token + id: pr-token + uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + with: + app-id: ${{ secrets.GH_AUTOFIX_APP_ID }} + private-key: ${{ secrets.GH_AUTOFIX_PRIVATE_KEY }} + - name: Sync supabase/troubleshooting changes back to supabase/supabase env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ steps.pr-token.outputs.token }} run: | git config user.name 'github-docs-bot' git config user.email 'github-docs-bot@supabase.com' diff --git a/apps/learn/.gitignore b/apps/learn/.gitignore new file mode 100644 index 0000000000000..57c2e5aba20fa --- /dev/null +++ b/apps/learn/.gitignore @@ -0,0 +1,38 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +.contentlayer \ No newline at end of file diff --git a/apps/learn/README.md b/apps/learn/README.md new file mode 100644 index 0000000000000..04cb9c788f9cf --- /dev/null +++ b/apps/learn/README.md @@ -0,0 +1,54 @@ +Plan: +Course 1: Supabase Foundations Learn the basics of Supabase: database, auth, and RLS. 5 chapters. + +- + +Course 2: Project: Smart Office 15 Build a realtime room-booking dashboard using Supabase. 15 chapters. +Course 3: Supabase Internals: Performance & Scaling. Learn how to profile queries, tune indexes, and scale Postgres with Supabase. 20 chapters. +Course 4: Supabase Internals: Debugging & Operations. Understand how to diagnose slow queries, use read replicas, and manage production workloads. 20 chapters. + +——— +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. + +## Supabase types + +To regenerate the Supabase database types, run + +``` +supabase gen types --local > registry/default/fixtures/database.types.ts +``` diff --git a/apps/learn/app/(app)/[...slug]/page.tsx b/apps/learn/app/(app)/[...slug]/page.tsx new file mode 100644 index 0000000000000..7eabcd2ead1f0 --- /dev/null +++ b/apps/learn/app/(app)/[...slug]/page.tsx @@ -0,0 +1,158 @@ +import { metadata as mainMetadata } from '@/app/layout' +import { ChapterCompletion } from '@/components/chapter-completion' +import { CourseHero } from '@/components/course-hero' +import { ExploreMore } from '@/components/explore-more' +import { Mdx } from '@/components/mdx-components' +import { NextUp } from '@/components/next-up' +import { DashboardTableOfContents } from '@/components/toc' +import { getTableOfContents } from '@/lib/toc' +import { getCurrentChapter } from '@/lib/get-current-chapter' +import { getNextPage } from '@/lib/get-next-page' +import { absoluteUrl, cn } from '@/lib/utils' +import '@/styles/code-block-variables.css' +import '@/styles/mdx.css' +import { allDocs } from 'contentlayer/generated' +import { ChevronRight } from 'lucide-react' +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import Balancer from 'react-wrap-balancer' +import { ScrollArea } from 'ui' + +interface DocPageProps { + params: Promise<{ + slug: string[] + }> +} + +async function getDocFromParams({ params }: { params: { slug: string[] } }) { + const slug = params.slug?.join('/') || '' + const doc = allDocs.find((doc) => doc.slugAsParams === slug) + + if (!doc) { + return null + } + + return doc +} + +export async function generateMetadata(props: DocPageProps): Promise { + const params = await props.params + const doc = await getDocFromParams({ params }) + // get page params so we can check if it's the introduction page + const slugSegments = doc?.slugAsParams.split('/') + const isIntroductionPage = slugSegments?.[slugSegments.length - 1] === 'introduction' + + if (!doc) { + return {} + } + + const metadata: Metadata = { + ...mainMetadata, + title: doc.title, + description: doc.description, + openGraph: { + ...(mainMetadata.openGraph || {}), + title: doc.title, + description: doc.description, + type: 'article', + url: absoluteUrl(doc.slug), + }, + } + return metadata +} + +export async function generateStaticParams(): Promise<{ slug: string[] }[]> { + return allDocs.map((doc) => ({ + slug: doc.slugAsParams.split('/'), + })) +} + +export default async function DocPage(props: DocPageProps) { + const params = await props.params + const doc = await getDocFromParams({ params }) + + if (!doc) { + notFound() + } + + const toc = await getTableOfContents(doc.body.raw) + const nextPage = getNextPage(doc.slugAsParams) + const currentChapter = getCurrentChapter(doc.slugAsParams) + const slugSegments = doc.slugAsParams.split('/') + const isIntroductionPage = slugSegments[slugSegments.length - 1] === 'introduction' + + const exploreItems = ( + doc as { + explore?: Array<{ title: string; link: string; itemType?: string; description?: string }> + } + ).explore + + return ( +
+ {isIntroductionPage && doc.courseHero && ( + + )} + +
+
+
+
Learn
+ +
{doc.title}
+
+
+
+

{doc.title}

+ {doc.description && ( +

+ {doc.description} +

+ )} +
+
+
+ +
+ {exploreItems && exploreItems.length > 0 && } + {currentChapter && nextPage && ( + + )} + {nextPage && ( +
+ +
+ )} +
+ {doc.toc && ( +
+
+ +
+ +
+
+
+
+ )} +
+
+ ) +} diff --git a/apps/learn/app/(app)/layout.tsx b/apps/learn/app/(app)/layout.tsx new file mode 100644 index 0000000000000..6092ea6114cd9 --- /dev/null +++ b/apps/learn/app/(app)/layout.tsx @@ -0,0 +1,31 @@ +import { Sidebar } from '@/components/sidebar' +import { SiteFooter } from '@/components/site-footer' +import { TelemetryWrapper } from './telemetry-wrapper' + +interface AppLayoutProps { + children: React.ReactNode +} + +export default function AppLayout({ children }: AppLayoutProps) { + return ( + <> + {/* main container */} +
+ {/* main content */} +
+ {/* {children} */} +
+
+ +
+
{children}
+
+
+
+
+
+ + + + ) +} diff --git a/apps/learn/app/(app)/page.tsx b/apps/learn/app/(app)/page.tsx new file mode 100644 index 0000000000000..d2fae7dd40fc9 --- /dev/null +++ b/apps/learn/app/(app)/page.tsx @@ -0,0 +1,136 @@ +import { WhatWillILearn } from '@/components/what-will-i-learn' +import Link from 'next/link' +import { Activity, BookOpen, Database, Gauge, Wrench } from 'lucide-react' +import { Badge, Button, Card, CardDescription, CardHeader } from 'ui' + +// Horizontal grid line component +const HorizontalGridLine = () =>
+ +export const dynamic = 'force-dynamic' +export const revalidate = 0 + +export default function Home() { + const courses = [ + { + id: 1, + title: 'Supabase Foundations', + description: 'Learn the basics of Supabase: database, auth, and RLS.', + chapters: 5, + icon: Database, + level: 'Beginner', + }, + { + id: 2, + title: 'Project: Smart Office', + description: 'Build a realtime room-booking dashboard using Supabase.', + chapters: 15, + icon: Activity, + level: 'Intermediate', + }, + { + id: 3, + title: 'Supabase Internals: Performance & Scaling', + description: 'Learn how to profile queries, tune indexes, and scale Postgres with Supabase.', + chapters: 20, + icon: Gauge, + level: 'Advanced', + }, + { + id: 4, + title: 'Supabase Internals: Debugging & Operations', + description: + 'Understand how to diagnose slow queries, use read replicas, and manage production workloads.', + chapters: 20, + icon: Wrench, + level: 'Advanced', + }, + ] + return ( +
+
+ {/* Component Showcase with Grid */} +
+ {/* Grid Container */} +
+ {/* Grid Lines - Vertical (Columns) */} + {Array.from({ length: 13 }).map((_, i) => ( +
+ ))} + + {/* Grid Content */} +
+ {/* Heading Section */} +
+
+
+

+ Learn Supabase +

+

+ Learn how to build your own projects with Supabase. Our courses and projects + help you get started no matter your skill level, teaching you how to build + production-ready apps. +

+
+
+ +
+

Courses

+ +
+ {courses.map((course) => { + const Icon = course.icon + return ( + + +
+
+ +
+
+
+

{course.title}

+ + {course.level} + +
+ + {course.description} + +
+
+ + + {course.chapters} chapters + +
+ +
+
+
+
+
+ ) + })} +
+
+
+ +
+
+
+
+
+
+
+ ) +} diff --git a/apps/learn/app/(app)/telemetry-wrapper.tsx b/apps/learn/app/(app)/telemetry-wrapper.tsx new file mode 100644 index 0000000000000..124d9c5477ad6 --- /dev/null +++ b/apps/learn/app/(app)/telemetry-wrapper.tsx @@ -0,0 +1,16 @@ +'use client' + +import { API_URL } from '@/lib/constants' +import { IS_PLATFORM, PageTelemetry } from 'common' +import { useConsentToast } from 'ui-patterns/consent' + +export const TelemetryWrapper = () => { + const { hasAcceptedConsent } = useConsentToast() + return ( + + ) +} diff --git a/apps/learn/app/Providers.tsx b/apps/learn/app/Providers.tsx new file mode 100644 index 0000000000000..24d7eac1223c2 --- /dev/null +++ b/apps/learn/app/Providers.tsx @@ -0,0 +1,26 @@ +'use client' + +import { Provider as JotaiProvider } from 'jotai' +import { ThemeProvider as NextThemesProvider } from 'next-themes' +import { ThemeProviderProps } from 'next-themes/dist/types' + +import { FrameworkProvider } from '@/context/framework-context' +import { MobileMenuProvider } from '@/context/mobile-menu-context' +import { AuthProvider } from 'common' +import { TooltipProvider } from 'ui' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return ( + + + + + + {children} + + + + + + ) +} diff --git a/apps/learn/app/SonnerToast.tsx b/apps/learn/app/SonnerToast.tsx new file mode 100644 index 0000000000000..819a8890e9dfd --- /dev/null +++ b/apps/learn/app/SonnerToast.tsx @@ -0,0 +1,9 @@ +'use client' + +import { useConfig } from '@/hooks/use-config' +import { SonnerToaster as Toaster } from 'ui' + +export function SonnerToaster() { + const [config] = useConfig() + return +} diff --git a/apps/learn/app/favicon.ico b/apps/learn/app/favicon.ico new file mode 100644 index 0000000000000..718d6fea4835e Binary files /dev/null and b/apps/learn/app/favicon.ico differ diff --git a/apps/learn/app/layout.tsx b/apps/learn/app/layout.tsx new file mode 100644 index 0000000000000..8903f15159d9f --- /dev/null +++ b/apps/learn/app/layout.tsx @@ -0,0 +1,60 @@ +import type { Metadata } from 'next' + +import '@/styles/globals.css' +import { API_URL } from '@/lib/constants' +import { FeatureFlagProvider, TelemetryTagManager } from 'common' +import { genFaviconData } from 'common/MetaFavicons/app-router' +import { Inter } from 'next/font/google' +import { ThemeProvider } from './Providers' +import { SonnerToaster } from './SonnerToast' + +const inter = Inter({ subsets: ['latin'] }) + +const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? '' + +export const metadata: Metadata = { + applicationName: 'Learn Supabase', + title: 'Learn Supabase', + description: 'Learn Supabase.', + metadataBase: new URL('https://supabase.com/learn'), + icons: genFaviconData(BASE_PATH), + openGraph: { + type: 'article', + authors: 'Supabase', + url: `${BASE_PATH}`, + images: `${BASE_PATH}/img/supabase-og-image.png`, + publishedTime: new Date().toISOString(), + modifiedTime: new Date().toISOString(), + }, + twitter: { + card: 'summary_large_image', + site: '@supabase', + creator: '@supabase', + images: `${BASE_PATH}/img/supabase-og-image.png`, + }, +} + +interface RootLayoutProps { + children: React.ReactNode +} + +export default async function Layout({ children }: RootLayoutProps) { + return ( + + + + + + + {children} + + + + + + ) +} diff --git a/apps/learn/components/callout.tsx b/apps/learn/components/callout.tsx new file mode 100644 index 0000000000000..63ed3afc9b8ab --- /dev/null +++ b/apps/learn/components/callout.tsx @@ -0,0 +1,17 @@ +import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_ } from 'ui' + +interface CalloutProps { + icon?: string + title?: string + children?: React.ReactNode +} + +export function Callout({ title, children, icon, ...props }: CalloutProps) { + return ( + + {icon && {icon}} + {title && {title}} + {children} + + ) +} diff --git a/apps/learn/components/chapter-completion.tsx b/apps/learn/components/chapter-completion.tsx new file mode 100644 index 0000000000000..d214849cd8985 --- /dev/null +++ b/apps/learn/components/chapter-completion.tsx @@ -0,0 +1,147 @@ +'use client' + +import { Check } from 'lucide-react' +import { useEffect, useRef, useState } from 'react' +import { useLocalStorage } from './use-local-storage' + +interface ChapterCompletionProps { + chapterNumber: number + completionMessage?: string +} + +export function ChapterCompletion({ chapterNumber, completionMessage }: ChapterCompletionProps) { + const [completedChapters, setCompletedChapters] = useLocalStorage( + 'completed-chapters', + [] + ) + const [isCompleted, setIsCompleted] = useState(false) + const [isVisible, setIsVisible] = useState(false) + const containerRef = useRef(null) + const timerRef = useRef(null) + + // Check if chapter is already completed on mount + useEffect(() => { + if (completedChapters.includes(chapterNumber)) { + setIsCompleted(true) + } + }, [chapterNumber, completedChapters]) + + useEffect(() => { + const container = containerRef.current + if (!container) return + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setIsVisible(true) + } else { + setIsVisible(false) + // Reset timer if user scrolls away + if (timerRef.current) { + clearTimeout(timerRef.current) + timerRef.current = null + } + } + }) + }, + { + threshold: 0.5, // Trigger when 50% of the component is visible + rootMargin: '0px', + } + ) + + observer.observe(container) + + return () => { + observer.disconnect() + if (timerRef.current) { + clearTimeout(timerRef.current) + } + } + }, []) + + useEffect(() => { + if (isVisible && !isCompleted) { + // Start timer when component becomes visible + timerRef.current = setTimeout(() => { + setIsCompleted(true) + // Save to local storage + if (!completedChapters.includes(chapterNumber)) { + setCompletedChapters([...completedChapters, chapterNumber]) + } + }, 5000) // 5 seconds + } else if (!isVisible && timerRef.current) { + // Clear timer if user scrolls away before completion + clearTimeout(timerRef.current) + timerRef.current = null + } + + return () => { + if (timerRef.current) { + clearTimeout(timerRef.current) + } + } + }, [isVisible, isCompleted, chapterNumber, completedChapters, setCompletedChapters]) + + return ( +
+
+ {/* Large circle with chapter number */} +
+
+ + {chapterNumber} + +
+ + {/* Small checkmark circle overlapping bottom-right */} +
+ +
+
+
+ {/* Completion text */} +

+ You've completed Chapter {chapterNumber} +

+ + {completionMessage && ( +

+ {completionMessage} +

+ )} +
+
+
+ ) +} diff --git a/apps/learn/components/command-copy-button.tsx b/apps/learn/components/command-copy-button.tsx new file mode 100644 index 0000000000000..8aa85d40fc545 --- /dev/null +++ b/apps/learn/components/command-copy-button.tsx @@ -0,0 +1,77 @@ +'use client' + +import { Check, Copy } from 'lucide-react' +import { useEffect, useState } from 'react' + +import { useSendTelemetryEvent } from 'lib/telemetry' +import { Button_Shadcn_ } from 'ui' + +export function CommandCopyButton({ command }: { command: string }) { + const [copied, setCopied] = useState(false) + const sendTelemetryEvent = useSendTelemetryEvent() + + useEffect(() => { + if (copied) { + const timeout = setTimeout(() => setCopied(false), 2000) + return () => clearTimeout(timeout) + } + }, [copied]) + + const parseCommandForTelemetry = (cmd: string) => { + // Extract framework from URL (e.g., 'nextjs' from 'password-based-auth-nextjs.json') + const frameworkMatch = cmd.match(/ui\/r\/.*?-(nextjs|react|react-router|tanstack)\.json/) + + // if the block doesn't have a framework defined (like infinite query), default to react + const framework = frameworkMatch + ? (frameworkMatch[1] as 'nextjs' | 'react-router' | 'tanstack' | 'react') + : 'react' + + // Extract package manager from command prefix (npx, pnpm, yarn, bun) + const packageManager = cmd.startsWith('npx') + ? ('npm' as const) + : cmd.startsWith('pnpm') + ? ('pnpm' as const) + : cmd.startsWith('yarn') + ? ('yarn' as const) + : cmd.startsWith('bun') + ? ('bun' as const) + : ('npm' as const) + + // Extract template title from URL (e.g., 'password-based-auth' from 'password-based-auth-nextjs.json') + const titleMatch = cmd.match(/\/ui\/r\/(.*?)\.json/) + const title = (titleMatch ? titleMatch[1] : '').replaceAll(`-${framework}`, '') + + return { + framework, + packageManager, + title, + } + } + + return ( + { + // Copy command to clipboard + navigator.clipboard.writeText(command) + setCopied(true) + + // Parse command and send telemetry event + const { framework, packageManager, title } = parseCommandForTelemetry(command) + + sendTelemetryEvent({ + action: 'supabase_ui_command_copy_button_clicked', + properties: { + templateTitle: title, + command: command, + framework, + packageManager, + }, + }) + }} + > + {copied ? : } + + ) +} diff --git a/apps/learn/components/command-menu.tsx b/apps/learn/components/command-menu.tsx new file mode 100644 index 0000000000000..d764ce7e38f4b --- /dev/null +++ b/apps/learn/components/command-menu.tsx @@ -0,0 +1,112 @@ +'use client' + +import { CircleIcon, LaptopIcon, MoonIcon, SunIcon } from 'lucide-react' +import { useTheme } from 'next-themes' +import { useRouter } from 'next/navigation' +import * as React from 'react' + +import { COMMAND_ITEMS } from '@/config/docs' +import { cn } from '@/lib/utils' +import { + Button, + CommandDialog, + CommandEmpty_Shadcn_, + CommandGroup_Shadcn_, + CommandInput_Shadcn_, + CommandItem_Shadcn_, + CommandList_Shadcn_, + CommandSeparator_Shadcn_, + DialogProps, +} from 'ui' + +export function CommandMenu({ ...props }: DialogProps) { + const router = useRouter() + const [open, setOpen] = React.useState(false) + const { setTheme } = useTheme() + + React.useEffect(() => { + const down = (e: KeyboardEvent) => { + if ((e.key === 'k' && (e.metaKey || e.ctrlKey)) || e.key === '/') { + if ( + (e.target instanceof HTMLElement && e.target.isContentEditable) || + e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement || + e.target instanceof HTMLSelectElement + ) { + return + } + + e.preventDefault() + setOpen((open) => !open) + } + } + + document.addEventListener('keydown', down) + return () => document.removeEventListener('keydown', down) + }, []) + + const runCommand = React.useCallback((command: () => unknown) => { + setOpen(false) + command() + }, []) + + return ( + <> + + + + + No results found. + + {COMMAND_ITEMS.map((navItem) => ( + runCommand(() => router.push(navItem.href as string))} + > +
+ +
+ {navItem.label} +
+ ))} +
+ + + runCommand(() => setTheme('light'))}> + + Light + + runCommand(() => setTheme('dark'))}> + + Dark + + runCommand(() => setTheme('classic-dark'))}> + + Classic dark + + runCommand(() => setTheme('system'))}> + + System + + +
+
+ + ) +} diff --git a/apps/learn/components/command.tsx b/apps/learn/components/command.tsx new file mode 100644 index 0000000000000..c3c5ce6240d52 --- /dev/null +++ b/apps/learn/components/command.tsx @@ -0,0 +1,102 @@ +'use client' + +import { motion } from 'framer-motion' +import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui' +import { CommandCopyButton } from './command-copy-button' +import { useLocalStorage } from './use-local-storage' + +interface CommandCopyProps { + name: string + highlight?: boolean + // For Vue, we need to use the `shadcn-vue` package instead of `shadcn` + framework?: 'react' | 'vue' +} + +type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun' + +const LOCAL_STORAGE_KEY = 'package-manager-copy-command' + +const getBaseUrl = () => { + if (process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'production') { + // we have a special alias for the production environment, added in https://github.com/shadcn-ui/ui/pull/8161 + return `@supabase` + } else if (process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'preview') { + return `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}` + } else { + return 'http://localhost:3004' + } +} + +const getComponentPath = (name: string) => { + if (process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'production') { + return `/${name}` + } else { + return `${process.env.NEXT_PUBLIC_BASE_PATH ?? ''}/r/${name}.json` + } +} + +export function Command({ name, highlight, framework = 'react' }: CommandCopyProps) { + const [value, setValue] = useLocalStorage(LOCAL_STORAGE_KEY, 'npm') + + const baseUrl = getBaseUrl() + const componentPath = getComponentPath(name) + + const commands: Record = + framework === 'vue' + ? { + npm: `npx shadcn-vue@latest add ${baseUrl}${componentPath}`, + pnpm: `pnpm dlx shadcn-vue@latest add ${baseUrl}${componentPath}`, + yarn: `yarn dlx shadcn-vue@latest add ${baseUrl}${componentPath}`, + bun: `bunx --bun shadcn-vue@latest add ${baseUrl}${componentPath}`, + } + : { + npm: `npx shadcn@latest add ${baseUrl}${componentPath}`, + pnpm: `pnpm dlx shadcn@latest add ${baseUrl}${componentPath}`, + yarn: `yarn dlx shadcn@latest add ${baseUrl}${componentPath}`, + bun: `bunx --bun shadcn@latest add ${baseUrl}${componentPath}`, + } + + return ( + +
+ {highlight && ( + + )} + +
+ + {(Object.keys(commands) as PackageManager[]).map((manager) => ( + + {manager} + + ))} + + + {(Object.keys(commands) as PackageManager[]).map((manager) => ( + +
+
+ $ + {commands[manager]} +
+
+ +
+
+
+ ))} +
+
+
+ ) +} diff --git a/apps/learn/components/copy-button.tsx b/apps/learn/components/copy-button.tsx new file mode 100644 index 0000000000000..f2e84e2429eec --- /dev/null +++ b/apps/learn/components/copy-button.tsx @@ -0,0 +1,40 @@ +'use client' + +import { Check, Copy } from 'lucide-react' +import * as React from 'react' + +import { Button, cn, copyToClipboard } from 'ui' + +interface CopyButtonProps extends React.HTMLAttributes { + value: string + src?: string +} + +export function CopyButton({ value, className, src, ...props }: CopyButtonProps) { + const [hasCopied, setHasCopied] = React.useState(false) + + React.useEffect(() => { + setTimeout(() => { + setHasCopied(false) + }, 2000) + }, [hasCopied]) + + return ( + + ) +} diff --git a/apps/learn/components/course-hero.tsx b/apps/learn/components/course-hero.tsx new file mode 100644 index 0000000000000..2b30e227ab761 --- /dev/null +++ b/apps/learn/components/course-hero.tsx @@ -0,0 +1,68 @@ +import { BookA, Clock, GraduationCap } from 'lucide-react' + +interface CourseHeroProps { + title: string + subtitle: string + description: string + stats?: { + label: string + icon: React.ReactNode + accent: string + }[] + instructors?: { + name: string + icon: React.ReactNode + accent: string + }[] +} + +export function CourseHero({ title, subtitle, description, instructors }: CourseHeroProps) { + return ( +
+ {/* Chapter label */} +
+ + + Chapter Introduction + +
+ +

+ {title} +

+ +

+ Learn the foundations of Supabase, the Postgres development platform. +

+ +

+ In this short course, you'll explore how Supabase brings together Database, Auth, + Storage, Edge Functions, and Realtime into a unified developer platform. +

+ + {/* Course metadata */} +
+
+
+ +
+ 5 Chapters +
+ +
+
+ +
+ ~1 hour +
+ +
+
+ +
+ Beginner +
+
+
+ ) +} diff --git a/apps/learn/components/explore-more.tsx b/apps/learn/components/explore-more.tsx new file mode 100644 index 0000000000000..4145774bb3c8e --- /dev/null +++ b/apps/learn/components/explore-more.tsx @@ -0,0 +1,110 @@ +import Link from 'next/link' +import { BookOpen, Code, Users, ExternalLink, BookA } from 'lucide-react' +import { IconYoutubeSolid } from 'ui' + +interface ExploreItem { + title: string + link: string + itemType?: string + description?: string +} + +interface ExploreMoreProps { + items: ExploreItem[] +} + +// Icon mapping based on itemType +function getIcon(itemType?: string) { + if (!itemType) return BookOpen + const lowerType = itemType.toLowerCase() + if (lowerType === 'doc' || lowerType === 'documentation') { + return BookA + } + if (lowerType === 'reference' || lowerType === 'api') { + return Code + } + if (lowerType === 'community' || lowerType === 'forum') { + return Users + } + if (lowerType === 'video' || lowerType === 'tutorial') { + return IconYoutubeSolid + } + return BookA // default icon +} + +// Default descriptions based on itemType +function getDescription(itemType?: string): string { + if (!itemType) return 'Learn more about this topic' + const lowerType = itemType.toLowerCase() + if (lowerType === 'doc' || lowerType === 'documentation') { + return 'Complete guides and references' + } + if (lowerType === 'reference' || lowerType === 'api') { + return 'Detailed API documentation' + } + if (lowerType === 'community' || lowerType === 'forum') { + return 'Get help from the community' + } + if (lowerType === 'video' || lowerType === 'tutorial') { + return 'Watch video tutorials and guides' + } + return 'Learn more about this topic' +} + +export function ExploreMore({ items }: ExploreMoreProps) { + if (!items || items.length === 0) { + return null + } + + return ( +
+
+

+ Explore More +

+
+ {items.map((item, index) => { + const isExternal = item.link.startsWith('http://') || item.link.startsWith('https://') + const Icon = getIcon(item.itemType) + const description = item.description || getDescription(item.itemType) + + const cardContent = ( +
+
+ +
+
+ +
+

{item.title}

+

{description}

+
+
+
+ ) + + if (isExternal) { + return ( + + {cardContent} + + ) + } + + return ( + + {cardContent} + + ) + })} +
+
+
+ ) +} diff --git a/apps/learn/components/mdx-components.tsx b/apps/learn/components/mdx-components.tsx new file mode 100644 index 0000000000000..d71686970ef02 --- /dev/null +++ b/apps/learn/components/mdx-components.tsx @@ -0,0 +1,186 @@ +import { useMDXComponent } from 'next-contentlayer2/hooks' +import Link from 'next/link' + +import { + Accordion_Shadcn_ as Accordion, + AccordionContent_Shadcn_ as AccordionContent, + AccordionItem_Shadcn_ as AccordionItem, + AccordionTrigger_Shadcn_ as AccordionTrigger, + cn, +} from 'ui' +import { Callout } from './callout' +import { CopyButton } from './copy-button' +import TanStackBeta from './tanstack-beta' + +const components = { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, + h1: ({ className, ...props }: React.HTMLAttributes) => ( +

+ ), + h2: ({ className, ...props }: React.HTMLAttributes) => ( +

+ ), + h3: ({ className, ...props }: React.HTMLAttributes) => ( +

+ ), + h4: ({ className, ...props }: React.HTMLAttributes) => ( +

+ ), + h5: ({ className, ...props }: React.HTMLAttributes) => ( +
+ ), + h6: ({ className, ...props }: React.HTMLAttributes) => ( +
+ ), + a: ({ className, ...props }: React.HTMLAttributes) => ( + + ), + p: ({ className, ...props }: React.HTMLAttributes) => ( +

+ ), + ul: ({ className, ...props }: React.HTMLAttributes) => ( +

    + ), + ol: ({ className, ...props }: React.HTMLAttributes) => ( +
      + ), + li: ({ className, ...props }: React.HTMLAttributes) => ( +
    1. + ), + blockquote: ({ className, ...props }: React.HTMLAttributes) => ( +
      + ), + img: ({ className, alt, ...props }: React.ImgHTMLAttributes) => ( + // eslint-disable-next-line @next/next/no-img-element + {alt} + ), + hr: ({ ...props }: React.HTMLAttributes) => ( +
      + ), + table: ({ className, ...props }: React.HTMLAttributes) => ( +
      + + + ), + tr: ({ className, ...props }: React.HTMLAttributes) => ( + + ), + th: ({ className, ...props }: React.HTMLAttributes) => ( +
      + ), + td: ({ className, ...props }: React.HTMLAttributes) => ( + + ), + pre: ({ + className, + __rawString__, + __withMeta__, + __src__, + ...props + }: React.HTMLAttributes & { + __rawString__?: string + __withMeta__?: boolean + __src__?: string + }) => { + return ( +
      +
      +        {__rawString__ && (
      +          
      +        )}
      +      
      + ) + }, + code: ({ className, ...props }: React.HTMLAttributes) => ( + + ), + Callout, + CopyButton, + TanStackBeta, + Card: ({ className, ...props }: React.HTMLAttributes) => ( +
      + ), + LinkedCard: ({ className, ...props }: React.ComponentProps) => ( + + ), +} + +interface MdxProps { + code: string +} + +export function Mdx({ code }: MdxProps) { + const Component = useMDXComponent(code, { + style: 'default', + }) + + return ( +
      + +
      + ) +} diff --git a/apps/learn/components/mobile-menu-sheet.tsx b/apps/learn/components/mobile-menu-sheet.tsx new file mode 100644 index 0000000000000..4e7c3cc019076 --- /dev/null +++ b/apps/learn/components/mobile-menu-sheet.tsx @@ -0,0 +1,18 @@ +'use client' + +import { useMobileMenu } from '@/hooks/use-mobile-menu' +import { Sheet } from 'ui' + +interface MobileMenuSheetProps { + children: React.ReactNode +} + +export function MobileMenuSheet({ children }: MobileMenuSheetProps) { + const { open, setOpen } = useMobileMenu() + + return ( + + {children} + + ) +} diff --git a/apps/learn/components/next-up.tsx b/apps/learn/components/next-up.tsx new file mode 100644 index 0000000000000..b05ee51b4bf2d --- /dev/null +++ b/apps/learn/components/next-up.tsx @@ -0,0 +1,38 @@ +import Link from 'next/link' +import { ArrowRight } from 'lucide-react' +import { Button } from 'ui' + +interface NextUpProps { + title: string + description: string + href: string + chapterNumber?: number +} + +export function NextUp({ title, description, href, chapterNumber }: NextUpProps) { + return ( +
      +
      +
      +

      Next Up

      +

      + {chapterNumber && {chapterNumber}: } + {title} +

      +

      {description}

      +
      + + + +
      +
      +
      +
      + ) +} diff --git a/apps/learn/components/side-navigation-item.tsx b/apps/learn/components/side-navigation-item.tsx new file mode 100644 index 0000000000000..619f4ae6bb1b3 --- /dev/null +++ b/apps/learn/components/side-navigation-item.tsx @@ -0,0 +1,168 @@ +'use client' + +import Link, { LinkProps } from 'next/link' +import { usePathname } from 'next/navigation' +import React, { useState, useEffect } from 'react' +import { ChevronRight, Lock } from 'lucide-react' + +import { useMobileMenu } from '@/hooks/use-mobile-menu' +import { SidebarNavItem } from '@/types/nav' +import { Badge, cn } from 'ui' + +// We extend: +// 1. LinkProps - for Next.js Link component props (prefetch, etc) +// 2. AnchorHTMLAttributes - for standard HTML anchor props (className, etc) +// We omit href from both since we compute it internally from item.href +interface NavigationItemProps + extends Omit, + Omit, 'href'> { + item: SidebarNavItem + onClick?: (e: React.MouseEvent) => void + level?: number + internalPaths?: string[] + isLoggedIn?: boolean +} + +const NavigationItem: React.FC = ({ + item, + onClick, + level = 0, + internalPaths, + isLoggedIn = true, + ...props +}) => { + const { setOpen } = useMobileMenu() + const pathname = usePathname() + const [isOpen, setIsOpen] = useState(false) + + const pathParts = pathname.split('/') + const slug = pathParts[pathParts.length - 1] + + const hasChildren = item.items && item.items.length > 0 + + // Check if internal content exists for this item and user is logged in + const hasInternal = item.href && internalPaths?.includes(item.href) && isLoggedIn + + // Auto-expand if any child is active + useEffect(() => { + if (hasChildren) { + const hasActiveChild = item.items?.some((child) => { + return pathname === child.href + }) + if (hasActiveChild) { + setIsOpen(true) + } + } + }, [hasChildren, item.items, pathname]) + + // Use item.href if available, otherwise build from slug + let href = item.href + if (!href && slug) { + href = `/docs/${slug}` + } + + // Determine if this link represents the current page + const isActive = pathname === href + + const handleLinkClick = (e: React.MouseEvent) => { + // Close the mobile menu when navigating + setOpen(false) + + // Call the onClick prop if it exists + if (onClick) { + onClick(e) + } + } + + const handleButtonClick = (e: React.MouseEvent) => { + e.preventDefault() + setIsOpen(!isOpen) + } + + const itemClasses = cn( + 'flex text-sm rounded-md transition-colors', + level === 0 ? 'px-3 py-2' : 'px-3 py-1.5', + isActive + ? 'bg-surface-200 text-foreground' + : hasChildren && isOpen + ? 'bg-surface-100 text-foreground' + : 'text-foreground-lighter hover:bg-surface-100 hover:text-foreground' + ) + + return ( +
    2. + {hasChildren ? ( + <> + + {isOpen && ( +
        + {item.items?.map((childItem, i) => ( + + ))} +
      + )} + + ) : ( + <> + + + {item.title} + {item.new && ( + + NEW + + )} + + + {hasInternal && ( + + + + {item.title} (Internal) + + + )} + + )} +
    3. + ) +} + +NavigationItem.displayName = 'NavigationItem' + +export default NavigationItem diff --git a/apps/learn/components/side-navigation.tsx b/apps/learn/components/side-navigation.tsx new file mode 100644 index 0000000000000..83c393b68d9c4 --- /dev/null +++ b/apps/learn/components/side-navigation.tsx @@ -0,0 +1,162 @@ +'use client' + +import Link from 'next/link' +import { useIsLoggedIn, useUser, logOut } from 'common' +import { AuthenticatedDropdownMenu, type menuItem } from 'ui-patterns' +import { LogOut, Settings, UserIcon } from 'lucide-react' +import { useRouter } from 'next/navigation' + +import NavigationItem from '@/components/side-navigation-item' +import { courses } from '@/config/docs' +import { mergeInternalContentIntoSections } from '@/lib/merge-internal-content' +import { SidebarNavItem } from '@/types/nav' +import { CommandMenu } from './command-menu' +import { ThemeSwitcherDropdown } from './theme-switcher-dropdown' + +interface SideNavigationProps { + internalPaths: string[] +} + +function SideNavigation({ internalPaths }: SideNavigationProps) { + const isLoggedIn = useIsLoggedIn() + const user = useUser() + const router = useRouter() + + // First, merge orphaned internal content into their respective sections + // Note: All content is visible to everyone. Auth is only for saving progress. + const coursesWithInternalContent = { + ...courses, + items: mergeInternalContentIntoSections(courses.items, internalPaths), + } + + // User menu for authenticated dropdown + const userMenu: menuItem[][] = [ + [ + { + label: user?.email ?? "You're logged in", + type: 'text', + icon: UserIcon, + }, + { + label: 'Account Preferences', + icon: Settings, + href: 'https://supabase.com/dashboard/account/me', + }, + ], + [ + { + label: 'Logout', + type: 'button', + icon: LogOut, + onClick: async () => { + await logOut() + router.refresh() + }, + }, + ], + ] + + return ( + + ) +} + +export default SideNavigation diff --git a/apps/learn/components/sidebar.tsx b/apps/learn/components/sidebar.tsx new file mode 100644 index 0000000000000..7b6e4c93fdd25 --- /dev/null +++ b/apps/learn/components/sidebar.tsx @@ -0,0 +1,36 @@ +import { Menu } from 'lucide-react' + +import SideNavigation from '@/components/side-navigation' +import { getInternalContentPaths } from '@/lib/get-internal-content' +import { Button, ScrollArea, SheetContent, SheetTrigger } from 'ui' +import { MobileMenuSheet } from './mobile-menu-sheet' +import { ThemeSwitcherDropdown } from './theme-switcher-dropdown' + +export function Sidebar() { + // Get internal content paths on server side + const internalPaths = Array.from(getInternalContentPaths()) + + return ( + <> +
      + + +
      + + + + ) +} diff --git a/apps/learn/components/site-footer.tsx b/apps/learn/components/site-footer.tsx new file mode 100644 index 0000000000000..d44c37194ef13 --- /dev/null +++ b/apps/learn/components/site-footer.tsx @@ -0,0 +1,29 @@ +export function SiteFooter() { + return ( + + ) +} diff --git a/apps/learn/components/tanstack-beta.tsx b/apps/learn/components/tanstack-beta.tsx new file mode 100644 index 0000000000000..84c68949adb00 --- /dev/null +++ b/apps/learn/components/tanstack-beta.tsx @@ -0,0 +1,29 @@ +import { TriangleAlert } from 'lucide-react' +import { Callout } from './callout' + +export default function TanStackBeta() { + return ( +
      + +
      +
      + + Heads up: TanStack Start is in beta. +
      + We're excited to support TanStack Start in our UI library! But since it's still + in beta, things may change quickly — expect breaking changes and some rough edges. + We'll do our best to keep up and make integration with Supabase as smooth as + possible. If you run into issues, have a look at the TanStack docs. + + TanStack Quickstart guide + +
      +
      +
      + ) +} diff --git a/apps/learn/components/theme-switcher-dropdown.tsx b/apps/learn/components/theme-switcher-dropdown.tsx new file mode 100644 index 0000000000000..e52a515d143ff --- /dev/null +++ b/apps/learn/components/theme-switcher-dropdown.tsx @@ -0,0 +1,91 @@ +'use client' + +import { Moon, Sun } from 'lucide-react' +import { useTheme } from 'next-themes' +import { useEffect, useState } from 'react' + +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + RadioGroup_Shadcn_, + Theme, + singleThemes, +} from 'ui' + +const ThemeSwitcherDropdown = () => { + const [mounted, setMounted] = useState(false) + const { theme, setTheme, resolvedTheme } = useTheme() + + /** + * Avoid Hydration Mismatch + * https://github.com/pacocoursey/next-themes?tab=readme-ov-file#avoid-hydration-mismatch + */ + // useEffect only runs on the client, so now we can safely show the UI + useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + return null + } + + function SingleThemeSelection() { + return ( +
      + +
      + ) + } + + const iconClasses = 'text-foreground-light group-data-[state=open]:text-foreground' + + return ( + <> + + + + + + Theme + + setTheme(themeValue)} + > + {singleThemes.map((theme: Theme) => ( + + {theme.name} + + ))} + + + + + ) +} + +export { ThemeSwitcherDropdown } diff --git a/apps/learn/components/toc.tsx b/apps/learn/components/toc.tsx new file mode 100644 index 0000000000000..d796b84518005 --- /dev/null +++ b/apps/learn/components/toc.tsx @@ -0,0 +1,107 @@ +// @ts-nocheck +'use client' + +import * as React from 'react' + +import { TableOfContents } from '@/lib/toc' +import { cn } from '@/lib/utils' +import { useMounted } from '@/hooks/use-mounted' + +interface TocProps { + toc: TableOfContents +} + +export function DashboardTableOfContents({ toc }: TocProps) { + const itemIds = React.useMemo( + () => + toc.items + ? toc.items + .flatMap((item) => [item.url, item?.items?.map((item) => item.url)]) + .flat() + .filter(Boolean) + .map((id) => id?.split('#')[1]) + : [], + [toc] + ) + const activeHeading = useActiveItem(itemIds) + const mounted = useMounted() + + if (!toc?.items || !mounted) { + return null + } + + return ( +
      +

      On This Page

      + +
      + ) +} + +function useActiveItem(itemIds: string[]) { + const [activeId, setActiveId] = React.useState(null) + + React.useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id) + } + }) + }, + { rootMargin: `0% 0% -80% 0%` } + ) + + itemIds?.forEach((id) => { + const element = document.getElementById(id) + if (element) { + observer.observe(element) + } + }) + + return () => { + itemIds?.forEach((id) => { + const element = document.getElementById(id) + if (element) { + observer.unobserve(element) + } + }) + } + }, [itemIds]) + + return activeId +} + +interface TreeProps { + tree: TableOfContents + level?: number + activeItem?: string +} + +function Tree({ tree, level = 1, activeItem }: TreeProps) { + return tree?.items?.length && level < 3 ? ( +
        + {tree.items.map((item, index) => { + return ( +
      • + + {item.title} + + {item.items?.length ? ( + + ) : null} +
      • + ) + })} +
      + ) : null +} diff --git a/apps/learn/components/use-local-storage.tsx b/apps/learn/components/use-local-storage.tsx new file mode 100644 index 0000000000000..0ef31e76346ee --- /dev/null +++ b/apps/learn/components/use-local-storage.tsx @@ -0,0 +1,47 @@ +// Reference: https://usehooks.com/useLocalStorage/ + +import { useCallback, useState } from 'react' + +export function useLocalStorage(key: string, initialValue: T) { + // State to store our value + // Pass initial state function to useState so logic is only executed once + const [storedValue, setStoredValue] = useState(() => { + if (typeof window === 'undefined') { + return initialValue + } + + try { + // Get from local storage by key + const item = window.localStorage.getItem(key) + // Parse stored json or if none return initialValue + return item ? JSON.parse(item) : initialValue + } catch (error) { + // If error also return initialValue + console.log(error) + return initialValue + } + }) + + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue = useCallback( + (value: T | ((val: T) => T)) => { + try { + // Allow value to be a function so we have same API as useState + const valueToStore = value instanceof Function ? value(storedValue) : value + // Save state + setStoredValue(valueToStore) + // Save to local storage + if (typeof window !== 'undefined') { + window.localStorage.setItem(key, JSON.stringify(valueToStore)) + } + } catch (error) { + // A more advanced implementation would handle the error case + console.log(error) + } + }, + [key, storedValue] + ) + + return [storedValue, setValue] as const +} diff --git a/apps/learn/components/what-will-i-learn.tsx b/apps/learn/components/what-will-i-learn.tsx new file mode 100644 index 0000000000000..4be8a0522c21f --- /dev/null +++ b/apps/learn/components/what-will-i-learn.tsx @@ -0,0 +1,138 @@ +import { + BarChart3, + CalendarRange, + Database, + History, + Lock, + Network, + Rocket, + Timer, +} from 'lucide-react' +import Image from 'next/image' +import { Card, CardDescription, CardHeader, CardTitle } from 'ui' + +const learningTracks = [ + { + icon: Database, + title: 'Model and manage your data', + description: 'Design relational schemas, automate migrations, and keep your project tidy.', + takeaways: [ + 'set up tables, relationships, and seed data', + 'work with realtime subscriptions and triggers', + 'version and deploy database changes safely', + ], + }, + { + icon: Lock, + title: 'Secure every request', + description: 'Master authentication and Row Level Security to keep data scoped correctly.', + takeaways: [ + 'configure supabase auth providers', + 'write row level security policies with confidence', + 'audit, monitor, and debug access issues fast', + ], + }, + { + icon: Network, + title: 'Go realtime by default', + description: 'Stream updates into your UI with channels, presence, and edge functions.', + takeaways: [ + 'broadcast updates with realtime channels', + 'listen for database changes from the client', + 'connect edge functions to your realtime flows', + ], + }, + { + icon: Rocket, + title: 'Deploy production-grade apps', + description: 'Integrate storage, vector search, and observability for resilient launches.', + takeaways: [ + 'serve files securely from supabase storage', + 'index and query embeddings for ai features', + 'monitor performance and scale without surprises', + ], + }, +] + +export function WhatWillILearn() { + return ( +
      +
      +

      What will I learn?

      + +

      + In the fundamental courses, you'll learn all the basics: +

      +
      + +
      + {learningTracks.map((track) => { + const Icon = track.icon + return ( + + + + + {track.title} + + + {track.description} + + + + ) + })} +
      +
      +

      Smart Office project

      +

      + In the Smart Office project, you'll learn how to build a full-featured Dashboard: +

      +
      +
      + Smart Office project dashboard screenshot +
      +
      +
        +
      • + + + + Real-time room monitoring that tracks live occupancy, sensor readings +
      • +
      • + + + + Booking management with live status filters, calendar integrations. +
      • +
      • + + + + + Advanced analytics that surface 30+ days of utilization trends and patterns. + +
      • +
      • + + + + + Detect capacity issues, open facilities tasks, and track resolution SLAs without + leaving the app. + +
      • +
      +
      +
      +
      +
      + ) +} diff --git a/apps/learn/config/docs.ts b/apps/learn/config/docs.ts new file mode 100644 index 0000000000000..f65e051d37845 --- /dev/null +++ b/apps/learn/config/docs.ts @@ -0,0 +1,74 @@ +import { SidebarNavGroup } from 'types/nav' + +export const courses: SidebarNavGroup = { + title: 'Courses', + items: [ + { + title: 'Foundations', + href: '/foundations', + items: [ + // ✅ Always visible (no requiresAuth) + { + title: 'Introduction', + href: '/foundations/introduction', + }, + { + title: 'Architecture', + href: '/foundations/architecture', + }, + { + title: 'Data Fundamentals', + href: '/foundations/data-fundamentals', + }, + { + title: 'Authentication', + href: '/foundations/authentication', + }, + { + title: 'Security', + href: '/foundations/security', + }, + { + title: 'Realtime', + href: '/foundations/realtime', + }, + { + title: 'Storage', + href: '/foundations/storage', + }, + { + title: 'Edge Functions', + href: '/foundations/edge-functions', + }, + + { + title: 'Vector Search', + href: '/foundations/vector', + }, + ], + commandItemLabel: 'Foundations', + }, + { + title: 'Project: Smart Office', + href: '/projects/smart-office', + commandItemLabel: 'Project: Smart Office', + }, + { + title: 'Performance & Scaling', + href: '/internals/performance-scaling', + commandItemLabel: 'Supabase Internals: Performance & Scaling', + }, + { + title: 'Debugging & Operations', + href: '/internals/debugging-operations', + commandItemLabel: 'Supabase Internals: Debugging & Operations', + }, + ], +} + +export const COMMAND_ITEMS = [ + ...courses.items.map((item) => ({ + label: item.commandItemLabel, + href: item.href, + })), +] diff --git a/apps/learn/content/foundations/architecture.mdx b/apps/learn/content/foundations/architecture.mdx new file mode 100644 index 0000000000000..160e7a8bc5aee --- /dev/null +++ b/apps/learn/content/foundations/architecture.mdx @@ -0,0 +1,179 @@ +--- +title: Architecture +description: Learn how Supabase's core products work together. +chapterNumber: 1 + +explore: + - title: Supabase Docs + link: https://supabase.com/docs/guides/database/overview + itemType: doc + - title: Postgres for Beginners + link: https://www.postgresqltutorial.com/ + itemType: doc +--- + +This page explains how Supabase is structured and how its main services work +together. Supabase is built on top of a powerful open source database called +Postgres, but it includes much more than a database. Around Postgres, +Supabase adds services for authentication, serverless functions, file storage, realtime updates, AI-powered search, and easy APIs to access it — all designed to work easily together. + +The goal is to give you a complete backend that feels consistent and connected, +so you can focus on building your app instead of wiring together separate tools. + +--- + +## The mental model + +At the center of Supabase is Postgres. Every other part connects to it. + +```text +Clients (web, mobile, scripts) + | + v + API layer (REST via PostgREST, GraphQL) + | + v + Postgres <---- Realtime (listens for changes) + | ^ + | | + v | + Storage | Auth (manages users) + | | Edge Functions (runs code) + v | Vector Search (pgvector) + Files + URLs Studio, CLI, Policies (RLS) +``` + +You interact with Supabase mainly through the API layer or the client libraries. +The API talks to Postgres, and Postgres enforces security rules that you define. +All the other services stay in sync because they share the same database. + +--- + +## The main components + +### Database (Postgres) + +Your data lives in Postgres tables. You can design tables, columns, and +relationships using SQL or the visual editor in Supabase Studio. Supabase +manages backups, scaling, and migrations for you. + +**Why this matters:** Postgres is a full relational database with features like +foreign keys, indexes, and extensions. Supabase gives you these tools without +the usual setup. + +--- + +### API layer (REST and GraphQL) + +Supabase automatically exposes your tables as APIs using PostgREST (for REST) +and PostGraphile (for GraphQL). You can read, insert, update, and delete rows +with simple HTTP requests. You can also expose your own SQL functions as custom +endpoints. + +**Why this matters:** You can connect any frontend or script directly to your +database without building a separate API server. + +--- + +### Auth (users and tokens) + +Auth manages users and issues tokens that identify them. When someone signs in, +their token is sent with every request, and Postgres uses that identity to apply +the right access rules. + +**Why this matters:** Authentication is built into your database layer, so you +don't need an external service to handle sign-ups or sessions. + +--- + +### Row Level Security (RLS) and policies + +RLS lets you write SQL rules that decide who can read or modify each row in your +tables. These policies apply everywhere — through REST, GraphQL, or Realtime. + +**Why this matters:** Security lives directly in the database. You define it +once, and it's enforced across every API call. + +--- + +### Storage (files and buckets) + +Storage provides an S3-style interface for storing files such as images, videos, +and documents. File metadata lives in Postgres, so you can apply the same access +policies you use for your data. + +**Why this matters:** File access and data access follow the same security rules, +which keeps your system consistent and simple. + +--- + +### Realtime + +Realtime listens to changes in Postgres and sends them to connected clients over +WebSocket. You can subscribe to table events and see updates as they happen. + +**Why this matters:** You can build collaborative or live-updating interfaces +without managing your own socket server. + +--- + +### Edge Functions + +Edge Functions let you run server-side JavaScript or TypeScript using Deno. +They're good for webhooks, background tasks, and any logic that shouldn't run +on the client. + +**Why this matters:** You can write custom backend logic without maintaining a +full server or worrying about scaling. + +--- + +### Vector Search + +The pgvector extension adds support for storing and searching **embeddings** — +numerical representations of text, images, or other data. This allows you to +search by meaning, not just by keywords. + +**Why this matters:** You can add AI-style semantic search or recommendations +directly to your database. + +--- + +### Studio and CLI + +Supabase Studio is the web dashboard for managing your project: running SQL, +browsing data, editing policies, and managing users. The CLI is for local +development and migrations, so your schema can live alongside your code. + +**Why this matters:** You can move smoothly between GUI and code, keeping your +database versioned and reproducible. + +--- + +## How everything connects + +When your app makes a request, it goes through a simple path: + +1. The client calls a Supabase API endpoint, sending a project key and (usually) + a user token. +2. The API turns that request into a SQL query. +3. Postgres runs the query and applies any RLS policies. +4. The API returns the results as JSON. +5. If there are changes, Realtime broadcasts them to any connected clients that + are allowed to see them. + +Edge Functions can follow the same pattern — they can call the APIs or connect +directly to Postgres when they need more control. + +--- + +## Why this architecture helps + +- **Everything shares one source of truth:** Postgres is at the center. +- **Security is consistent:** RLS policies apply across every service. +- **No separate backend required:** APIs are generated automatically from you database. +- **You can grow gradually:** Start with just a table, add realtime, storage, or functions later. +- **It's just Postgres:** You can use standard SQL, extensions, and tools. + +Supabase gives you the power of Postgres with the convenience of a fully +integrated backend platform. diff --git a/apps/learn/content/foundations/authentication.mdx b/apps/learn/content/foundations/authentication.mdx new file mode 100644 index 0000000000000..7c2935d5a76ff --- /dev/null +++ b/apps/learn/content/foundations/authentication.mdx @@ -0,0 +1,125 @@ +--- +title: Authentication +description: Learn how to manage users, and how signed-in requests connect to the database. +chapterNumber: 3 +--- + +Most applications need to know who is using them. Authentication is how we identify +a user and attach their actions to their own data. Supabase Authentication works closely with the database, so each request carries the user's identity. + +Authentication in Supabase is handled by the Supabase Auth service. It manages creating accounts, signing in, and identifying the user on each request. + +--- + +## Users + +A user in Supabase has an id and some optional profile information (such as +email, phone number, name, etc.). When someone signs up, Supabase creates a new user record and assigns +them a unique identifier. + +You do not need to create or manage the users table yourself. Supabase stores it +internally and keeps it updated. + +Example of the kind of data Supabase stores for a user: + +```text +id: 74d23f1b-1c94-3e7b-4d6a-9fa71e2a3c4ba987 +email: maria@example.com +created_at: 2025-01-12 09:44:21 +updated_at: 2025-01-12 09:44:21 +last_sign_in_at: 2025-01-12 09:44:21 +``` + +You can reference this user id in your tables when you want to link data to a +specific user. + +For example, you can associate notes with a user by storing the user's id in the note table. + +```text +| id | user_id | content | created_at | +|----|--------------------------------------|------------------|----------------------| +| 1 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Buy groceries | 2025-01-10 14:22:11 | +| 2 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Call the dentist | 2025-01-10 15:08:54 | +``` + +--- + +## Tokens and identity + +When a user signs in, Supabase returns a token. This token proves who the user +is. This token is stored locally (usually in memory or local +storage) and automatically included in each API request. + +Inside the database, this token becomes the value returned by `auth.uid()`. +This lets security policies decide which data belongs to which user. + +For example: + +```text +select auth.uid(); +``` + +returns the id of the signed-in user who made the request. + +--- + +## Sign up and sign in + +Supabase Auth supports several sign-in methods: + +- Email and password +- Magic links (sign in by clicking a link in an email) +- Social providers (Google, GitHub, etc.) +- Phone number (SMS) + +For now, we will use email and password, since it is the simplest starting point. + +--- + +## Where authentication connects to data + +Authentication tells us who the user is. But it does not decide what they are +allowed to access. To handle that part, we use a Postgres feature called Row Level Security (RLS). + +RLS lets the database check each row before returning it. The database uses the +user's id to decide whether that row should be visible or changeable. This means +the rules for access live inside the database itself, not in the application +code. + +From our previous notes example, RLS would allow a user to select only their own notes, but not the notes of other users. + +So the flow looks like this: + +```text +| id | user_id | content | created_at | +|----|--------------------------------------|------------------|----------------------| +| 1 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Buy groceries | 2025-01-10 14:22:11 | +| 2 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Call the dentist | 2025-01-10 15:08:54 | +``` + +```text +User signs in +↓ +Supabase gives the client a user token +↓ +Client sends token with API requests +↓ +Postgres receives token and sets auth.uid() +↓ +RLS policies use auth.uid() to enforce access rules +``` + +Authentication says who the user is. +RLS decides what they are allowed to do. + +--- + +## What we learned + +- Supabase Auth manages user accounts and sign-in. +- Each user has a unique id that you can reference in your tables. +- When a user signs in, Supabase provides a token. +- The token tells the database who the user is through `auth.uid()`. +- Authentication by itself does not control access. RLS does that part. + +Understanding this identity flow is important before we begin writing policies. diff --git a/apps/learn/content/foundations/data-fundamentals.mdx b/apps/learn/content/foundations/data-fundamentals.mdx new file mode 100644 index 0000000000000..e355449f432ae --- /dev/null +++ b/apps/learn/content/foundations/data-fundamentals.mdx @@ -0,0 +1,144 @@ +--- +title: Data Fundamentals +description: 'Learn how data is structured in Postgres: tables, rows, columns, and keys.' +chapterNumber: 2 + +explore: + - title: Supabase Docs + link: https://supabase.com/docs/guides/database/overview + itemType: doc + - title: Postgres for Beginners + link: https://www.postgresqltutorial.com/ + itemType: reference +--- + +Before we store any information in Supabase, we need to understand how data is +organized in Postgres. Supabase uses Postgres as its database, which means data +is arranged in a structured and predictable way. This helps keep your app +organized, consistent, and easy to query. + +The core ideas are simple: you create tables to hold related data, and each +table has rows (records) and columns (fields). Once you understand that pattern, +you can model almost anything. + +--- + +## Tables, rows, and columns + +A table is like a spreadsheet. It has: + +- **Columns**: the fields that define what kind of data the table stores +- **Rows**: the individual entries in the table + +Imagine a table for storing notes: + +```text +| id | content | created_at | +|----|------------------|----------------------| +| 1 | Buy groceries | 2025-01-10 14:22:11 | +| 2 | Call the dentist | 2025-01-10 15:08:54 | +``` + +- Each **row** is a single note. +- Each **column** represents one piece of information about the note. + +--- + +## Data types + +Every column has a data type. This describes the kind of value that can be +stored there. + +Some common data types: + +```text +| Data type | Example value | Meaning | +| ----------- | ---------------- | ----------------------------- | +| text | Hello world | A string of characters | +| integer | 42 | A number without decimals | +| boolean | true | Yes/no or on/off values | +| timestamptz | 2025-01-10 14:22 | Date and time (with timezone) | +| json | {"name": "John"} | A JSON object | +``` + +Choosing correct types makes your data easier to work with and protects you from +unexpected values. + +--- + +## Primary keys + +Most tables include a primary key. This is a column that uniquely identifies each +row. In many Supabase projects, this column is named `id` and uses a type called +`uuid`, which automatically generates a unique value. + +You create this column in the table definition like this: + +```text +id uuid primary key default gen_random_uuid() +``` + +You generally don't need to think about it after setting it up. It simply gives +each record a stable identity. + +--- + +## Relationships between tables + +You can link tables together using foreign keys. This is how you express things +like: + +- A user has many notes +- A project has many tasks +- A team has many members + +For example: + +```text +| id | user_id | content | created_at | +|----|--------------------------------------|------------------|----------------------| +| 1 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Buy groceries | 2025-01-10 14:22:11 | +| 2 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Call the dentist | 2025-01-10 15:08:54 | +``` + +Here, `user_id` refers to a row in a `users` table. This means the note belongs +to that user, and we can see that this user has two notes. + +Relationships let you organize data without repeating information. + +--- + +## Why relational data is useful in practice + +Relational data is really useful in practice because it allows you to: + +- Keep data valid with constraints
      + _Example: no duplicate emails._ + +- Link records safely with foreign keys
      + _Example: delete a user and their notes in one action._ + +- Express real permissions with RLS
      + _Example: users can read their own notes; admins can read all._ + +- Query exactly what you need with table joins
      + _Example: fetch notes with the author's email._ + +--- + +## What we learned + +A database in Supabase is made up of tables. +Each table has: + +- columns (which describe the type of information stored) +- rows (the individual pieces of data in that table) + +You can relate tables to each other using foreign keys. +This lets you model real situations, such as: + +- a user has many notes +- a team has many members +- a project has many tasks + +Thinking in tables, rows, and relationships is the foundation of working with data in Supabase. diff --git a/apps/learn/content/foundations/edge-functions.mdx b/apps/learn/content/foundations/edge-functions.mdx new file mode 100644 index 0000000000000..add8c13d7d260 --- /dev/null +++ b/apps/learn/content/foundations/edge-functions.mdx @@ -0,0 +1,7 @@ +--- +title: Edge Functions +description: 'Learn how to run custom backend logic close to your users, using Deno and the Supabase serverless runtime.' +chapterNumber: 6 +--- + +Soon... diff --git a/apps/learn/content/foundations/index.mdx b/apps/learn/content/foundations/index.mdx new file mode 100644 index 0000000000000..11b9e1111ab0d --- /dev/null +++ b/apps/learn/content/foundations/index.mdx @@ -0,0 +1,69 @@ +--- +title: Supabase Foundations +description: Learn the foundations of Supabase, the Postgres development platform. +courseHero: + title: Supabase Foundations + subtitle: Learn the foundations of Supabase, the Postgres development platform. + description: In this short course, you'll explore how Supabase brings together **Database**, **Auth**, **Storage**, **Edge Functions**, and **Realtime** into a unified developer platform. + instructors: + - name: Jane Doe + gradientFrom: '#00c3ff' + gradientTo: '#ffff1c' + - name: John Doe + gradientFrom: '#ff00ea' +--- + +In this short course, you'll explore how Supabase brings together **Database**, **Auth**, **Storage**, **Edge Functions**, and **Realtime** into a unified developer platform. + +By the end, you'll understand how each piece of Supabase fits together, how to persist and protect your data, and how to start confidently building your own applications. + +## What you'll learn + +- **Architecture**: how Supabase's core products work together. +- **Data fundamentals**: create tables, insert and retrieve data, and explore our generated APIs. +- **Authentication**: create and sign in users, and make secure requests. +- **Security**: enable Row Level Security (RLS) and write policies to protect your data. +- **Storage**: upload and serve files using buckets. +- **Edge Functions**: run custom backend logic close to your users, using Deno and Supabase's serverless runtime. +- **Vector search**: store and query embeddings for AI-powered search and recommendations. + +--- + +## Prerequisites + +- A [Supabase account](https://supabase.com/) (free) +- Basic familiarity with a terminal to run queries. + +--- + +## Course outline + +The course is split into 7 chapters, each approximately 10 minutes long. + +1. **What is Supabase?** + Get an overview of Supabase's architecture and core products. Create your first project and make a simple API call. +2. **Your First Table** + Create a Postgres table in Supabase, insert data, and query it through the REST API. +3. **Authentication Basics** + Add users, sign them in, and understand how Supabase secures your APIs with JWTs. +4. **Secure Your Data with Row-Level Security** + Enable Row-Level Security and write policies to ensure users only access their own data. +5. **Storage** + Store files in Supabase Storage and upload and serve files from your buckets. +6. **Edge Functions** + Deploy simple functions to run backend logic close to your users using Deno. +7. **Vector Search** + Store and query embeddings for AI-powered search and recommendations using Supabase Vector. + +--- + +## Before you begin + +Create a new project at [supabase.com](https://supabase.com), open the **Supabase Dashboard**, and grab your **API URL** and **anon key** from the [Connect dialog](https://supabase.com/dashboard/project/_/settings/api?showConnect=true&connectTab=frameworks). +You'll use them throughout the course to send real requests to your database and APIs. + +--- + +## Duration + +Approx. **1 hour** — a fast, hands-on introduction to the Supabase platform. diff --git a/apps/learn/content/foundations/introduction.mdx b/apps/learn/content/foundations/introduction.mdx new file mode 100644 index 0000000000000..4c378e9efe559 --- /dev/null +++ b/apps/learn/content/foundations/introduction.mdx @@ -0,0 +1,79 @@ +--- +title: Supabase Foundations +description: Learn the foundations of Supabase, the Postgres development platform. + +explore: + - title: Supabase Docs + link: https://supabase.com/docs + itemType: doc + +courseHero: + title: Supabase Foundations + subtitle: Learn the foundations of Supabase, the Postgres development platform. + description: In this short course, you'll explore how Supabase brings together **Database**, **Auth**, **Storage**, **Edge Functions**, and **Realtime** into a unified developer platform. + stats: + - label: 5 Chapters + icon: book-open + accent: emerald + - label: ~1 hour + icon: clock + accent: blue + - label: Beginner + icon: zap + accent: amber +--- + +In this short course, you'll explore how Supabase brings together **Database**, **Auth**, **Storage**, **Edge Functions**, and **Realtime** into a unified developer platform. + +By the end, you'll understand how each piece of Supabase fits together, how to persist and protect your data, and how to start confidently building your own applications. + +## What you'll learn + +- **Architecture**: how Supabase's core products work together. +- **Data fundamentals**: create tables, insert and retrieve data, and explore our generated APIs. +- **Authentication**: create and sign in users, and make secure requests. +- **Security**: enable Row Level Security (RLS) and write policies to protect your data. +- **Storage**: upload and serve files using buckets. +- **Edge Functions**: run custom backend logic close to your users, using Deno and Supabase's serverless runtime. +- **Vector search**: store and query embeddings for AI-powered search and recommendations. + +--- + +## Prerequisites + +- A [Supabase account](https://supabase.com/) (free) +- Basic familiarity with a terminal to run queries. + +--- + +## Course outline + +The course is split into 7 chapters, each approximately 10 minutes long. + +1. **What is Supabase?** + Get an overview of Supabase's architecture and core products. Create your first project and make a simple API call. +2. **Your First Table** + Create a Postgres table in Supabase, insert data, and query it through the REST API. +3. **Authentication Basics** + Add users, sign them in, and understand how Supabase secures your APIs with JWTs. +4. **Secure Your Data with Row-Level Security** + Enable Row-Level Security and write policies to ensure users only access their own data. +5. **Storage** + Store files in Supabase Storage and upload and serve files from your buckets. +6. **Edge Functions** + Deploy simple functions to run backend logic close to your users using Deno. +7. **Vector Search** + Store and query embeddings for AI-powered search and recommendations using Supabase Vector. + +--- + +## Before you begin + +Create a new project at [supabase.com](https://supabase.com), open the **Supabase Dashboard**, and grab your **API URL** and **anon key** from the [Connect dialog](https://supabase.com/dashboard/project/_/settings/api?showConnect=true&connectTab=frameworks). +You'll use them throughout the course to send real requests to your database and APIs. + +--- + +## Duration + +Approx. **1 hour** — a fast, hands-on introduction to the Supabase platform. diff --git a/apps/learn/content/foundations/quickstart.mdx b/apps/learn/content/foundations/quickstart.mdx new file mode 100644 index 0000000000000..e33c066d63fa3 --- /dev/null +++ b/apps/learn/content/foundations/quickstart.mdx @@ -0,0 +1,77 @@ +--- +title: Quick Start +description: Install shadcn/ui and use the components in your project +--- + +## Set up shadcn/ui + +Start by installing [shadcn/ui](https://ui.shadcn.com/) in your project. Use the appropriate setup guide linked below for your framework of choice. + +If you're already using a recent version of shadcn/ui in your existing project, you can skip this step—this library should work with your current setup. However, if you encounter issues, try deleting your existing `components.json` file and follow one of the guides to reset your setup. + +
      + + + Next.js + + +

      Next.js

      +
      +

      + `$``npx create-next-app -e with-supabase` +

      + +
      +
      + + + + +

      React Router

      +
      + + + + +

      TanStack Start

      +
      + + + React SPA + + +

      React SPA

      +
      +
      + + + +When you've finished setting up shadcn/ui, you can start exploring the components in the sidebar + +## Troubleshooting + +If you encounter issues, try deleting your existing `components.json` file and follow one of the guides to reset your setup. + +For issues and questions about specific frameworks, see the [FAQs](/ui/docs/getting-started/faq) diff --git a/apps/learn/content/foundations/realtime.mdx b/apps/learn/content/foundations/realtime.mdx new file mode 100644 index 0000000000000..2ec81c00f5b75 --- /dev/null +++ b/apps/learn/content/foundations/realtime.mdx @@ -0,0 +1,7 @@ +--- +title: Realtime +description: 'Learn how to build real-time applications with Supabase.' +chapterNumber: 6 +--- + +Soon... diff --git a/apps/learn/content/foundations/security.mdx b/apps/learn/content/foundations/security.mdx new file mode 100644 index 0000000000000..2dd21f181b66c --- /dev/null +++ b/apps/learn/content/foundations/security.mdx @@ -0,0 +1,112 @@ +--- +title: Security +description: Learn how Supabase uses Row Level Security (RLS) to control which data each user can access. +chapterNumber: 4 +explore: + - title: Supabase Docs + link: https://supabase.com/docs/guides/database/postgres/row-level-security + itemType: doc + - title: RLS and Policies + link: https://www.youtube.com/watch?v=WAa3a-HxLVs + itemType: video +--- + +Authentication tells us who the user is. But we still need to decide what data +each user is allowed to see or change. Supabase handles this using a Postgres +feature called Row Level Security (RLS). + +RLS lets the database check each row before returning it. For every request, the +database asks: _should this user be allowed to access this row?_ + +Here is an example of a table with some notes: + +```text ++--------------------------------------------------------------------------+ +| user_id | content | created_at | ++---------+------------------+---------------------------------------------+ +| 1 | Buy groceries | 2025-01-10 14:22:11 | ← belongs to user Alice +| 2 | Call the dentist | 2025-01-10 15:08:54 | ← belongs to user Bob ++--------------------------------------------------------------------------+ +``` + +Because this check happens in the database, the rules can be applied to every request: +REST, GraphQL, Realtime, and Edge Functions all follow the same security +behavior. There is no separate code path to maintain. + +--- + +## Enabling RLS + +RLS is turned off by default so you can design your tables first. Once you are +ready to secure a table, you turn it on via the Dashboard or via SQL: + +```text +ALTER TABLE users ENABLE ROW LEVEL SECURITY; +``` + +Enabling RLS does not allow any access. It simply tells Postgres that row access +must now follow policies. + +At this point, your table becomes locked down. No one can read or write data +until you add policies. + +--- + +## Policies + +A policy is a rule that describes who can do what. Policies are written in SQL, +but we will use very small, readable examples. + +Example: Allow a signed-in user to see only their own rows. + +```text +create policy "Users can view their own notes" +on notes +for select +using (user_id = auth.uid()); +``` + +Explanation: + +- `for select` means the policy applies to reading rows +- `auth.uid()` is the id of the signed-in user +- The user is only allowed to read rows where the `user_id` matches theirs + +This gives us personal data isolation without writing any backend code. + +--- + +## A mental model for policies + +Think of policies as sentences: +`Allow X to do Y when Z is true.` + +For example: + +```text +Allow a user to read a note when they created it. +Allow a user to update a note when they created it. +Allow a user to delete an item when they own the project it belongs to. +Allow an admin to do everything. +``` + +Policies describe access in plain language. +SQL just expresses the rule. + +In a typical app, a table may have one policy or several. Each policy represents +a small piece of business logic — who can do what and under what conditions. +Together, they form the full access rules for that table. + +--- + +## What we learned + +- Authentication identifies the user. +- RLS decides what data that user can access. +- RLS is a Postgres feature that checks every row before returning it. +- When RLS is enabled, access is blocked until policies are added. +- Policies describe who can read or write data and under what conditions. +- `auth.uid()` gives you the id of the signed-in user, which is used in policy + conditions. + +Understanding RLS is the foundation of secure Supabase applications. diff --git a/apps/learn/content/foundations/storage.mdx b/apps/learn/content/foundations/storage.mdx new file mode 100644 index 0000000000000..c27587a9081b0 --- /dev/null +++ b/apps/learn/content/foundations/storage.mdx @@ -0,0 +1,7 @@ +--- +title: Storage +description: 'Learn how to store files in Supabase using buckets.' +chapterNumber: 5 +--- + +Soon... diff --git a/apps/learn/content/foundations/vector.mdx b/apps/learn/content/foundations/vector.mdx new file mode 100644 index 0000000000000..2e8e5a939193a --- /dev/null +++ b/apps/learn/content/foundations/vector.mdx @@ -0,0 +1,7 @@ +--- +title: Vector Search +description: 'Learn how to store and query embeddings for AI-powered search and recommendations.' +chapterNumber: 8 +--- + +Soon... diff --git a/apps/learn/content/internal/foundations/grafana.mdx b/apps/learn/content/internal/foundations/grafana.mdx new file mode 100644 index 0000000000000..b756b9a13503c --- /dev/null +++ b/apps/learn/content/internal/foundations/grafana.mdx @@ -0,0 +1,8 @@ +--- +title: Grafana +description: 'Learn how to use Grafana' +chapterNumber: 8 +--- + +This is an internal guide for Grafana. +It doesn't have an external equivalent. diff --git a/apps/learn/content/internal/foundations/realtime.mdx b/apps/learn/content/internal/foundations/realtime.mdx new file mode 100644 index 0000000000000..82de849b829f8 --- /dev/null +++ b/apps/learn/content/internal/foundations/realtime.mdx @@ -0,0 +1,7 @@ +--- +title: Realtime +description: 'Learn how to build real-time applications with Supabase.' +chapterNumber: 6 +--- + +This is the internal realtime guide. diff --git a/apps/learn/content/platform/platform-kit.mdx b/apps/learn/content/platform/platform-kit.mdx new file mode 100644 index 0000000000000..85ec55eb21cc9 --- /dev/null +++ b/apps/learn/content/platform/platform-kit.mdx @@ -0,0 +1,103 @@ +--- +title: Platform Kit +description: The easiest way to build platforms on top of Supabase +--- + + + +## Installation + +See the [Supabase Platform Kit documentation](https://supabase.com/docs) for installation instructions. + +## Introduction + +The Platform Kit is a collection of customizable API's, hooks and components you can use to provide an embedded Supabase experience within your own platform. It comes in the form of a single dialog that enables the management of database, authentication, storage, users, secrets, logs, and performance monitoring. + +**Features** + +- Database, Auth, Storage, User, Secrets, Logs, and Performance management +- Responsive dialog/drawer interface (desktop & mobile) +- API proxy for Management API +- AI-powered SQL generation (optional) +- Customize to your liking + +## Who is it for + +Anyone who is providing Postgres databases to their users. + +## Usage + +Embed the manager dialog in your app and manage its state: + +```tsx +import { useState } from 'react' +import { useMobile } from '@/hooks/use-mobile' +import { Button } from '@/components/ui/button' +import SupabaseManagerDialog from '@/components/supabase-manager' + +export default function Example() { + const [open, setOpen] = useState(false) + const projectRef = 'your-project-ref' // Replace with your actual project ref + const isMobile = useMobile() + + return ( + <> + + + + ) +} +``` + +## Quick Start + +1. **Set up environment variables:** in your `.env.local` file: + + ```bash + SUPABASE_MANAGEMENT_API_TOKEN=your-personal-access-token + NEXT_PUBLIC_ENABLE_AI_QUERIES=true + OPENAI_API_KEY=your-openai-api-key + ``` + +2. **Add project-level authentication checks** in your API proxy at `app/api/supabase-proxy/[...path]/route.ts` as well as your ai/sql route at `app/api/ai/sql/route.ts` to ensure only authorized users can access their own project resources. + +3. **Add a Toaster for notifications:** + Place the following component at the root of your app (e.g., in your `layout.tsx` or `App.tsx`) to enable toast notifications: + + ```tsx + import { Toaster } from '@/components/ui/sonner' + + export default function RootLayout({ children }) { + return ( + + + +
      {children}
      + + + + ) + } + ``` + +That's it! The default setup uses your Supabase personal access token for the Management API. + +## Security + +- Never expose your Management API token to the client +- Always implement authentication and permission checks in your proxy + +## Further reading + +- [Supabase Management API](https://supabase.com/docs/reference/api/introduction) diff --git a/apps/learn/contentlayer.config.js b/apps/learn/contentlayer.config.js new file mode 100644 index 0000000000000..e031de9fc25e1 --- /dev/null +++ b/apps/learn/contentlayer.config.js @@ -0,0 +1,254 @@ +import { getHighlighter, loadTheme } from '@shikijs/compat' +import { defineDocumentType, defineNestedType, makeSource } from 'contentlayer2/source-files' +import path from 'path' +import rehypeAutolinkHeadings from 'rehype-autolink-headings' +import rehypePrettyCode from 'rehype-pretty-code' +import rehypeSlug from 'rehype-slug' +import { codeImport } from 'remark-code-import' +import remarkGfm from 'remark-gfm' +import { visit } from 'unist-util-visit' + +/** @type {import('contentlayer2/source-files').ComputedFields} */ +const computedFields = { + slug: { + type: 'string', + resolve: (doc) => `/${doc._raw.flattenedPath}`, + }, + slugAsParams: { + type: 'string', + resolve: (doc) => doc._raw.flattenedPath, + }, +} + +const LinksProperties = defineNestedType(() => ({ + name: 'LinksProperties', + fields: { + doc: { + type: 'string', + }, + api: { + type: 'string', + }, + }, +})) + +const ExploreItem = defineNestedType(() => ({ + name: 'ExploreItem', + fields: { + title: { + type: 'string', + required: true, + }, + link: { + type: 'string', + required: true, + }, + itemType: { + type: 'string', + required: false, + }, + description: { + type: 'string', + required: false, + }, + }, +})) + +const CourseHero = defineNestedType(() => ({ + name: 'CourseHero', + fields: { + title: { + type: 'string', + required: true, + }, + subtitle: { + type: 'string', + required: true, + }, + description: { + type: 'string', + required: true, + }, + }, +})) + +const NestedProperties = defineNestedType(() => ({ + name: 'NestedProperties', + fields: { + radix: { + type: 'boolean', + }, + shadcn: { + type: 'boolean', + }, + vaul: { + type: 'boolean', + }, + inputOtp: { + type: 'boolean', + }, + reactAccessibleTreeview: { + type: 'boolean', + }, + }, +})) + +export const Doc = defineDocumentType(() => ({ + name: 'Doc', + filePathPattern: `**/*.mdx`, + contentType: 'mdx', + fields: { + title: { + type: 'string', + required: true, + }, + description: { + type: 'string', + required: true, + }, + published: { + type: 'boolean', + default: true, + }, + links: { + type: 'nested', + of: LinksProperties, + }, + featured: { + type: 'boolean', + default: false, + required: false, + }, + component: { + type: 'boolean', + default: false, + required: false, + }, + fragment: { + type: 'boolean', + default: false, + required: false, + }, + toc: { + type: 'boolean', + default: true, + required: false, + }, + chapterNumber: { + type: 'number', + required: false, + }, + explore: { + type: 'list', + of: ExploreItem, + required: false, + }, + courseHero: { + type: 'nested', + of: CourseHero, + required: false, + }, + source: { + type: 'nested', + of: NestedProperties, + }, + }, + computedFields, +})) + +export default makeSource({ + contentDirPath: './content', + documentTypes: [Doc], + mdx: { + remarkPlugins: [remarkGfm, codeImport], + rehypePlugins: [ + rehypeSlug, + () => (tree) => { + visit(tree, (node) => { + if (node?.type === 'element' && node?.tagName === 'pre') { + const [codeEl] = node.children + if (codeEl.tagName !== 'code') { + return + } + + if (codeEl.data?.meta) { + // Extract event from meta and pass it down the tree. + const regex = /event="([^"]*)"/ + const match = codeEl.data?.meta.match(regex) + if (match) { + node.__event__ = match ? match[1] : null + codeEl.data.meta = codeEl.data.meta.replace(regex, '') + } + } + + node.__rawString__ = codeEl.children?.[0].value + node.__src__ = node.properties?.__src__ + node.__style__ = node.properties?.__style__ + } + }) + }, + [ + rehypePrettyCode, + // rehypePrettyCodeOptions, + { + getHighlighter: async () => { + const theme = await loadTheme(path.join(process.cwd(), '/lib/themes/supabase-2.json')) + return await getHighlighter({ theme }) + }, + onVisitLine(node) { + // Prevent lines from collapsing in `display: grid` mode, and allow empty + // lines to be copy/pasted + if (node.children.length === 0) { + node.children = [{ type: 'text', value: ' ' }] + } + }, + onVisitHighlightedLine(node) { + node.properties.className.push('line--highlighted') + }, + onVisitHighlightedWord(node) { + node.properties.className = ['word--highlighted'] + }, + }, + ], + () => (tree) => { + visit(tree, (node) => { + if (node?.type === 'element' && node?.tagName === 'div') { + if (!('data-rehype-pretty-code-fragment' in node.properties)) { + return + } + + const preElement = node.children.at(-1) + if (preElement.tagName !== 'pre') { + return + } + + preElement.properties['__withMeta__'] = node.children.at(0).tagName === 'div' + preElement.properties['__rawString__'] = node.__rawString__ + + if (node.__src__) { + preElement.properties['__src__'] = node.__src__ + } + + if (node.__event__) { + preElement.properties['__event__'] = node.__event__ + } + + if (node.__style__) { + preElement.properties['__style__'] = node.__style__ + } + } + }) + }, + // rehypeNpmCommand, + [ + rehypeAutolinkHeadings, + { + properties: { + className: ['subheading-anchor'], + ariaLabel: 'Link to section', + }, + }, + ], + ], + }, +}) diff --git a/apps/learn/context/framework-context.tsx b/apps/learn/context/framework-context.tsx new file mode 100644 index 0000000000000..9854b2d8a56c1 --- /dev/null +++ b/apps/learn/context/framework-context.tsx @@ -0,0 +1,46 @@ +'use client' + +import { createContext, useContext, useEffect, useState } from 'react' + +type Course = 'foundations' | 'smart-office' | 'performance-scaling' | 'debugging-operations' +type CourseContextType = { + course: Course + setCourse: (course: Course) => void +} + +const CourseContext = createContext(undefined) + +export function FrameworkProvider({ children }: { children: React.ReactNode }) { + const [course, setCourseState] = useState('foundations') + + // Initialize from localStorage on mount (client-side only) + useEffect(() => { + const storedCourse = localStorage.getItem('preferredCourse') + const validCourses: Course[] = [ + 'foundations', + 'smart-office', + 'performance-scaling', + 'debugging-operations', + ] + if (storedCourse && validCourses.includes(storedCourse as Course)) { + setCourseState(storedCourse as Course) + } + }, []) + + // Update localStorage when framework changes + const setCourse = (newCourse: Course) => { + setCourseState(newCourse) + localStorage.setItem('preferredCourse', newCourse) + } + + return {children} +} + +// Custom hook to use the framework context +export function useCourse() { + const context = useContext(CourseContext) + if (context === undefined) { + throw new Error('useCourse must be used within a CourseProvider') + } + return context +} diff --git a/apps/learn/context/mobile-menu-context.tsx b/apps/learn/context/mobile-menu-context.tsx new file mode 100644 index 0000000000000..f584ee3f80b5a --- /dev/null +++ b/apps/learn/context/mobile-menu-context.tsx @@ -0,0 +1,36 @@ +'use client' + +import React, { ReactNode, useState, useCallback } from 'react' +import { MobileMenuContext, MobileMenuContextType } from '@/hooks/use-mobile-menu' + +interface MobileMenuProviderProps { + children: ReactNode +} + +/** + * Provider component for mobile menu state + * Wraps children with context that provides mobile menu state and controls + */ +export function MobileMenuProvider({ children }: MobileMenuProviderProps) { + const [open, setOpen] = useState(false) + + // Use useCallback for stable function references + const handleSetOpen = useCallback((value: boolean) => { + setOpen(value) + }, []) + + const toggle = useCallback(() => { + setOpen((prev) => { + return !prev + }) + }, []) + + // Memoize the context value to prevent unnecessary re-renders + const value: MobileMenuContextType = { + open, + setOpen: handleSetOpen, + toggle, + } + + return {children} +} diff --git a/apps/learn/eslint.config.cjs b/apps/learn/eslint.config.cjs new file mode 100644 index 0000000000000..fec3ac9b3ab84 --- /dev/null +++ b/apps/learn/eslint.config.cjs @@ -0,0 +1,4 @@ +const { defineConfig } = require('eslint/config') +const supabaseConfig = require('eslint-config-supabase/next') + +module.exports = defineConfig([supabaseConfig]) diff --git a/apps/learn/hooks/use-config.ts b/apps/learn/hooks/use-config.ts new file mode 100644 index 0000000000000..f1c4f10923c3d --- /dev/null +++ b/apps/learn/hooks/use-config.ts @@ -0,0 +1,22 @@ +import { useAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' +import { ComponentProps } from 'react' +import { SonnerToaster } from 'ui' + +type Config = { + style: string + radius: number + sonnerPosition: ComponentProps['position'] + sonnerExpand: boolean +} + +const configAtom = atomWithStorage('config', { + style: 'default', + radius: 0.5, + sonnerPosition: 'bottom-right', + sonnerExpand: false, +}) + +export function useConfig() { + return useAtom(configAtom) +} diff --git a/apps/learn/hooks/use-mobile-menu.ts b/apps/learn/hooks/use-mobile-menu.ts new file mode 100644 index 0000000000000..1126e372f9e27 --- /dev/null +++ b/apps/learn/hooks/use-mobile-menu.ts @@ -0,0 +1,35 @@ +'use client' + +import { createContext, useContext } from 'react' + +/** + * Context type for mobile menu state and controls + */ +export interface MobileMenuContextType { + /** Current state of the mobile menu */ + open: boolean + /** Function to set the mobile menu state */ + setOpen: (open: boolean) => void + /** Function to toggle the mobile menu state */ + toggle: () => void +} + +// Create context with null as default value to indicate it's not provided +export const MobileMenuContext = createContext(null) + +/** + * Hook to access the mobile menu state and controls + * Must be used within a MobileMenuProvider + */ +export function useMobileMenu(): MobileMenuContextType { + const context = useContext(MobileMenuContext) + + if (context === null) { + throw new Error( + 'useMobileMenu must be used within a MobileMenuProvider. ' + + 'Ensure your component is wrapped with MobileMenuProvider.' + ) + } + + return context +} diff --git a/apps/learn/hooks/use-mounted.ts b/apps/learn/hooks/use-mounted.ts new file mode 100644 index 0000000000000..13256dce0c15d --- /dev/null +++ b/apps/learn/hooks/use-mounted.ts @@ -0,0 +1,11 @@ +import * as React from 'react' + +export function useMounted() { + const [mounted, setMounted] = React.useState(false) + + React.useEffect(() => { + setMounted(true) + }, []) + + return mounted +} diff --git a/apps/learn/lib/constants.ts b/apps/learn/lib/constants.ts new file mode 100644 index 0000000000000..9b2dc13741b33 --- /dev/null +++ b/apps/learn/lib/constants.ts @@ -0,0 +1 @@ +export const API_URL = process.env.NEXT_PUBLIC_API_URL! diff --git a/apps/learn/lib/get-current-chapter.ts b/apps/learn/lib/get-current-chapter.ts new file mode 100644 index 0000000000000..a8d99bf2cf863 --- /dev/null +++ b/apps/learn/lib/get-current-chapter.ts @@ -0,0 +1,36 @@ +import { allDocs } from 'contentlayer/generated' +import { normalizeSlug } from './get-next-page' + +/** + * Gets the current chapter number from a doc + */ +export function getCurrentChapter( + slug: string +): { chapterNumber?: number; completionMessage?: string } | null { + const normalizedSlug = normalizeSlug(slug) + const doc = allDocs.find((doc) => normalizeSlug(doc.slugAsParams) === normalizedSlug) + + if (!doc) { + return null + } + + // Get chapterNumber from frontmatter first, then fallback to parsing from title + let chapterNumber: number | undefined = (doc as any)?.chapterNumber + if (!chapterNumber) { + // Try to extract chapter number from title (e.g., "2: CSS Styling" -> 2) + const chapterMatch = doc.title.match(/^(\d+):/) + chapterNumber = chapterMatch ? parseInt(chapterMatch[1], 10) : undefined + } + + if (!chapterNumber) { + return null + } + + // Use description as completion message, or generate a default one + const completionMessage = doc.description || undefined + + return { + chapterNumber, + completionMessage, + } +} diff --git a/apps/learn/lib/get-internal-content.ts b/apps/learn/lib/get-internal-content.ts new file mode 100644 index 0000000000000..cc9e16cb13aef --- /dev/null +++ b/apps/learn/lib/get-internal-content.ts @@ -0,0 +1,44 @@ +import { existsSync, readdirSync, statSync } from 'fs' +import path from 'path' + +/** + * Get all internal content file paths recursively + * Returns a Set of paths like '/foundations/realtime', '/foundations/grafana' + */ +export function getInternalContentPaths(): Set { + const internalDir = path.join(process.cwd(), 'content/internal') + const paths = new Set() + + if (!existsSync(internalDir)) { + return paths + } + + function scanDirectory(dir: string, prefix: string = '') { + const entries = readdirSync(dir) + + for (const entry of entries) { + const fullPath = path.join(dir, entry) + const stat = statSync(fullPath) + + if (stat.isDirectory()) { + // Recursively scan subdirectories + scanDirectory(fullPath, prefix ? `${prefix}/${entry}` : entry) + } else if (entry.endsWith('.mdx') || entry.endsWith('.md')) { + // Remove file extension to get the path + const filename = entry.replace(/\.(mdx|md)$/, '') + const contentPath = prefix ? `/${prefix}/${filename}` : `/${filename}` + paths.add(contentPath) + } + } + } + + scanDirectory(internalDir) + return paths +} + +/** + * Check if internal content exists for a given href + */ +export function hasInternalContent(href: string, internalPaths: Set): boolean { + return internalPaths.has(href) +} diff --git a/apps/learn/lib/get-next-page.ts b/apps/learn/lib/get-next-page.ts new file mode 100644 index 0000000000000..4eda18f71cdd4 --- /dev/null +++ b/apps/learn/lib/get-next-page.ts @@ -0,0 +1,82 @@ +import { allDocs } from 'contentlayer/generated' +import { courses } from '@/config/docs' +import { SidebarNavItem } from '@/types/nav' + +/** + * Flattens the navigation structure to get all pages in order + * Only includes items that have an href (leaf nodes) + */ +function flattenNavItems(items: SidebarNavItem[]): SidebarNavItem[] { + const result: SidebarNavItem[] = [] + + for (const item of items) { + if (item.items && item.items.length > 0) { + // Recursively process children first + result.push(...flattenNavItems(item.items)) + } else if (item.href) { + // Only add items with href (leaf nodes) + result.push(item) + } + } + + return result +} + +/** + * Normalizes a slug/href for comparison + */ +export function normalizeSlug(slug: string): string { + return slug.replace(/^\//, '').toLowerCase() +} + +/** + * Gets the next page in the navigation structure based on the current page slug + */ +export function getNextPage( + currentSlug: string +): { title: string; href: string; description?: string; chapterNumber?: number } | null { + // Flatten all navigation items + const allPages = flattenNavItems(courses.items) + + // Normalize the current slug for comparison + const normalizedCurrentSlug = normalizeSlug(currentSlug) + + // Find the current page index + const currentIndex = allPages.findIndex((page) => { + if (!page.href) return false + const pageSlug = normalizeSlug(page.href) + return pageSlug === normalizedCurrentSlug + }) + + // If current page not found or it's the last page, return null + if (currentIndex === -1 || currentIndex === allPages.length - 1) { + return null + } + + // Get the next page + const nextPage = allPages[currentIndex + 1] + if (!nextPage || !nextPage.href) { + return null + } + + // Try to get description and chapterNumber from the doc + const nextPageSlug = normalizeSlug(nextPage.href) + const doc = allDocs.find((doc) => normalizeSlug(doc.slugAsParams) === nextPageSlug) + const description = doc?.description || nextPage.title + + // Get chapterNumber from frontmatter first, then fallback to parsing from title + // Using type assertion since contentlayer types may need regeneration + let chapterNumber: number | undefined = (doc as any)?.chapterNumber + if (!chapterNumber) { + // Try to extract chapter number from title (e.g., "2: CSS Styling" -> 2) + const chapterMatch = nextPage.title.match(/^(\d+):/) + chapterNumber = chapterMatch ? parseInt(chapterMatch[1], 10) : undefined + } + + return { + title: nextPage.title, + href: nextPage.href, + description, + chapterNumber, + } +} diff --git a/apps/learn/lib/merge-internal-content.ts b/apps/learn/lib/merge-internal-content.ts new file mode 100644 index 0000000000000..cb99f7ae91627 --- /dev/null +++ b/apps/learn/lib/merge-internal-content.ts @@ -0,0 +1,85 @@ +import { SidebarNavItem } from '@/types/nav' + +/** + * Get all hrefs from navigation items (only direct children, not nested) + */ +function getNavHrefsInSection(items?: SidebarNavItem[]): Set { + const hrefs = new Set() + if (!items) return hrefs + + items.forEach((item) => { + if (item.href) { + hrefs.add(item.href) + } + }) + + return hrefs +} + +/** + * Merge orphaned internal content into their respective parent sections + * For example, if /internal/foundations/grafana exists but /foundations/grafana doesn't, + * it will be added to the end of the Foundations section + */ +export function mergeInternalContentIntoSections( + navItems: SidebarNavItem[], + internalPaths: string[] +): SidebarNavItem[] { + return navItems.map((parentItem) => { + // If this item doesn't have children, return as is + if (!parentItem.items) return parentItem + + // Get the section path (e.g., "/foundations" from parent href) + const parentPath = parentItem.href + if (!parentPath) return parentItem + + // Get all existing hrefs in this section + const existingHrefs = getNavHrefsInSection(parentItem.items) + + // Find orphaned internal items that belong to this section + const orphanedItems: SidebarNavItem[] = [] + + internalPaths.forEach((internalPath) => { + // Check if this internal path belongs to this parent section + // e.g., /foundations/grafana belongs to /foundations + if (internalPath.startsWith(parentPath + '/')) { + // Check if there's no corresponding public item + const publicPath = internalPath // e.g., /foundations/grafana + if (!existingHrefs.has(publicPath)) { + // Extract the title from the path + const pathParts = internalPath.split('/').filter(Boolean) + const fileName = pathParts[pathParts.length - 1] + + // Convert kebab-case to Title Case + const title = fileName + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + + orphanedItems.push({ + title, + href: `/internal${internalPath}`, + requiresAuth: true, + }) + } + } + }) + + // If there are orphaned items, add them to the end of this section with a heading + if (orphanedItems.length > 0) { + // Create a separator item for "Internal Resources" + const internalResourcesHeading: SidebarNavItem = { + title: 'Internal Resources', + href: undefined, + items: orphanedItems.sort((a, b) => a.title.localeCompare(b.title)), + } + + return { + ...parentItem, + items: [...parentItem.items, internalResourcesHeading], + } + } + + return parentItem + }) +} diff --git a/apps/learn/lib/telemetry.ts b/apps/learn/lib/telemetry.ts new file mode 100644 index 0000000000000..804549688ccdb --- /dev/null +++ b/apps/learn/lib/telemetry.ts @@ -0,0 +1,19 @@ +'use client' + +import { usePathname } from 'next/navigation' +import { useCallback } from 'react' + +import { sendTelemetryEvent } from 'common' +import { TelemetryEvent } from 'common/telemetry-constants' +import { API_URL } from 'lib/constants' + +export function useSendTelemetryEvent() { + const pathname = usePathname() + + return useCallback( + (event: TelemetryEvent) => { + return sendTelemetryEvent(API_URL, event, pathname) + }, + [pathname] + ) +} diff --git a/apps/learn/lib/themes/supabase-2.json b/apps/learn/lib/themes/supabase-2.json new file mode 100644 index 0000000000000..d4120e118a2f8 --- /dev/null +++ b/apps/learn/lib/themes/supabase-2.json @@ -0,0 +1,207 @@ +{ + "name": "Supabase Theme", + "displayName": "Supabase Theme", + "semanticHighlighting": true, + "colors": { + "editor.background": "var(--background-surface-75) / 0.75", + "editor.foreground": "var(--code-foreground)", + "editor.lineHighlightBackground": "var(--code-highlight-color)", + "editorCursor.foreground": "var(--code-foreground)", + "editorWhitespace.foreground": "var(--code-foreground)", + "editorIndentGuide.background": "var(--code-foreground)", + "editor.selectionBackground": "var(--code-highlight-color)", + "editor.selectionHighlightBackground": "var(--code-highlight-color)" + }, + "tokenColors": [ + { + "scope": ["comment", "punctuation.definition.comment", "string.comment"], + "settings": { + "foreground": "var(--code-token-comment)", + "fontStyle": "italic" + } + }, + { + "scope": ["constant", "entity.name.constant", "variable.other.constant"], + "settings": { + "foreground": "var(--code-token-constant)" + } + }, + { + "scope": ["entity", "entity.name", "entity.name.type", "support.type"], + "settings": { + "foreground": "var(--code-token-function)" + } + }, + { + "scope": ["variable", "support.variable", "variable.parameter"], + "settings": { + "foreground": "var(--code-token-parameter)" + } + }, + { + "scope": ["keyword", "storage.type", "storage.modifier", "keyword.control"], + "settings": { + "foreground": "var(--code-token-keyword)" + } + }, + { + "scope": ["string", "constant.other.symbol", "constant.other.key", "string.quoted"], + "settings": { + "foreground": "var(--code-token-string)" + } + }, + { + "scope": ["support.function", "entity.name.function"], + "settings": { + "foreground": "var(--code-token-function)" + } + }, + { + "scope": ["punctuation", "punctuation.separator", "meta.brace", "punctuation.definition"], + "settings": { + "foreground": "var(--code-token-punctuation)" + } + }, + { + "scope": ["meta.tag", "declaration.tag", "markup.deleted"], + "settings": { + "foreground": "var(--code-token-function)" + } + }, + { + "scope": ["markup.inserted"], + "settings": { + "foreground": "var(--code-token-string)" + } + }, + { + "scope": ["markup.changed"], + "settings": { + "foreground": "var(--code-token-string-expression)" + } + }, + { + "scope": ["invalid", "invalid.deprecated"], + "settings": { + "foreground": "var(--code-foreground)", + "background": "var(--code-token-comment)" + } + }, + { + "scope": ["support.type.property-name"], + "settings": { + "foreground": "var(--code-token-property)" + } + }, + { + "scope": ["entity.other.attribute-name"], + "settings": { + "foreground": "var(--code-token-property)" + } + }, + { + "scope": ["meta.object-literal.key"], + "settings": { + "foreground": "var(--code-token-property)" + } + }, + { + "scope": ["entity.other.attribute-name", "entity.other.inherited-class"], + "settings": { + "foreground": "var(--code-token-property)" + } + }, + { + "scope": ["storage", "storage.type", "storage.modifier"], + "settings": { + "foreground": "var(--code-token-keyword)" + } + }, + { + "scope": ["support.class"], + "settings": { + "foreground": "var(--code-token-function)" + } + }, + { + "scope": ["constant.language"], + "settings": { + "foreground": "var(--code-token-constant)" + } + }, + { + "scope": ["meta.embedded"], + "settings": { + "foreground": "var(--code-token-string-expression)" + } + }, + { + "scope": ["meta.function-call"], + "settings": { + "foreground": "var(--code-token-function)" + } + }, + { + "scope": ["meta.method-call"], + "settings": { + "foreground": "var(--code-token-function)" + } + }, + { + "scope": ["meta.return-type"], + "settings": { + "foreground": "var(--code-token-keyword)" + } + }, + { + "scope": [ + "meta.import", + "meta.import.js", + "meta.import.ts", + "meta.import.tsx", + "meta.import.jsx", + "string.quoted.single.js", + "string.quoted.double.js", + "string.quoted.single.ts", + "string.quoted.double.ts", + "string.quoted.single.tsx", + "string.quoted.double.tsx", + "string.quoted.single.jsx", + "string.quoted.double.jsx" + ], + "settings": { + "foreground": "var(--code-token-string-expression)" + } + }, + { + "scope": ["meta.var.expr"], + "settings": { + "foreground": "var(--code-token-variable)" + } + }, + { + "scope": ["meta.block"], + "settings": { + "foreground": "var(--code-foreground)" + } + }, + { + "scope": ["meta.delimiter.period"], + "settings": { + "foreground": "var(--code-foreground)" + } + }, + { + "scope": ["meta.brace.round"], + "settings": { + "foreground": "var(--code-foreground)" + } + }, + { + "scope": ["meta.paren.expr"], + "settings": { + "foreground": "var(--code-foreground)" + } + } + ] +} diff --git a/apps/learn/lib/toc.ts b/apps/learn/lib/toc.ts new file mode 100644 index 0000000000000..c11d0b8303148 --- /dev/null +++ b/apps/learn/lib/toc.ts @@ -0,0 +1,79 @@ +// @ts-nocheck +// TODO: I'll fix this later. + +import { toc } from 'mdast-util-toc' +import { remark } from 'remark' +import { visit } from 'unist-util-visit' + +const textTypes = ['text', 'emphasis', 'strong', 'inlineCode'] + +function flattenNode(node) { + const p = [] + visit(node, (node) => { + if (!textTypes.includes(node.type)) return + p.push(node.value) + }) + return p.join(``) +} + +interface Item { + title: string + url: string + items?: Item[] +} + +interface Items { + items?: Item[] +} + +function getItems(node, current): Items { + if (!node) { + return {} + } + + if (node.type === 'paragraph') { + visit(node, (item) => { + if (item.type === 'link') { + current.url = item.url + current.title = flattenNode(node) + } + + if (item.type === 'text') { + current.title = flattenNode(node) + } + }) + + return current + } + + if (node.type === 'list') { + current.items = node.children.map((i) => getItems(i, {})) + + return current + } else if (node.type === 'listItem') { + const heading = getItems(node.children[0], {}) + + if (node.children.length > 1) { + getItems(node.children[1], heading) + } + + return heading + } + + return {} +} + +const getToc = () => (node, file) => { + const table = toc(node) + const items = getItems(table.map, {}) + + file.data = items +} + +export type TableOfContents = Items + +export async function getTableOfContents(content: string): Promise { + const result = await remark().use(getToc).process(content) + + return result.data +} diff --git a/apps/learn/lib/utils.ts b/apps/learn/lib/utils.ts new file mode 100644 index 0000000000000..6edcf90fba7a7 --- /dev/null +++ b/apps/learn/lib/utils.ts @@ -0,0 +1,7 @@ +import { cn as uiCN } from 'ui' + +export const cn = uiCN + +export function absoluteUrl(path: string) { + return `${process.env.NEXT_PUBLIC_APP_URL}${path}` +} diff --git a/apps/learn/next.config.mjs b/apps/learn/next.config.mjs new file mode 100644 index 0000000000000..44a9a34c9ecf3 --- /dev/null +++ b/apps/learn/next.config.mjs @@ -0,0 +1,27 @@ +import { withContentlayer } from 'next-contentlayer2' + +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ['ui', 'common', 'shared-data', 'icons', 'tsconfig'], + basePath: process.env.NEXT_PUBLIC_BASE_PATH, + async redirects() { + return [ + ...(process.env.NEXT_PUBLIC_BASE_PATH?.length + ? [ + { + source: '/', + destination: process.env.NEXT_PUBLIC_BASE_PATH, + basePath: false, + permanent: false, + }, + ] + : []), + ] + }, + eslint: { + // We are already running linting via GH action, this will skip linting during production build on Vercel + ignoreDuringBuilds: true, + }, +} + +export default withContentlayer(nextConfig) diff --git a/apps/learn/package.json b/apps/learn/package.json new file mode 100644 index 0000000000000..12a90d61f9d16 --- /dev/null +++ b/apps/learn/package.json @@ -0,0 +1,116 @@ +{ + "name": "learn", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "preinstall": "npx only-allow pnpm", + "dev": "next dev --port 3007", + "build": "pnpm run content:build && next build --turbopack", + "internal:sync": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/sync-internal-content.mts", + "start": "next start", + "lint": "eslint .", + "lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml", + "content:build": "contentlayer2 build", + "clean": "rimraf node_modules .next .turbo", + "typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@hookform/resolvers": "^3.1.1", + "@monaco-editor/react": "^4.6.0", + "@radix-ui/react-accordion": "*", + "@radix-ui/react-alert-dialog": "*", + "@radix-ui/react-aspect-ratio": "*", + "@radix-ui/react-avatar": "*", + "@radix-ui/react-checkbox": "*", + "@radix-ui/react-collapsible": "*", + "@radix-ui/react-context-menu": "*", + "@radix-ui/react-dialog": "*", + "@radix-ui/react-dropdown-menu": "*", + "@radix-ui/react-hover-card": "*", + "@radix-ui/react-label": "*", + "@radix-ui/react-menubar": "*", + "@radix-ui/react-navigation-menu": "*", + "@radix-ui/react-popover": "*", + "@radix-ui/react-progress": "*", + "@radix-ui/react-radio-group": "*", + "@radix-ui/react-scroll-area": "*", + "@radix-ui/react-select": "*", + "@radix-ui/react-separator": "*", + "@radix-ui/react-slider": "*", + "@radix-ui/react-slot": "*", + "@radix-ui/react-switch": "*", + "@radix-ui/react-tabs": "*", + "@radix-ui/react-toast": "*", + "@radix-ui/react-toggle": "*", + "@radix-ui/react-toggle-group": "*", + "@radix-ui/react-tooltip": "*", + "@react-router/fs-routes": "^7.4.0", + "@supabase/postgrest-js": "catalog:", + "@supabase/supa-mdx-lint": "0.2.6-alpha", + "@supabase/vue-blocks": "workspace:*", + "@tanstack/react-query": "^5.83.0", + "axios": "^1.12.0", + "class-variance-authority": "*", + "cmdk": "^1.0.0", + "common": "workspace:*", + "common-tags": "^1.8.2", + "contentlayer2": "0.4.6", + "eslint-config-supabase": "workspace:*", + "framer-motion": "^11.0.3", + "icons": "workspace:*", + "jotai": "^2.8.0", + "lucide-react": "*", + "next": "catalog:", + "next-contentlayer2": "0.4.6", + "next-themes": "^0.3.0", + "openai": "^5.9.0", + "openapi-fetch": "0.12.4", + "react": "catalog:", + "react-docgen": "^7.0.3", + "react-dom": "catalog:", + "react-hook-form": "^7.45.0", + "react-markdown": "^10.1.0", + "react-wrap-balancer": "^1.1.0", + "recharts": "^2.8.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-pretty-code": "^0.9.0", + "rehype-slug": "^6.0.0", + "remark": "^14.0.3", + "remark-code-import": "^1.2.0", + "remark-gfm": "^4.0.0", + "sonner": "^1.5.0", + "ui": "workspace:*", + "ui-patterns": "workspace:*", + "unist-util-visit": "^5.0.0", + "vaul": "^0.9.6", + "zod": "catalog:" + }, + "devDependencies": { + "@react-router/dev": "^7.1.5", + "@shikijs/compat": "^1.1.7", + "@supabase/ssr": "^0.7.0", + "@supabase/supabase-js": "catalog:", + "@tanstack/react-router": "^1.114.27", + "@tanstack/react-start": "^1.114.25", + "@types/common-tags": "^1.8.4", + "@types/lodash": "^4.17.16", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "config": "workspace:^", + "dotenv": "^17.2.3", + "lodash": "^4.17.21", + "mdast-util-toc": "^6.1.1", + "postcss": "^8.5.3", + "react-dropzone": "^14.3.8", + "react-router": "^7.5.2", + "rimraf": "^4.1.3", + "shadcn": "^3.0.0", + "shiki": "^1.1.7", + "tailwindcss": "catalog:", + "tsconfig": "workspace:*", + "tsx": "catalog:", + "typescript": "catalog:", + "vite": "catalog:" + } +} diff --git a/apps/learn/postcss.config.cjs b/apps/learn/postcss.config.cjs new file mode 100644 index 0000000000000..08a01d4d16775 --- /dev/null +++ b/apps/learn/postcss.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + tailwindcss: {}, + }, +} diff --git a/apps/learn/public/favicon/android-icon-144x144.png b/apps/learn/public/favicon/android-icon-144x144.png new file mode 100644 index 0000000000000..144190d197673 Binary files /dev/null and b/apps/learn/public/favicon/android-icon-144x144.png differ diff --git a/apps/learn/public/favicon/android-icon-192x192.png b/apps/learn/public/favicon/android-icon-192x192.png new file mode 100644 index 0000000000000..c4f30c5f922d2 Binary files /dev/null and b/apps/learn/public/favicon/android-icon-192x192.png differ diff --git a/apps/learn/public/favicon/android-icon-36x36.png b/apps/learn/public/favicon/android-icon-36x36.png new file mode 100644 index 0000000000000..aed5d2b3063f6 Binary files /dev/null and b/apps/learn/public/favicon/android-icon-36x36.png differ diff --git a/apps/learn/public/favicon/android-icon-48x48.png b/apps/learn/public/favicon/android-icon-48x48.png new file mode 100644 index 0000000000000..0821ae73784f2 Binary files /dev/null and b/apps/learn/public/favicon/android-icon-48x48.png differ diff --git a/apps/learn/public/favicon/android-icon-72x72.png b/apps/learn/public/favicon/android-icon-72x72.png new file mode 100644 index 0000000000000..10d2bbaaa8699 Binary files /dev/null and b/apps/learn/public/favicon/android-icon-72x72.png differ diff --git a/apps/learn/public/favicon/android-icon-96x96.png b/apps/learn/public/favicon/android-icon-96x96.png new file mode 100644 index 0000000000000..a8d96ffed730e Binary files /dev/null and b/apps/learn/public/favicon/android-icon-96x96.png differ diff --git a/apps/learn/public/favicon/apple-icon-114x114.png b/apps/learn/public/favicon/apple-icon-114x114.png new file mode 100644 index 0000000000000..396cee8516afe Binary files /dev/null and b/apps/learn/public/favicon/apple-icon-114x114.png differ diff --git a/apps/learn/public/favicon/apple-icon-120x120.png b/apps/learn/public/favicon/apple-icon-120x120.png new file mode 100644 index 0000000000000..7e4e727910e0c Binary files /dev/null and b/apps/learn/public/favicon/apple-icon-120x120.png differ diff --git a/apps/learn/public/favicon/apple-icon-144x144.png b/apps/learn/public/favicon/apple-icon-144x144.png new file mode 100644 index 0000000000000..144190d197673 Binary files /dev/null and b/apps/learn/public/favicon/apple-icon-144x144.png differ diff --git a/apps/learn/public/favicon/apple-icon-152x152.png b/apps/learn/public/favicon/apple-icon-152x152.png new file mode 100644 index 0000000000000..cb261b8bba09f Binary files /dev/null and b/apps/learn/public/favicon/apple-icon-152x152.png differ diff --git a/apps/learn/public/favicon/apple-icon-180x180.png b/apps/learn/public/favicon/apple-icon-180x180.png new file mode 100644 index 0000000000000..3cf7f1132c8f3 Binary files /dev/null and b/apps/learn/public/favicon/apple-icon-180x180.png differ diff --git a/apps/learn/public/favicon/apple-icon-57x57.png b/apps/learn/public/favicon/apple-icon-57x57.png new file mode 100644 index 0000000000000..278a7a87bee75 Binary files /dev/null and b/apps/learn/public/favicon/apple-icon-57x57.png differ diff --git a/apps/learn/public/favicon/apple-icon-60x60.png b/apps/learn/public/favicon/apple-icon-60x60.png new file mode 100644 index 0000000000000..a693570eea352 Binary files /dev/null and b/apps/learn/public/favicon/apple-icon-60x60.png differ diff --git a/apps/learn/public/favicon/apple-icon-72x72.png b/apps/learn/public/favicon/apple-icon-72x72.png new file mode 100644 index 0000000000000..10d2bbaaa8699 Binary files /dev/null and b/apps/learn/public/favicon/apple-icon-72x72.png differ diff --git a/apps/learn/public/favicon/apple-icon-76x76.png b/apps/learn/public/favicon/apple-icon-76x76.png new file mode 100644 index 0000000000000..241bd6a24525f Binary files /dev/null and b/apps/learn/public/favicon/apple-icon-76x76.png differ diff --git a/apps/learn/public/favicon/apple-icon-precomposed.png b/apps/learn/public/favicon/apple-icon-precomposed.png new file mode 100644 index 0000000000000..c4f30c5f922d2 Binary files /dev/null and b/apps/learn/public/favicon/apple-icon-precomposed.png differ diff --git a/apps/learn/public/favicon/apple-icon.png b/apps/learn/public/favicon/apple-icon.png new file mode 100644 index 0000000000000..c4f30c5f922d2 Binary files /dev/null and b/apps/learn/public/favicon/apple-icon.png differ diff --git a/apps/learn/public/favicon/browserconfig.xml b/apps/learn/public/favicon/browserconfig.xml new file mode 100644 index 0000000000000..1a8136def88cb --- /dev/null +++ b/apps/learn/public/favicon/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/apps/learn/public/favicon/favicon-128.png b/apps/learn/public/favicon/favicon-128.png new file mode 100644 index 0000000000000..4fab8e34978e7 Binary files /dev/null and b/apps/learn/public/favicon/favicon-128.png differ diff --git a/apps/learn/public/favicon/favicon-16x16.png b/apps/learn/public/favicon/favicon-16x16.png new file mode 100644 index 0000000000000..7d05a72096bbb Binary files /dev/null and b/apps/learn/public/favicon/favicon-16x16.png differ diff --git a/apps/learn/public/favicon/favicon-180x180.png b/apps/learn/public/favicon/favicon-180x180.png new file mode 100644 index 0000000000000..3cf7f1132c8f3 Binary files /dev/null and b/apps/learn/public/favicon/favicon-180x180.png differ diff --git a/apps/learn/public/favicon/favicon-196x196.png b/apps/learn/public/favicon/favicon-196x196.png new file mode 100644 index 0000000000000..1e0258526ae9c Binary files /dev/null and b/apps/learn/public/favicon/favicon-196x196.png differ diff --git a/apps/learn/public/favicon/favicon-32x32.png b/apps/learn/public/favicon/favicon-32x32.png new file mode 100644 index 0000000000000..7eb8cabea60b5 Binary files /dev/null and b/apps/learn/public/favicon/favicon-32x32.png differ diff --git a/apps/learn/public/favicon/favicon-48x48.png b/apps/learn/public/favicon/favicon-48x48.png new file mode 100644 index 0000000000000..40377a51b54a7 Binary files /dev/null and b/apps/learn/public/favicon/favicon-48x48.png differ diff --git a/apps/learn/public/favicon/favicon-96x96.png b/apps/learn/public/favicon/favicon-96x96.png new file mode 100644 index 0000000000000..a8d96ffed730e Binary files /dev/null and b/apps/learn/public/favicon/favicon-96x96.png differ diff --git a/apps/learn/public/favicon/favicon.ico b/apps/learn/public/favicon/favicon.ico new file mode 100644 index 0000000000000..343ad561d7124 Binary files /dev/null and b/apps/learn/public/favicon/favicon.ico differ diff --git a/apps/learn/public/favicon/manifest.json b/apps/learn/public/favicon/manifest.json new file mode 100644 index 0000000000000..b3de4e47baede --- /dev/null +++ b/apps/learn/public/favicon/manifest.json @@ -0,0 +1,46 @@ +{ + "name": "Supabase", + "short_name": "Supabase", + "description": "The Postgres Development Platform.", + "display": "standalone", + "theme_color": "#1C1C1C", + "background_color": "#1C1C1C", + "icons": [ + { + "src": "/favicon/android-icon-36x36.png", + "sizes": "36x36", + "type": "image/png", + "density": "0.75" + }, + { + "src": "/favicon/android-icon-48x48.png", + "sizes": "48x48", + "type": "image/png", + "density": "1.0" + }, + { + "src": "/favicon/android-icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "density": "1.5" + }, + { + "src": "/favicon/android-icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "density": "2.0" + }, + { + "src": "/favicon/android-icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "density": "3.0" + }, + { + "src": "/favicon/android-icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "density": "4.0" + } + ] +} diff --git a/apps/learn/public/favicon/ms-icon-144x144.png b/apps/learn/public/favicon/ms-icon-144x144.png new file mode 100644 index 0000000000000..144190d197673 Binary files /dev/null and b/apps/learn/public/favicon/ms-icon-144x144.png differ diff --git a/apps/learn/public/favicon/ms-icon-150x150.png b/apps/learn/public/favicon/ms-icon-150x150.png new file mode 100644 index 0000000000000..bbdf205047c3c Binary files /dev/null and b/apps/learn/public/favicon/ms-icon-150x150.png differ diff --git a/apps/learn/public/favicon/ms-icon-310x310.png b/apps/learn/public/favicon/ms-icon-310x310.png new file mode 100644 index 0000000000000..42bd0698a0fae Binary files /dev/null and b/apps/learn/public/favicon/ms-icon-310x310.png differ diff --git a/apps/learn/public/favicon/ms-icon-70x70.png b/apps/learn/public/favicon/ms-icon-70x70.png new file mode 100644 index 0000000000000..6a428df84d32f Binary files /dev/null and b/apps/learn/public/favicon/ms-icon-70x70.png differ diff --git a/apps/learn/public/img/design-system-marks/atoms-illustration--dark.svg b/apps/learn/public/img/design-system-marks/atoms-illustration--dark.svg new file mode 100644 index 0000000000000..fae400998e3bd --- /dev/null +++ b/apps/learn/public/img/design-system-marks/atoms-illustration--dark.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/design-system-marks/atoms-illustration--deep-dark.svg b/apps/learn/public/img/design-system-marks/atoms-illustration--deep-dark.svg new file mode 100644 index 0000000000000..fae400998e3bd --- /dev/null +++ b/apps/learn/public/img/design-system-marks/atoms-illustration--deep-dark.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/design-system-marks/atoms-illustration--light.svg b/apps/learn/public/img/design-system-marks/atoms-illustration--light.svg new file mode 100644 index 0000000000000..a2189389763c4 --- /dev/null +++ b/apps/learn/public/img/design-system-marks/atoms-illustration--light.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/design-system-marks/design-system-marks--dark.svg b/apps/learn/public/img/design-system-marks/design-system-marks--dark.svg new file mode 100644 index 0000000000000..928eafbfc3847 --- /dev/null +++ b/apps/learn/public/img/design-system-marks/design-system-marks--dark.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/design-system-marks/design-system-marks--deep-dark.svg b/apps/learn/public/img/design-system-marks/design-system-marks--deep-dark.svg new file mode 100644 index 0000000000000..928eafbfc3847 --- /dev/null +++ b/apps/learn/public/img/design-system-marks/design-system-marks--deep-dark.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/design-system-marks/design-system-marks--light.svg b/apps/learn/public/img/design-system-marks/design-system-marks--light.svg new file mode 100644 index 0000000000000..26fff33c64f07 --- /dev/null +++ b/apps/learn/public/img/design-system-marks/design-system-marks--light.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/design-system-marks/fragments-illustration--dark.svg b/apps/learn/public/img/design-system-marks/fragments-illustration--dark.svg new file mode 100644 index 0000000000000..01331d3d8dee9 --- /dev/null +++ b/apps/learn/public/img/design-system-marks/fragments-illustration--dark.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/design-system-marks/fragments-illustration--deep-dark.svg b/apps/learn/public/img/design-system-marks/fragments-illustration--deep-dark.svg new file mode 100644 index 0000000000000..01331d3d8dee9 --- /dev/null +++ b/apps/learn/public/img/design-system-marks/fragments-illustration--deep-dark.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/design-system-marks/fragments-illustration--light.svg b/apps/learn/public/img/design-system-marks/fragments-illustration--light.svg new file mode 100644 index 0000000000000..e65e5c2e91ace --- /dev/null +++ b/apps/learn/public/img/design-system-marks/fragments-illustration--light.svg @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/smart-office.png b/apps/learn/public/img/smart-office.png new file mode 100644 index 0000000000000..c2a684ec6bde8 Binary files /dev/null and b/apps/learn/public/img/smart-office.png differ diff --git a/apps/learn/public/img/supabase-og-image.png b/apps/learn/public/img/supabase-og-image.png new file mode 100644 index 0000000000000..cba27d698598a Binary files /dev/null and b/apps/learn/public/img/supabase-og-image.png differ diff --git a/apps/learn/public/img/themes/dark.svg b/apps/learn/public/img/themes/dark.svg new file mode 100644 index 0000000000000..ea74324a8352d --- /dev/null +++ b/apps/learn/public/img/themes/dark.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/themes/deep-dark.svg b/apps/learn/public/img/themes/deep-dark.svg new file mode 100644 index 0000000000000..05bab64b78d39 --- /dev/null +++ b/apps/learn/public/img/themes/deep-dark.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/themes/light.svg b/apps/learn/public/img/themes/light.svg new file mode 100644 index 0000000000000..0ef07743552fb --- /dev/null +++ b/apps/learn/public/img/themes/light.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/img/themes/system.svg b/apps/learn/public/img/themes/system.svg new file mode 100644 index 0000000000000..386cd71770219 --- /dev/null +++ b/apps/learn/public/img/themes/system.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/learn/public/llms.txt b/apps/learn/public/llms.txt new file mode 100644 index 0000000000000..bea8b27be06d4 --- /dev/null +++ b/apps/learn/public/llms.txt @@ -0,0 +1,87 @@ +# Supabase UI Library +Last updated: 2025-10-15T07:54:48.550Z + +## Overview +Library of components for your project. The components integrate with Supabase and are shadcn compatible. + +## Docs +- [Prompts](https://supabase.com/ui/docs/ai-editors-rules/prompts) + - Rules for AI Code Editors for Supabase +- [FAQ](https://supabase.com/ui/docs/getting-started/faq) + - Frequently asked questions +- [Introduction](https://supabase.com/ui/docs/getting-started/introduction) + - A flexible, open-source, React-based 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) + - Install shadcn/ui and use the components in your project +- [Infinite Query Hook](https://supabase.com/ui/docs/infinite-query-hook) + - React hook for infinite lists, fetching data from Supabase. +- [Supabase Client Libraries](https://supabase.com/ui/docs/nextjs/client) + - Supabase client for Next.js +- [Current User Avatar](https://supabase.com/ui/docs/nextjs/current-user-avatar) + - Supabase Auth-aware avatar +- [Dropzone (File Upload)](https://supabase.com/ui/docs/nextjs/dropzone) + - Displays a control for easier uploading of files directly to Supabase Storage +- [Password-based Authentication](https://supabase.com/ui/docs/nextjs/password-based-auth) + - Password-based authentication block for Next.js +- [Realtime Avatar Stack](https://supabase.com/ui/docs/nextjs/realtime-avatar-stack) + - Avatar stack in realtime +- [Realtime Chat](https://supabase.com/ui/docs/nextjs/realtime-chat) + - Real-time chat component for collaborative applications +- [Realtime Cursor](https://supabase.com/ui/docs/nextjs/realtime-cursor) + - Real-time cursor sharing for collaborative applications +- [Social Authentication](https://supabase.com/ui/docs/nextjs/social-auth) + - Social authentication block for Next.js +- [Supabase Client Libraries](https://supabase.com/ui/docs/nuxtjs/client) + - Supabase client for Nuxt.js +- [Platform Kit](https://supabase.com/ui/docs/platform/platform-kit) + - The easiest way to build platforms on top of Supabase +- [Supabase Client Libraries](https://supabase.com/ui/docs/react-router/client) + - Supabase client for React Router +- [Current User Avatar](https://supabase.com/ui/docs/react-router/current-user-avatar) + - Supabase Auth-aware avatar +- [Dropzone (File Upload)](https://supabase.com/ui/docs/react-router/dropzone) + - Displays a control for easier uploading of files directly to Supabase Storage +- [Password-based Authentication](https://supabase.com/ui/docs/react-router/password-based-auth) + - Password-based authentication block for React Router +- [Realtime Avatar Stack](https://supabase.com/ui/docs/react-router/realtime-avatar-stack) + - Avatar stack in realtime +- [Realtime Chat](https://supabase.com/ui/docs/react-router/realtime-chat) + - Real-time chat component for collaborative applications +- [Realtime Cursor](https://supabase.com/ui/docs/react-router/realtime-cursor) + - Real-time cursor sharing for collaborative applications +- [Social Authentication](https://supabase.com/ui/docs/react-router/social-auth) + - Social authentication block for React Router +- [Supabase Client Libraries](https://supabase.com/ui/docs/react/client) + - Supabase client for React Single Page Applications +- [Current User Avatar](https://supabase.com/ui/docs/react/current-user-avatar) + - Supabase Auth-aware avatar +- [Dropzone (File Upload)](https://supabase.com/ui/docs/react/dropzone) + - Displays a control for easier uploading of files directly to Supabase Storage +- [Password-based Authentication](https://supabase.com/ui/docs/react/password-based-auth) + - Password-based authentication block for React Single Page Applications +- [Realtime Avatar Stack](https://supabase.com/ui/docs/react/realtime-avatar-stack) + - Avatar stack in realtime +- [Realtime Chat](https://supabase.com/ui/docs/react/realtime-chat) + - Real-time chat component for collaborative applications +- [Realtime Cursor](https://supabase.com/ui/docs/react/realtime-cursor) + - Real-time cursor sharing for collaborative applications +- [Social Authentication](https://supabase.com/ui/docs/react/social-auth) + - Social authentication block for React Single Page Applications +- [Supabase Client Libraries](https://supabase.com/ui/docs/tanstack/client) + - Supabase client for TanStack Start +- [Current User Avatar](https://supabase.com/ui/docs/tanstack/current-user-avatar) + - Supabase Auth-aware avatar +- [Dropzone (File Upload)](https://supabase.com/ui/docs/tanstack/dropzone) + - Displays a control for easier uploading of files directly to Supabase Storage +- [Password-based Authentication](https://supabase.com/ui/docs/tanstack/password-based-auth) + - Password-based authentication block for TanStack Start +- [Realtime Avatar Stack](https://supabase.com/ui/docs/tanstack/realtime-avatar-stack) + - Avatar stack in realtime +- [Realtime Chat](https://supabase.com/ui/docs/tanstack/realtime-chat) + - Real-time chat component for collaborative applications +- [Realtime Cursor](https://supabase.com/ui/docs/tanstack/realtime-cursor) + - Real-time cursor sharing for collaborative applications +- [Social Authentication](https://supabase.com/ui/docs/tanstack/social-auth) + - Social authentication block for Tanstack Start +- [Supabase Client Libraries](https://supabase.com/ui/docs/vue/client) + - Supabase client for Vue Single Page Applications diff --git a/apps/learn/public/next.svg b/apps/learn/public/next.svg new file mode 100644 index 0000000000000..5174b28c565c2 --- /dev/null +++ b/apps/learn/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/learn/public/vercel.svg b/apps/learn/public/vercel.svg new file mode 100644 index 0000000000000..d2f84222734f2 --- /dev/null +++ b/apps/learn/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/learn/scripts/build-llms-txt.ts b/apps/learn/scripts/build-llms-txt.ts new file mode 100644 index 0000000000000..19e4721f7b869 --- /dev/null +++ b/apps/learn/scripts/build-llms-txt.ts @@ -0,0 +1,106 @@ +import fs from 'fs' +import path from 'path' + +const BASE_URL = 'https://supabase.com/ui/docs' + +interface DocMeta { + title: string + description?: string + path: string +} + +console.log('🤖 Building llms.txt') + +// Function to extract frontmatter from MDX files +function extractFrontmatter(content: string): { title?: string; description?: string } { + const frontmatterRegex = /---\n([\s\S]*?)\n---/ + const match = content.match(frontmatterRegex) + if (!match) return {} + + const frontmatter = match[1] + const titleMatch = frontmatter.match(/title:\s*(.*)/) + const descriptionMatch = frontmatter.match(/description:\s*(.*)/) + + return { + title: titleMatch?.[1], + description: descriptionMatch?.[1], + } +} + +// Function to recursively get all MDX files +function getMdxFiles(dir: string): string[] { + const files: string[] = [] + const entries = fs.readdirSync(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + files.push(...getMdxFiles(fullPath)) + } else if (entry.name.endsWith('.mdx')) { + files.push(fullPath) + } + } + + return files +} + +// Function to get all MDX files and their metadata +function getDocFiles(): DocMeta[] { + const docsDir = path.join(process.cwd(), 'content') + const mdxFiles = getMdxFiles(docsDir).sort((a, b) => a.localeCompare(b)) + + const docs: DocMeta[] = [] + + for (const fullPath of mdxFiles) { + console.log(fullPath) + const content = fs.readFileSync(fullPath, 'utf-8') + const { title, description } = extractFrontmatter(content) + + if (title) { + // Get relative path and convert to URL path + const relativePath = path.relative(docsDir, fullPath) + const urlPath = relativePath + .replace(/\.mdx$/, '') + .replace(/\/index$/, '') + .replace(/\\/g, '/') + + docs.push({ + title, + description, + path: urlPath, + }) + } + } + + return docs +} + +// Generate the llms.txt content +const docs = getDocFiles() +let content = `# Learn Supabase +Last updated: ${new Date().toISOString()} + +## Overview +Library of components for your project. The components integrate with Supabase and are shadcn compatible. + +## Docs +` + +// Add documentation links +for (const doc of docs) { + const url = `${BASE_URL}/${doc.path}` + content += `- [${doc.title}](${url})` + if (doc.description) { + content += `\n - ${doc.description}` + } + content += '\n' +} + +// Write the file +const publicDir = path.join(process.cwd(), 'public') +if (!fs.existsSync(publicDir)) { + fs.mkdirSync(publicDir, { recursive: true }) +} + +fs.writeFileSync(path.join(publicDir, 'llms.txt'), content) +console.log('✅ Generated llms.txt in public directory') diff --git a/apps/learn/scripts/sync-internal-content.mts b/apps/learn/scripts/sync-internal-content.mts new file mode 100644 index 0000000000000..2ca486c2fe5c8 --- /dev/null +++ b/apps/learn/scripts/sync-internal-content.mts @@ -0,0 +1,78 @@ +import { config } from 'dotenv' +import { existsSync, rmSync, mkdirSync, renameSync } from 'node:fs' +import { execSync } from 'node:child_process' +import path from 'node:path' + +const envLocalPath = path.resolve(process.cwd(), '.env.local') +if (existsSync(envLocalPath)) { + config({ path: envLocalPath }) +} + +const repo = process.env.INTERNAL_CONTENT_REPO +const token = process.env.INTERNAL_CONTENT_GITHUB_TOKEN +const branch = process.env.INTERNAL_CONTENT_BRANCH || 'main' + +const targetDir = path.resolve(process.cwd(), 'content/internal') +const tmpDir = path.resolve(process.cwd(), '.tmp-internal-content') + +if (!token) { + console.log('INTERNAL_CONTENT_GITHUB_TOKEN not set; skipping internal content sync.') + process.exit(0) +} + +if (!repo) { + console.error('INTERNAL_CONTENT_REPO not set; cannot sync internal content.') + process.exit(1) +} + +const repoUrl = `https://${token}@github.com/${repo}.git` +const safeRepo = `https://github.com/${repo}.git` + +// Run command with output captured to prevent token leakage +const run = (cmd: string) => { + try { + const output = execSync(cmd, { stdio: 'pipe', encoding: 'utf-8' }) + return output + } catch (error: any) { + // Sanitize error messages by replacing token with [REDACTED] + const sanitizedMessage = error.message?.replace(new RegExp(token, 'g'), '[REDACTED]') ?? 'Unknown error' + const sanitizedStderr = error.stderr?.toString().replace(new RegExp(token, 'g'), '[REDACTED]') ?? '' + const sanitizedStdout = error.stdout?.toString().replace(new RegExp(token, 'g'), '[REDACTED]') ?? '' + + const sanitizedError = new Error(sanitizedMessage) + ;(sanitizedError as any).stderr = sanitizedStderr + ;(sanitizedError as any).stdout = sanitizedStdout + ;(sanitizedError as any).status = error.status + throw sanitizedError + } +} + +try { + // Clean up any existing content + rmSync(targetDir, { recursive: true, force: true }) + rmSync(tmpDir, { recursive: true, force: true }) + + // Clone to temp directory (using token URL but errors will be sanitized) + console.log(`Cloning ${safeRepo} branch ${branch}...`) + run(`git clone --depth 1 --branch ${branch} ${repoUrl} ${tmpDir}`) + + // Remove git folder so targetDir is not a git repo + rmSync(path.join(tmpDir, '.git'), { recursive: true, force: true }) + + // Ensure parent directory exists + mkdirSync(path.dirname(targetDir), { recursive: true }) + + // Move temp folder into place (Node, not shell) + renameSync(tmpDir, targetDir) + + console.log('Internal content synced successfully') +} catch (e: any) { + // Sanitize error message to prevent token leakage + const sanitizedMessage = (e?.message ?? String(e)).replace(new RegExp(token, 'g'), '[REDACTED]') + console.error('Failed to sync internal content:', sanitizedMessage) + if (e?.stderr) { + console.error('Error details:', e.stderr) + } + rmSync(tmpDir, { recursive: true, force: true }) + process.exit(1) +} diff --git a/apps/learn/styles/code-block-variables.css b/apps/learn/styles/code-block-variables.css new file mode 100644 index 0000000000000..d4a7fe542976c --- /dev/null +++ b/apps/learn/styles/code-block-variables.css @@ -0,0 +1,35 @@ +[data-theme='dark'], +[data-theme='deep-dark'], +.dark, +.deep-dark { + --code-token-keyword: #bda4ff; + --code-foreground: #ffffff; + --code-token-constant: #3ecf8e; + --code-token-string: #ffcda1; + --code-token-comment: #7e7e7e; + --code-token-parameter: #ffffff; + --code-token-function: #3ecf8e; + --code-token-string-expression: #ffcda1; + --code-token-punctuation: #ffffff; + --code-token-link: #ffffff; + --code-token-number: #ffffff; + --code-token-property: #3ecf8e; + --code-highlight-color: #232323; +} + +[data-theme='light'], +.light { + --code-token-keyword: #6b35dc; + --code-foreground: hsl(var(--foreground-light) / 1); + --code-token-constant: #15593b; + --code-token-string: #f1a10d; + --code-token-comment: #7e7e7e; + --code-token-parameter: hsl(var(--foreground-light) / 1); + --code-token-function: #15593b; + --code-token-string-expression: #f1a10d; + --code-token-punctuation: hsl(var(--foreground-light) / 1); + --code-token-link: hsl(var(--foreground-light) / 1); + --code-token-number: hsl(var(--foreground-light) / 1); + --code-token-property: #15593b; + --code-highlight-color: #1c1c1c; +} diff --git a/apps/learn/styles/globals.css b/apps/learn/styles/globals.css new file mode 100644 index 0000000000000..2a46862296367 --- /dev/null +++ b/apps/learn/styles/globals.css @@ -0,0 +1,199 @@ +@import './../../../packages/ui/build/css/source/global.css'; +@import './../../../packages/ui/build/css/themes/dark.css'; +@import './../../../packages/ui/build/css/themes/classic-dark.css'; +@import './../../../packages/ui/build/css/themes/light.css'; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --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: 0 84.2% 60.2%; + --border: 220 13% 91%; + --input: 220 13% 91%; + --ring: 220 13% 70.8%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.625rem; + --sidebar: 210 20% 98.5%; + --sidebar-foreground: 224 71.4% 4.1%; + --sidebar-primary: 220.9 39.3% 11%; + --sidebar-primary-foreground: 210 20% 98%; + --sidebar-accent: 220 14.3% 95.9%; + --sidebar-accent-foreground: 220.9 39.3% 11%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 220 13% 70.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: 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: 0 84.2% 60.2%; + --border: 215 27.9% 16.9%; + --input: 215 27.9% 16.9%; + --ring: 217.9 10.6% 55.6%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar: 220.9 39.3% 11%; + --sidebar-foreground: 210 20% 98%; + --sidebar-primary: 220 70% 50%; + --sidebar-primary-foreground: 210 20% 98%; + --sidebar-accent: 215 27.9% 16.9%; + --sidebar-accent-foreground: 210 20% 98%; + --sidebar-border: 215 27.9% 16.9%; + --sidebar-ring: 220 13% 43.9%; + } +} + +@layer base { + * { + @apply border-border; + } + html { + @apply scroll-smooth; + } + body { + @apply bg text-foreground; + /* font-feature-settings: "rlig" 1, "calt" 1; */ + font-synthesis-weight: none; + text-rendering: optimizeLegibility; + } +} + +@layer utilities { + .step { + counter-increment: step; + } + + .step:before { + @apply absolute w-9 h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 border-background; + @apply ml-[-50px] mt-[-4px]; + content: counter(step); + } + + .chunk-container { + @apply shadow-none; + } + + .chunk-container::after { + content: ''; + @apply absolute -inset-4 shadow-xl rounded-xl border; + } +} + +@media (max-width: 640px) { + .container { + @apply px-4; + } +} + +.preview { + --background: 0 0% 100%; + --foreground: 0 0% 14.5%; + --card: 0 0% 100%; + --card-foreground: 0 0% 14.5%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 14.5%; + --primary: 0 0% 20.5%; + --primary-foreground: 0 0% 98.5%; + --secondary: 0 0% 97%; + --secondary-foreground: 0 0% 20.5%; + --muted: 0 0% 97%; + --muted-foreground: 0 0% 55.6%; + --accent: 0 0% 97%; + --accent-foreground: 0 0% 20.5%; + --destructive: 27.3 24.5% 57.7%; + --destructive-foreground: 27.3 24.5% 57.7%; + --border: 0 0% 92.2%; + --input: 0 0% 92.2%; + --ring: 0 0% 70.8%; + --chart-1: 41.1 22.2% 64.6%; + --chart-2: 184.7 11.8% 60%; + --chart-3: 227.4 7% 39.8%; + --chart-4: 84.4 18.9% 82.8%; + --chart-5: 70.1 18.8% 76.9%; + --radius: 0.625rem; + --sidebar: 0 0% 98.5%; + --sidebar-foreground: 0 0% 14.5%; + --sidebar-primary: 0 0% 20.5%; + --sidebar-primary-foreground: 0 0% 98.5%; + --sidebar-accent: 0 0% 97%; + --sidebar-accent-foreground: 0 0% 20.5%; + --sidebar-border: 0 0% 92.2%; + --sidebar-ring: 0 0% 70.8%; +} + +[data-theme='dark'] .preview, +[data-theme='classic-dark'] .preview { + --background: 0 0% 14.5%; + --foreground: 0 0% 98.5%; + --card: 0 0% 0%; + --card-foreground: 0 0% 98.5%; + --popover: 0 0% 14.5%; + --popover-foreground: 0 0% 98.5%; + --primary: 0 0% 98.5%; + --primary-foreground: 0 0% 20.5%; + --secondary: 0 0% 26.9%; + --secondary-foreground: 0 0% 98.5%; + --muted: 0 0% 26.9%; + --muted-foreground: 0 0% 70.8%; + --accent: 0 0% 26.9%; + --accent-foreground: 0 0% 98.5%; + --destructive: 25.7 14.1% 39.6%; + --destructive-foreground: 25.3 23.7% 63.7%; + --border: 0 0% 26.9%; + --input: 0 0% 26.9%; + --ring: 0 0% 55.6%; + --chart-1: 264.4 24.3% 48.8%; + --chart-2: 162.5 17% 69.6%; + --chart-3: 70.1 18.8% 76.9%; + --chart-4: 303.9 26.5% 62.7%; + --chart-5: 16.4 24.6% 64.5%; + --sidebar: 0 0% 20.5%; + --sidebar-foreground: 0 0% 98.5%; + --sidebar-primary: 264.4 24.3% 48.8%; + --sidebar-primary-foreground: 0 0% 98.5%; + --sidebar-accent: 0 0% 26.9%; + --sidebar-accent-foreground: 0 0% 98.5%; + --sidebar-border: 0 0% 26.9%; + --sidebar-ring: 0 0% 43.9%; +} + +.preview * { + @apply border-border outline-ring/50; +} diff --git a/apps/learn/styles/mdx.css b/apps/learn/styles/mdx.css new file mode 100644 index 0000000000000..66ebc271c5c6b --- /dev/null +++ b/apps/learn/styles/mdx.css @@ -0,0 +1,81 @@ +.dark [data-theme='light'] { + display: none; +} + +.dark [data-theme='dark'] { + display: block; +} + +[data-rehype-pretty-code-fragment] { + @apply relative text-white; +} + +[data-rehype-pretty-code-fragment] code { + @apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0; + counter-reset: line; + box-decoration-break: clone; +} + +[data-rehype-pretty-code-fragment] .line { + @apply px-4 min-h-[1rem] py-0.5 w-full inline-block; +} + +[data-rehype-pretty-code-fragment] [data-line-numbers] .line { + @apply px-2; +} + +[data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before { + @apply text-foreground-muted text-xs; + counter-increment: line; + content: counter(line); + display: inline-block; + width: 1.8rem; + margin-right: 1.4rem; + text-align: right; +} + +[data-rehype-pretty-code-fragment] .line--highlighted { + @apply relative bg-surface-200 dark:bg-surface-100; +} + +[data-rehype-pretty-code-fragment] .line--highlighted { + @apply border-foreground-muted border-l-2; +} + +[data-rehype-pretty-code-fragment] .line-highlighted span { + @apply relative; +} + +[data-rehype-pretty-code-fragment] .word--highlighted { + @apply rounded-md bg-zinc-700/50 border-zinc-700/70 p-1; +} + +.dark [data-rehype-pretty-code-fragment] .word--highlighted { + @apply bg-zinc-900; +} + +[data-rehype-pretty-code-title] { + @apply mt-2 pt-6 px-4 text-sm font-medium; +} + +[data-rehype-pretty-code-title] + pre { + @apply mt-2; +} + +.mdx > .steps:first-child > h3:first-child { + @apply mt-0; +} + +.steps > h3 { + @apply mt-8 mb-4 text-base font-semibold; +} + +/* .preview-grid-background { + position: absolute; + inset: 0; + height: 100%; + width: 100%; + background-image: linear-gradient(to right, #80808012 1px, transparent 1px), + linear-gradient(to bottom, #80808012 1px, transparent 1px); + background-size: 24px 24px; +} */ diff --git a/apps/learn/tailwind.config.js b/apps/learn/tailwind.config.js new file mode 100644 index 0000000000000..9b80c0ac1c489 --- /dev/null +++ b/apps/learn/tailwind.config.js @@ -0,0 +1,70 @@ +const config = require('config/tailwind.config') + +module.exports = config({ + darkMode: ['class'], + content: [ + './app/**/*.{js,ts,jsx,tsx}', + './components/**/*.{js,ts,jsx,tsx}', + './registry/**/*.{js,ts,jsx,tsx}', + // purge styles from grid library + // + './../../packages/ui/src/**/*.{tsx,ts,js}', + './../../packages/ui-patterns/src/**/*.{tsx,ts,js}', + ], + theme: { + extend: { + maxWidth: { + site: '128rem', + }, + // The following variables are needed for the shadcn components in the examples to work. They're not clashing + // with the variables in the ui package. + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))', + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))', + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))', + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + 1: 'hsl(var(--chart-1))', + 2: 'hsl(var(--chart-2))', + 3: 'hsl(var(--chart-3))', + 4: 'hsl(var(--chart-4))', + 5: 'hsl(var(--chart-5))', + }, + }, + }, + }, +}) diff --git a/apps/learn/tsconfig.base.json b/apps/learn/tsconfig.base.json new file mode 100644 index 0000000000000..d72a9f3a27835 --- /dev/null +++ b/apps/learn/tsconfig.base.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "moduleResolution": "node", + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true + }, + "exclude": ["node_modules"] +} diff --git a/apps/learn/tsconfig.json b/apps/learn/tsconfig.json new file mode 100644 index 0000000000000..9e21bf5ea571e --- /dev/null +++ b/apps/learn/tsconfig.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "tsconfig/base.json", + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "baseUrl": ".", + "paths": { + "@/*": ["./*"], + "@ui/*": ["./../../packages/ui/src/*"], // handle ui package paths + "contentlayer/generated": ["./.contentlayer/generated"] + }, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".contentlayer/generated", + "./../../packages/ui/src/**/*.d.ts" + ], + "exclude": ["node_modules", "./scripts/build-registry.mts"] +} diff --git a/apps/learn/tsconfig.scripts.json b/apps/learn/tsconfig.scripts.json new file mode 100644 index 0000000000000..7d81d8fcb5f4c --- /dev/null +++ b/apps/learn/tsconfig.scripts.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.json", + "compilerOptions": { + "target": "es6", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "isolatedModules": false + }, + "include": [".contentlayer/generated", "scripts/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/apps/learn/types/nav.ts b/apps/learn/types/nav.ts new file mode 100644 index 0000000000000..3a4616d7ea9ae --- /dev/null +++ b/apps/learn/types/nav.ts @@ -0,0 +1,24 @@ +type supportedFrameworks = 'nextjs' | 'react-router' | 'tanstack' | 'react' | 'vue' | 'nuxtjs' +export interface NavItem { + title: string + href?: string + disabled?: boolean + external?: boolean + new?: boolean + icon?: any // to do: clean up later | keyof typeof Icons + label?: string + supportedFrameworks?: supportedFrameworks[] + requiresAuth?: boolean +} + +export interface NavItemWithChildren extends NavItem { + items?: NavItemWithChildren[] +} + +export interface MainNavItem extends NavItem {} + +export interface SidebarNavItem extends NavItemWithChildren {} + +export interface SidebarNavGroup extends NavItem { + items: (SidebarNavItem & { commandItemLabel: string })[] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea00fcb037dca..faa4dd2006cd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -773,6 +773,292 @@ importers: specifier: 'catalog:' version: 3.2.4(@types/node@22.13.14)(@vitest/ui@3.2.4)(jiti@2.5.1)(jsdom@20.0.3(supports-color@8.1.1))(msw@2.11.3(@types/node@22.13.14)(typescript@5.9.2))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + apps/learn: + dependencies: + '@hookform/resolvers': + specifier: ^3.1.1 + version: 3.3.1(react-hook-form@7.47.0(react@18.3.1)) + '@monaco-editor/react': + specifier: ^4.6.0 + version: 4.7.0(monaco-editor@0.52.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-accordion': + specifier: '*' + version: 1.2.12(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-alert-dialog': + specifier: '*' + version: 1.1.15(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-aspect-ratio': + specifier: '*' + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': + specifier: '*' + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: '*' + version: 1.3.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collapsible': + specifier: '*' + version: 1.1.12(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-context-menu': + specifier: '*' + version: 2.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: '*' + version: 1.1.15(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: '*' + version: 2.1.16(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-hover-card': + specifier: '*' + version: 1.1.15(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': + specifier: '*' + version: 2.1.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-menubar': + specifier: '*' + version: 1.1.16(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-navigation-menu': + specifier: '*' + version: 1.2.14(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: '*' + version: 1.1.15(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-progress': + specifier: '*' + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: '*' + version: 1.3.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-scroll-area': + specifier: '*' + version: 1.2.10(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: '*' + version: 2.2.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: '*' + version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slider': + specifier: '*' + version: 1.3.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: '*' + version: 1.2.4(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-switch': + specifier: '*' + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: '*' + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toast': + specifier: '*' + version: 1.2.14(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': + specifier: '*' + version: 1.1.10(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle-group': + specifier: '*' + version: 1.1.11(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: '*' + version: 1.2.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-router/fs-routes': + specifier: ^7.4.0 + version: 7.4.0(@react-router/dev@7.9.6(@types/node@22.13.14)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1))(typescript@5.9.2) + '@supabase/postgrest-js': + specifier: 'catalog:' + version: 2.94.1 + '@supabase/supa-mdx-lint': + specifier: 0.2.6-alpha + version: 0.2.6-alpha + '@supabase/vue-blocks': + specifier: workspace:* + version: link:../../blocks/vue + '@tanstack/react-query': + specifier: ^5.83.0 + version: 5.83.0(react@18.3.1) + axios: + specifier: ^1.12.0 + version: 1.12.2 + class-variance-authority: + specifier: '*' + version: 0.6.1 + cmdk: + specifier: ^1.0.0 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + common: + specifier: workspace:* + version: link:../../packages/common + common-tags: + specifier: ^1.8.2 + version: 1.8.2 + contentlayer2: + specifier: 0.4.6 + version: 0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1) + eslint-config-supabase: + specifier: workspace:* + version: link:../../packages/eslint-config-supabase + framer-motion: + specifier: ^11.0.3 + version: 11.11.17(@emotion/is-prop-valid@1.2.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + icons: + specifier: workspace:* + version: link:../../packages/icons + jotai: + specifier: ^2.8.0 + version: 2.8.1(@types/react@18.3.3)(react@18.3.1) + lucide-react: + specifier: '*' + version: 0.511.0(react@18.3.1) + next: + specifier: 'catalog:' + version: 15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4) + next-contentlayer2: + specifier: 0.4.6 + version: 0.4.6(contentlayer2@0.4.6(esbuild@0.25.2)(markdown-wasm@1.2.0)(supports-color@8.1.1))(esbuild@0.25.2)(markdown-wasm@1.2.0)(next@15.5.10(@babel/core@7.28.4(supports-color@8.1.1))(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(supports-color@8.1.1) + next-themes: + specifier: ^0.3.0 + version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + openai: + specifier: ^5.9.0 + version: 5.9.0(ws@8.18.3)(zod@3.25.76) + openapi-fetch: + specifier: 0.12.4 + version: 0.12.4 + react: + specifier: 'catalog:' + version: 18.3.1 + react-docgen: + specifier: ^7.0.3 + version: 7.1.1(supports-color@8.1.1) + react-dom: + specifier: 'catalog:' + version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.45.0 + version: 7.47.0(react@18.3.1) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@18.3.3)(react@18.3.1)(supports-color@8.1.1) + react-wrap-balancer: + specifier: ^1.1.0 + version: 1.1.0(react@18.3.1) + recharts: + specifier: ^2.8.0 + version: 2.15.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rehype-autolink-headings: + specifier: ^7.1.0 + version: 7.1.0 + rehype-pretty-code: + specifier: ^0.9.0 + version: 0.9.11(shiki@1.6.0) + rehype-slug: + specifier: ^6.0.0 + version: 6.0.0 + remark: + specifier: ^14.0.3 + version: 14.0.3(supports-color@8.1.1) + remark-code-import: + specifier: ^1.2.0 + version: 1.2.0 + remark-gfm: + specifier: ^4.0.0 + version: 4.0.1(supports-color@8.1.1) + sonner: + specifier: ^1.5.0 + version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + ui: + specifier: workspace:* + version: link:../../packages/ui + ui-patterns: + specifier: workspace:* + version: link:../../packages/ui-patterns + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 + vaul: + specifier: ^0.9.6 + version: 0.9.9(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zod: + specifier: 'catalog:' + version: 3.25.76 + devDependencies: + '@react-router/dev': + specifier: ^7.1.5 + version: 7.9.6(@types/node@22.13.14)(@vitejs/plugin-rsc@0.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1)))(babel-plugin-macros@3.1.0)(jiti@2.5.1)(react-router@7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(typescript@5.9.2)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(yaml@2.8.1) + '@shikijs/compat': + specifier: ^1.1.7 + version: 1.6.0 + '@supabase/ssr': + specifier: ^0.7.0 + version: 0.7.0(@supabase/supabase-js@2.94.1) + '@supabase/supabase-js': + specifier: 'catalog:' + version: 2.94.1 + '@tanstack/react-router': + specifier: ^1.114.27 + version: 1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-start': + specifier: ^1.114.25 + version: 1.114.27(@electric-sql/pglite@0.2.15)(@tanstack/react-router@1.114.27(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/node@22.13.14)(aws4fetch@1.0.20)(babel-plugin-macros@3.1.0)(db0@0.3.2(@electric-sql/pglite@0.2.15)(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3)))(drizzle-orm@0.44.2(@electric-sql/pglite@0.2.15)(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(pg@8.16.3))(ioredis@5.7.0(supports-color@8.1.1))(jiti@2.5.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.4)(supports-color@8.1.1)(terser@5.39.0)(tsx@4.20.3)(vite@7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1))(webpack@5.94.0(esbuild@0.25.2))(yaml@2.8.1) + '@types/common-tags': + specifier: ^1.8.4 + version: 1.8.4 + '@types/lodash': + specifier: ^4.17.16 + version: 4.17.16 + '@types/react': + specifier: 'catalog:' + version: 18.3.3 + '@types/react-dom': + specifier: 'catalog:' + version: 18.3.0 + config: + specifier: workspace:^ + version: link:../../packages/config + dotenv: + specifier: ^17.2.3 + version: 17.2.3 + lodash: + specifier: ^4.17.23 + version: 4.17.23 + mdast-util-toc: + specifier: ^6.1.1 + version: 6.1.1 + postcss: + specifier: ^8.5.3 + version: 8.5.6 + react-dropzone: + specifier: ^14.3.8 + version: 14.3.8(react@18.3.1) + react-router: + specifier: ^7.5.2 + version: 7.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + rimraf: + specifier: ^4.1.3 + version: 4.4.1 + shadcn: + specifier: ^3.0.0 + version: 3.3.1(@types/node@22.13.14)(babel-plugin-macros@3.1.0)(supports-color@8.1.1)(typescript@5.9.2) + shiki: + specifier: ^1.1.7 + version: 1.6.0 + tailwindcss: + specifier: 'catalog:' + version: 3.4.1(ts-node@10.9.2(@types/node@22.13.14)(typescript@5.9.2)) + tsconfig: + specifier: workspace:* + version: link:../../packages/tsconfig + tsx: + specifier: 'catalog:' + version: 4.20.3 + typescript: + specifier: 'catalog:' + version: 5.9.2 + vite: + specifier: 'catalog:' + version: 7.1.11(@types/node@22.13.14)(jiti@2.5.1)(sass@1.77.4)(terser@5.39.0)(tsx@4.20.3)(yaml@2.8.1) + apps/studio: dependencies: '@ai-sdk/amazon-bedrock': @@ -6574,19 +6860,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dialog@1.1.11': - resolution: {integrity: sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-dialog@1.1.15': resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} peerDependencies: @@ -6600,19 +6873,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dialog@1.1.7': - resolution: {integrity: sha512-EIdma8C0C/I6kL6sO02avaCRqi3fmWJpxH6mqbVScorW6nNktzKJT/le7VPho3o/7wCsyRg3z0+Q+Obr0Gy/VQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-direction@1.0.1': resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} peerDependencies: @@ -6696,19 +6956,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dismissable-layer@1.1.6': - resolution: {integrity: sha512-7gpgMT2gyKym9Jz2ZhlRXSg2y6cNQIK8d/cqBZ0RBCaps8pFryCWXiUKI+uHGFrhMrbGUP7U6PWgiXzIxoyF3Q==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-dismissable-layer@1.1.7': resolution: {integrity: sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==} peerDependencies: @@ -6722,19 +6969,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-dropdown-menu@2.0.6': - resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-dropdown-menu@2.1.12': resolution: {integrity: sha512-VJoMs+BWWE7YhzEQyVwvF9n22Eiyr83HotCVrMQzla/OwRovXCgah7AcaEr4hMNj4gJxSdtIbcHGvmJXOoJVHA==} peerDependencies: @@ -6823,19 +7057,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-focus-scope@1.1.3': - resolution: {integrity: sha512-4XaDlq0bPt7oJwR+0k0clCiCO/7lO7NKZTAaJBYxDNQT/vj4ig0/UvctrRscZaFREpRvUTkpKR96ov1e6jptQg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-focus-scope@1.1.4': resolution: {integrity: sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==} peerDependencies: @@ -7162,19 +7383,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-portal@1.1.5': - resolution: {integrity: sha512-ps/67ZqsFm+Mb6lSPJpfhRLrVL2i2fntgCmGMqqth4eaGUf+knAuuRtWVJrNjUhExgmdRqftSgzpf0DF0n6yXA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-portal@1.1.6': resolution: {integrity: sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==} peerDependencies: @@ -7240,19 +7448,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-presence@1.1.3': - resolution: {integrity: sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-presence@1.1.4': resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} peerDependencies: @@ -7671,19 +7866,6 @@ packages: '@types/react-dom': optional: true - '@radix-ui/react-tooltip@1.0.7': - resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 - react-dom: ^16.8 || ^17.0 || ^18.0 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - '@radix-ui/react-tooltip@1.1.8': resolution: {integrity: sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==} peerDependencies: @@ -7755,15 +7937,6 @@ packages: '@types/react': optional: true - '@radix-ui/react-use-controllable-state@1.1.1': - resolution: {integrity: sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@radix-ui/react-use-controllable-state@1.2.2': resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} peerDependencies: @@ -9391,6 +9564,9 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/estree-jsx@1.0.1': resolution: {integrity: sha512-sHyakZlAezNFxmYRo0fopDZW+XvK6ipeZkkp5EAOLjdPfZp8VjZBJ67vSRI99RSCAoqXVmXOHS4fnWoxpuGQtQ==} @@ -11747,6 +11923,10 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -11800,8 +11980,8 @@ packages: resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} engines: {node: '>=12'} - dotenv@17.2.2: - resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} dotty@0.1.2: @@ -12975,7 +13155,7 @@ packages: glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + deprecated: Glob versions prior to v9 are no longer supported glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} @@ -16866,6 +17046,10 @@ packages: '@types/react': optional: true + react-docgen@7.1.1: + resolution: {integrity: sha512-hlSJDQ2synMPKFZOsKo9Hi8WWZTC7POR8EmWvTSjow+VDgKzkmjQvFm2fk0tmRw+f0vTOIYKlarR0iL4996pdg==} + engines: {node: '>=16.14.0'} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -19779,11 +19963,6 @@ packages: peerDependencies: zod: ^3.24.1 - zod-to-json-schema@3.24.6: - resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} - peerDependencies: - zod: ^3.24.1 - zod-to-json-schema@3.25.0: resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} peerDependencies: @@ -21047,7 +21226,7 @@ snapshots: '@bsmnt/scrollytelling@0.3.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(gsap@3.13.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.3(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-slot': 1.2.4(@types/react@18.3.3)(react@18.3.1) gsap: 3.13.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -21373,7 +21552,7 @@ snapshots: '@dotenvx/dotenvx@1.51.0': dependencies: commander: 11.1.0 - dotenv: 17.2.2 + dotenv: 17.2.3 eciesjs: 0.4.15 execa: 5.1.1 fdir: 6.5.0(picomatch@4.0.3) @@ -21814,9 +21993,9 @@ snapshots: dependencies: '@graphiql/toolkit': 0.9.1(@types/node@22.13.14)(graphql-ws@5.14.1(graphql@16.11.0))(graphql@16.11.0) '@headlessui/react': 1.7.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-dialog': 1.1.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-dropdown-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-tooltip': 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': 1.2.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-visually-hidden': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/codemirror': 5.60.10 clsx: 1.2.1 @@ -25142,28 +25321,6 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 - '@radix-ui/react-dialog@1.1.11(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-portal': 1.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.0(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.3)(react@18.3.1) - aria-hidden: 1.2.4 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.3)(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 - '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.3 @@ -25186,28 +25343,6 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 - '@radix-ui/react-dialog@1.1.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-context': 1.1.2(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.1.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-portal': 1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 2.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.2.0(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.1.1(@types/react@18.3.3)(react@18.3.1) - aria-hidden: 1.2.4 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.6.3(@types/react@18.3.3)(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 - '@radix-ui/react-direction@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.26.10 @@ -25287,19 +25422,6 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 - '@radix-ui/react-dismissable-layer@1.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/primitive': 1.1.2 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.3)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 - '@radix-ui/react-dismissable-layer@1.1.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -25313,22 +25435,6 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 - '@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/runtime': 7.26.10 - '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 - '@radix-ui/react-dropdown-menu@2.1.12(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.2 @@ -25407,17 +25513,6 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 - '@radix-ui/react-focus-scope@1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-primitive': 2.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.3)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 - '@radix-ui/react-focus-scope@1.1.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1) @@ -25844,16 +25939,6 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 - '@radix-ui/react-portal@1.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/react-primitive': 2.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.3)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 - '@radix-ui/react-portal@1.1.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -25905,16 +25990,6 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 - '@radix-ui/react-presence@1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.3)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 - '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.3)(react@18.3.1) @@ -26382,27 +26457,6 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 - '@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@babel/runtime': 7.26.10 - '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 - '@radix-ui/react-tooltip@1.1.8(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -26477,13 +26531,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 - '@radix-ui/react-use-controllable-state@1.1.1(@types/react@18.3.3)(react@18.3.1)': - dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.3)(react@18.3.1) - react: 18.3.1 - optionalDependencies: - '@types/react': 18.3.3 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.3)(react@18.3.1)': dependencies: '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.3)(react@18.3.1) @@ -28840,6 +28887,8 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/doctrine@0.0.9': {} + '@types/estree-jsx@1.0.1': dependencies: '@types/estree': 1.0.5 @@ -30318,7 +30367,7 @@ snapshots: chokidar: 4.0.3 confbox: 0.2.2 defu: 6.1.4 - dotenv: 17.2.2 + dotenv: 17.2.3 exsolve: 1.0.7 giget: 2.0.0 jiti: 2.5.1 @@ -31565,6 +31614,10 @@ snapshots: dependencies: esutils: 2.0.3 + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -31618,7 +31671,7 @@ snapshots: dotenv@16.5.0: {} - dotenv@17.2.2: {} + dotenv@17.2.3: {} dotty@0.1.2: {} @@ -38015,6 +38068,21 @@ snapshots: '@types/node': 22.13.14 '@types/react': 18.3.3 + react-docgen@7.1.1(supports-color@8.1.1): + dependencies: + '@babel/core': 7.28.4(supports-color@8.1.1) + '@babel/traverse': 7.28.4(supports-color@8.1.1) + '@babel/types': 7.28.4 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.10 + strip-indent: 4.0.0 + transitivePeerDependencies: + - supports-color + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -39188,7 +39256,7 @@ snapshots: ts-morph: 26.0.0 tsconfig-paths: 4.2.0 zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) + zod-to-json-schema: 3.25.0(zod@3.25.76) transitivePeerDependencies: - '@cfworker/json-schema' - '@types/node' @@ -40881,7 +40949,7 @@ snapshots: vaul@0.9.9(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@radix-ui/react-dialog': 1.1.11(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -41596,10 +41664,6 @@ snapshots: dependencies: zod: 3.25.76 - zod-to-json-schema@3.24.6(zod@3.25.76): - dependencies: - zod: 3.25.76 - zod-to-json-schema@3.25.0(zod@3.25.76): dependencies: zod: 3.25.76