diff --git a/.github/workflows/self-host-tests-smoke.yml b/.github/workflows/self-host-tests-smoke.yml index ac7e0eecc6064..cc826e48b2eb7 100644 --- a/.github/workflows/self-host-tests-smoke.yml +++ b/.github/workflows/self-host-tests-smoke.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - config: [default, logs, kong, rustfs, kong-rustfs] + config: [default, logs, kong, rustfs, kong-rustfs, pgbouncer] steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -57,6 +57,8 @@ jobs: docker compose -f docker-compose.yml -f docker-compose.rustfs.yml up --quiet-pull --wait --wait-timeout 180 elif [ "${{ matrix.config }}" = "kong-rustfs" ]; then docker compose -f docker-compose.yml -f docker-compose.kong.yml -f docker-compose.rustfs.yml up --quiet-pull --wait --wait-timeout 180 + elif [ "${{ matrix.config }}" = "pgbouncer" ]; then + docker compose -f docker-compose.yml -f docker-compose.pgbouncer.yml up --quiet-pull --wait --wait-timeout 180 else docker compose up --quiet-pull --wait --wait-timeout 180 fi diff --git a/apps/design-system/.gitignore b/apps/design-system/.gitignore index 57c2e5aba20fa..2e7fc1fc9dac7 100644 --- a/apps/design-system/.gitignore +++ b/apps/design-system/.gitignore @@ -35,4 +35,7 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -.contentlayer \ No newline at end of file +.contentlayer + +# Generated by `pnpm generate:registry` (`scripts/build-registry.mts`) +/__registry__ diff --git a/apps/design-system/README.md b/apps/design-system/README.md index 893f21e36e697..a726c23098577 100644 --- a/apps/design-system/README.md +++ b/apps/design-system/README.md @@ -12,13 +12,15 @@ pnpm i pnpm dev ``` -The `dev` command runs both the Next.js development server and Contentlayer concurrently, which is recommended for most development workflows. +The `dev` command generates `__registry__`, then runs the Next.js development server and Contentlayer together. That is the recommended workflow. ### Alternative commands -You can also run the development server and content watcher separately: +You can also run the development server and content watcher separately. Generate the registry first, because `dev:next` and `dev:content` do not: ```bash +pnpm generate:registry + # Run only the Next.js development server pnpm dev:next @@ -26,19 +28,11 @@ pnpm dev:next pnpm dev:content ``` -Or run the development server from the root directory: - -```bash -pnpm dev:design-system -``` - -To run both the development server and content watcher from the root directory, you can use: +From the repo root, `pnpm dev:design-system` runs the same `dev` script, so it also generates `__registry__`. If you split the watchers from the root, generate first: ```bash -# Run the development server -pnpm dev:design-system - -# Run the content watcher (in a separate terminal shell) +pnpm --filter=design-system generate:registry +pnpm --filter=design-system dev:next pnpm --filter=design-system dev:content ``` @@ -46,27 +40,29 @@ Open [http://localhost:3003](http://localhost:3003) in your browser to see the r ### Watching for MDX changes -The `dev` command automatically watches for changes to MDX files with hot reload. If you're running the `pnpm dev:next` separately, you'll need to run `pnpm dev:content` in a separate terminal shell to watch for content changes. +The `dev` command watches MDX files and hot-reloads them. If you are running `pnpm dev:next` on its own, also run `pnpm dev:content` in another terminal. ### Adding components -The design system _references_ components rather than housing them. Thatโ€™s an important distinction to make, as everything that follows here is about the documentation of components. You can add or edit components in one of these two places: +The design system _references_ components rather than housing them. That distinction matters: everything below is about documenting components, not implementing them. Add or edit the components themselves in one of these two places: - [`packages/ui`](https://github.com/supabase/supabase/tree/master/packages/ui): basic UI components -- [`packages/ui-patterns`](https://github.com/supabase/supabase/tree/master/packages/ui-patterns): components which are built using NPM libraries or amalgamations of components from `patterns/ui` +- [`packages/ui-patterns`](https://github.com/supabase/supabase/tree/master/packages/ui-patterns): components built from libraries or from `packages/ui` -There are several parts of this design system that need to be manually updated after components have been added or removed (from documentation). These include: +After you add or remove documented components, update these source files: - `config/docs.ts`: list of components in the sidebar -- `content/docs`: the actual component documentation -- `registry/examples.ts`: list of example components -- `registry/fragments.ts`: list of fragment components -- `registry/charts.ts`: list of chart components -- `registry/default/example/*`: the actual example components +- `content/docs`: the component documentation +- `registry/examples.ts`: example components +- `registry/fragments.ts`: fragment components +- `registry/charts.ts`: chart components +- `registry/copy-writing.ts`: copywriting examples +- `registry/default/example/*`: the example component implementations +- `registry/default/block/*`: chart block implementations, when you add a chart -You will need to rebuild the design systemโ€™s registry after making new additions: +Do not edit `__registry__`. `pnpm dev`, `pnpm typecheck`, and `pnpm build` generate it from the files above, and it is gitignored. If you add registry entries while the app is already running, regenerate it: ```bash cd apps/design-system -pnpm build:registry +pnpm generate:registry ``` diff --git a/apps/design-system/__registry__/default/block/chart-bar-interactive.tsx b/apps/design-system/__registry__/default/block/chart-bar-interactive.tsx deleted file mode 100644 index 128664bb431e8..0000000000000 --- a/apps/design-system/__registry__/default/block/chart-bar-interactive.tsx +++ /dev/null @@ -1,211 +0,0 @@ -'use client' - -import * as React from 'react' -import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts' -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, - ChartConfig, - ChartContainer, - ChartTooltip, - ChartTooltipContent, -} from 'ui' - -export const description = 'An interactive bar chart' - -const chartData = [ - { date: '2024-04-01', desktop: 222, mobile: 150 }, - { date: '2024-04-02', desktop: 97, mobile: 180 }, - { date: '2024-04-03', desktop: 167, mobile: 120 }, - { date: '2024-04-04', desktop: 242, mobile: 260 }, - { date: '2024-04-05', desktop: 373, mobile: 290 }, - { date: '2024-04-06', desktop: 301, mobile: 340 }, - { date: '2024-04-07', desktop: 245, mobile: 180 }, - { date: '2024-04-08', desktop: 409, mobile: 320 }, - { date: '2024-04-09', desktop: 59, mobile: 110 }, - { date: '2024-04-10', desktop: 261, mobile: 190 }, - { date: '2024-04-11', desktop: 327, mobile: 350 }, - { date: '2024-04-12', desktop: 292, mobile: 210 }, - { date: '2024-04-13', desktop: 342, mobile: 380 }, - { date: '2024-04-14', desktop: 137, mobile: 220 }, - { date: '2024-04-15', desktop: 120, mobile: 170 }, - { date: '2024-04-16', desktop: 138, mobile: 190 }, - { date: '2024-04-17', desktop: 446, mobile: 360 }, - { date: '2024-04-18', desktop: 364, mobile: 410 }, - { date: '2024-04-19', desktop: 243, mobile: 180 }, - { date: '2024-04-20', desktop: 89, mobile: 150 }, - { date: '2024-04-21', desktop: 137, mobile: 200 }, - { date: '2024-04-22', desktop: 224, mobile: 170 }, - { date: '2024-04-23', desktop: 138, mobile: 230 }, - { date: '2024-04-24', desktop: 387, mobile: 290 }, - { date: '2024-04-25', desktop: 215, mobile: 250 }, - { date: '2024-04-26', desktop: 75, mobile: 130 }, - { date: '2024-04-27', desktop: 383, mobile: 420 }, - { date: '2024-04-28', desktop: 122, mobile: 180 }, - { date: '2024-04-29', desktop: 315, mobile: 240 }, - { date: '2024-04-30', desktop: 454, mobile: 380 }, - { date: '2024-05-01', desktop: 165, mobile: 220 }, - { date: '2024-05-02', desktop: 293, mobile: 310 }, - { date: '2024-05-03', desktop: 247, mobile: 190 }, - { date: '2024-05-04', desktop: 385, mobile: 420 }, - { date: '2024-05-05', desktop: 481, mobile: 390 }, - { date: '2024-05-06', desktop: 498, mobile: 520 }, - { date: '2024-05-07', desktop: 388, mobile: 300 }, - { date: '2024-05-08', desktop: 149, mobile: 210 }, - { date: '2024-05-09', desktop: 227, mobile: 180 }, - { date: '2024-05-10', desktop: 293, mobile: 330 }, - { date: '2024-05-11', desktop: 335, mobile: 270 }, - { date: '2024-05-12', desktop: 197, mobile: 240 }, - { date: '2024-05-13', desktop: 197, mobile: 160 }, - { date: '2024-05-14', desktop: 448, mobile: 490 }, - { date: '2024-05-15', desktop: 473, mobile: 380 }, - { date: '2024-05-16', desktop: 338, mobile: 400 }, - { date: '2024-05-17', desktop: 499, mobile: 420 }, - { date: '2024-05-18', desktop: 315, mobile: 350 }, - { date: '2024-05-19', desktop: 235, mobile: 180 }, - { date: '2024-05-20', desktop: 177, mobile: 230 }, - { date: '2024-05-21', desktop: 82, mobile: 140 }, - { date: '2024-05-22', desktop: 81, mobile: 120 }, - { date: '2024-05-23', desktop: 252, mobile: 290 }, - { date: '2024-05-24', desktop: 294, mobile: 220 }, - { date: '2024-05-25', desktop: 201, mobile: 250 }, - { date: '2024-05-26', desktop: 213, mobile: 170 }, - { date: '2024-05-27', desktop: 420, mobile: 460 }, - { date: '2024-05-28', desktop: 233, mobile: 190 }, - { date: '2024-05-29', desktop: 78, mobile: 130 }, - { date: '2024-05-30', desktop: 340, mobile: 280 }, - { date: '2024-05-31', desktop: 178, mobile: 230 }, - { date: '2024-06-01', desktop: 178, mobile: 200 }, - { date: '2024-06-02', desktop: 470, mobile: 410 }, - { date: '2024-06-03', desktop: 103, mobile: 160 }, - { date: '2024-06-04', desktop: 439, mobile: 380 }, - { date: '2024-06-05', desktop: 88, mobile: 140 }, - { date: '2024-06-06', desktop: 294, mobile: 250 }, - { date: '2024-06-07', desktop: 323, mobile: 370 }, - { date: '2024-06-08', desktop: 385, mobile: 320 }, - { date: '2024-06-09', desktop: 438, mobile: 480 }, - { date: '2024-06-10', desktop: 155, mobile: 200 }, - { date: '2024-06-11', desktop: 92, mobile: 150 }, - { date: '2024-06-12', desktop: 492, mobile: 420 }, - { date: '2024-06-13', desktop: 81, mobile: 130 }, - { date: '2024-06-14', desktop: 426, mobile: 380 }, - { date: '2024-06-15', desktop: 307, mobile: 350 }, - { date: '2024-06-16', desktop: 371, mobile: 310 }, - { date: '2024-06-17', desktop: 475, mobile: 520 }, - { date: '2024-06-18', desktop: 107, mobile: 170 }, - { date: '2024-06-19', desktop: 341, mobile: 290 }, - { date: '2024-06-20', desktop: 408, mobile: 450 }, - { date: '2024-06-21', desktop: 169, mobile: 210 }, - { date: '2024-06-22', desktop: 317, mobile: 270 }, - { date: '2024-06-23', desktop: 480, mobile: 530 }, - { date: '2024-06-24', desktop: 132, mobile: 180 }, - { date: '2024-06-25', desktop: 141, mobile: 190 }, - { date: '2024-06-26', desktop: 434, mobile: 380 }, - { date: '2024-06-27', desktop: 448, mobile: 490 }, - { date: '2024-06-28', desktop: 149, mobile: 200 }, - { date: '2024-06-29', desktop: 103, mobile: 160 }, - { date: '2024-06-30', desktop: 446, mobile: 400 }, -] - -const chartConfig = { - views: { - label: 'Page Views', - }, - desktop: { - label: 'Desktop', - color: 'hsl(var(--chart-1))', - }, - mobile: { - label: 'Mobile', - color: 'hsl(var(--chart-2))', - }, -} satisfies ChartConfig - -export default function Component() { - const [activeChart, setActiveChart] = React.useState('desktop') - - const total = React.useMemo( - () => ({ - desktop: chartData.reduce((acc, curr) => acc + curr.desktop, 0), - mobile: chartData.reduce((acc, curr) => acc + curr.mobile, 0), - }), - [] - ) - - return ( - - -
- Bar Chart - Interactive - Showing total visitors for the last 3 months -
-
- {['desktop', 'mobile'].map((key) => { - const chart = key as keyof typeof chartConfig - return ( - - ) - })} -
-
- - - - - { - const date = new Date(value) - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - }) - }} - /> - { - return new Date(value).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - }) - }} - /> - } - /> - - - - -
- ) -} diff --git a/apps/design-system/__registry__/default/block/chart-composed-actions.tsx b/apps/design-system/__registry__/default/block/chart-composed-actions.tsx deleted file mode 100644 index ee4212e416ed3..0000000000000 --- a/apps/design-system/__registry__/default/block/chart-composed-actions.tsx +++ /dev/null @@ -1,106 +0,0 @@ -'use client' - -import { BarChart2, ExternalLink, LineChart } from 'lucide-react' -import { useEffect, useState } from 'react' -import { - Chart, - ChartActions, - ChartBar, - ChartCard, - ChartContent, - ChartEmptyState, - ChartHeader, - ChartLine, - ChartLoadingState, - ChartTitle, -} from 'ui-patterns/Chart' - -export default function ChartComposedActions() { - const [isLoading, setIsLoading] = useState(true) - const [chartType, setChartType] = useState<'bar' | 'line'>('bar') - - const actions = [ - { - label: 'Open in Logs Explorer', - onClick: () => { - alert('Opening in Logs Explorer...') - }, - icon: , - }, - { - label: 'Toggle Chart Type', - onClick: () => { - setChartType(chartType === 'bar' ? 'line' : 'bar') - }, - icon: chartType === 'bar' ? : , - }, - ] - - const data = Array.from({ length: 46 }, (_, i) => { - const date = new Date() - date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart - - return { - timestamp: date.toISOString(), - requests: Math.floor(Math.random() * 100), - } - }).reverse() - - useEffect(() => { - setTimeout(() => { - setIsLoading(false) - }, 1000) - }, []) - - return ( -
- - - - Total Requests - - - } - title="No data to show" - description="It may take up to 24 hours for data to refresh" - /> - } - loadingState={} - > -
- {chartType === 'bar' ? ( - `${value}k`, - width: 80, - }} - isFullHeight={true} - /> - ) : ( - `${value}k`, - width: 80, - }} - isFullHeight={true} - /> - )} -
-
-
-
-
- ) -} diff --git a/apps/design-system/__registry__/default/block/chart-composed-basic.tsx b/apps/design-system/__registry__/default/block/chart-composed-basic.tsx deleted file mode 100644 index 607363710eb09..0000000000000 --- a/apps/design-system/__registry__/default/block/chart-composed-basic.tsx +++ /dev/null @@ -1,142 +0,0 @@ -'use client' - -import { BarChart2, ExternalLink } from 'lucide-react' -import { useEffect, useState } from 'react' -import { - Chart, - ChartActions, - ChartBar, - ChartCard, - ChartContent, - ChartEmptyState, - ChartHeader, - ChartLine, - ChartLoadingState, - ChartTitle, -} from 'ui-patterns/Chart' - -export default function ComposedChartBasic() { - const [isLoading, setIsLoading] = useState(true) - - const actions = [ - { - label: 'Open in Logs Explorer', - onClick: () => { - alert('Ta da! You clicked me! ๐ŸŽ‰') - }, - icon: , - }, - ] - - const data = Array.from({ length: 40 }, (_, i) => { - const date = new Date() - date.setMinutes(date.getMinutes() - i * 3) // Each point 3 minutes apart - - const progress = i / 40 - const standard_score = Math.floor(55 + progress * 55 + (Math.random() - 0.5) * 12) - const performance = Math.floor(35 + progress * 35 + (Math.random() - 0.5) * 10) - const efficiency = Math.floor(25 + progress * 25 + (Math.random() - 0.5) * 12) - - return { - timestamp: date.toISOString(), - standard_score: Math.max(0, Math.min(100, standard_score)), - performance: Math.max(0, Math.min(100, performance)), - efficiency: Math.max(0, Math.min(100, efficiency)), - } - }).reverse() - - const chartConfig = { - standard_score: { - label: 'Standard Score', - color: 'hsl(var(--brand-default))', - }, - performance: { - label: 'Performance', - color: 'hsl(var(--chart-2))', - }, - efficiency: { - label: 'Efficiency', - color: 'hsl(var(--chart-5))', - }, - } - - useEffect(() => { - setTimeout(() => { - setIsLoading(false) - }, 1000) - }, []) - - return ( -
- - - - Standard Bar Chart - - - } - title="No data to show" - description="It may take up to 24 hours for data to refresh" - /> - } - loadingState={} - > -
- `${value}k`, - width: 80, - }} - isFullHeight={true} - /> -
-
-
-
- - - - - Standard Line Chart - - - } - title="No data to show" - description="It may take up to 24 hours for data to refresh" - /> - } - loadingState={} - > -
- `${value}k`, - width: 80, - }} - isFullHeight={true} - /> -
-
-
-
-
- ) -} diff --git a/apps/design-system/__registry__/default/block/chart-composed-demo.tsx b/apps/design-system/__registry__/default/block/chart-composed-demo.tsx deleted file mode 100644 index 88b131af9254d..0000000000000 --- a/apps/design-system/__registry__/default/block/chart-composed-demo.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client' - -import { BarChart2 } from 'lucide-react' -import { useEffect, useState } from 'react' -import { - Chart, - ChartCard, - ChartContent, - ChartEmptyState, - ChartHeader, - ChartLoadingState, - ChartMetric, -} from 'ui-patterns/Chart' -import { LogsBarChart } from 'ui-patterns/LogsBarChart' - -export default function ComposedChartDemo() { - const [isLoading, setIsLoading] = useState(true) - - const data = Array.from({ length: 60 }, (_, i) => { - const date = new Date() - date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart - - return { - timestamp: date.toISOString(), - ok_count: Math.floor(Math.random() * 100), // Random value 0-99 - error_count: Math.floor(Math.random() * 50), // Random value 0-50 - warning_count: Math.floor(Math.random() * 50), // Random value 0-50 - } - }).reverse() - - const totalUsersValue = 4663 - - useEffect(() => { - setTimeout(() => { - setIsLoading(false) - }, 1000) - }, []) - - return ( -
- - - - -
- - -
-
- } - title="No data to show" - description="It may take up to 24 hours for data to refresh" - /> - } - loadingState={} - > -
- -
-
-
-
-
- ) -} diff --git a/apps/design-system/__registry__/default/block/chart-composed-metrics.tsx b/apps/design-system/__registry__/default/block/chart-composed-metrics.tsx deleted file mode 100644 index deab28e4ee23a..0000000000000 --- a/apps/design-system/__registry__/default/block/chart-composed-metrics.tsx +++ /dev/null @@ -1,100 +0,0 @@ -'use client' - -import { ExternalLink } from 'lucide-react' -import { useEffect, useState } from 'react' -import { Skeleton } from 'ui' -import { - Chart, - ChartActions, - ChartCard, - ChartContent, - ChartHeader, - ChartMetric, - ChartSparkline, -} from 'ui-patterns/Chart' - -export default function ChartComposedMetrics() { - const [data, setData] = useState>([]) - - const actions = [ - { - label: 'Open in Logs Explorer', - onClick: () => { - alert('Ta da! You clicked me! ๐ŸŽ‰') - }, - icon: , - }, - ] - - useEffect(() => { - const now = new Date() - setData( - Array.from({ length: 12 }, (_, i) => ({ - value: Math.floor(4000 + i * 100 + (Math.random() * 2000 - 800)), - timestamp: new Date(now.getTime() - (11 - i) * 60 * 60 * 1000).toISOString(), - })) - ) - }, []) - - const averageValue = data.reduce((acc, curr) => acc + curr.value, 0) / data.length - - const diff = data[data.length - 1]?.value - data[0]?.value || 0 - const diffPercentage = (diff / averageValue) * 100 - - return ( -
- - - - - - - - - - - - - - - - - - - - - - - } - > - - - - - - - - - - - } - > - - - - -
- ) -} diff --git a/apps/design-system/__registry__/default/block/chart-composed-states.tsx b/apps/design-system/__registry__/default/block/chart-composed-states.tsx deleted file mode 100644 index 2d3657cca2c72..0000000000000 --- a/apps/design-system/__registry__/default/block/chart-composed-states.tsx +++ /dev/null @@ -1,121 +0,0 @@ -'use client' - -import { BarChart2, ExternalLink } from 'lucide-react' -import { Badge, CriticalIcon } from 'ui' -import { - Chart, - ChartActions, - ChartCard, - ChartContent, - ChartDisabledState, - ChartEmptyState, - ChartHeader, - ChartLoadingState, - ChartTitle, -} from 'ui-patterns/Chart' - -export default function ChartComposedStates() { - const actions = [ - { - label: 'Open in Logs Explorer', - onClick: () => { - alert('Ta da! You clicked me! ๐ŸŽ‰') - }, - icon: , - }, - ] - - const disabledActions = [ - { - label: 'Upgrade to Pro', - href: '#', - }, - ] - - return ( -
- - - - Response Errors - - - }>My chart here... - - - - - - - Response Errors - - - } - title="Some error happened" - description="Error happened why trying to fetch data. Please try again later." - /> - } - emptyState={ - } - title="No data to show" - description="It may take up to 24 hours for data to refresh" - /> - } - loadingState={} - > - My chart here... - - - - - - - - Response Errors - - - } - title="No data to show" - description="It may take up to 24 hours for data to refresh" - /> - } - loadingState={} - > - My chart here... - - - - - - - - Response Errors - - - } - disabledState={ - Pro} - label="API Processing Time" - description="This chart is available on the Pro plan and above" - actions={disabledActions} - /> - } - > - My chart here... - - - -
- ) -} diff --git a/apps/design-system/__registry__/default/block/chart-composed-table.tsx b/apps/design-system/__registry__/default/block/chart-composed-table.tsx deleted file mode 100644 index a8089ed1f57d0..0000000000000 --- a/apps/design-system/__registry__/default/block/chart-composed-table.tsx +++ /dev/null @@ -1,106 +0,0 @@ -'use client' - -import { format } from 'date-fns' -import { BarChart2, ExternalLink } from 'lucide-react' -import { useEffect, useState } from 'react' -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui' -import { - Chart, - ChartActions, - ChartBar, - ChartCard, - ChartContent, - ChartEmptyState, - ChartFooter, - ChartHeader, - ChartLoadingState, - ChartTitle, -} from 'ui-patterns/Chart' - -export default function ChartComposedTable() { - const [isLoading, setIsLoading] = useState(true) - - const actions = [ - { - label: 'Open in Logs Explorer', - onClick: () => { - alert('Opening in Logs Explorer...') - }, - icon: , - }, - ] - - const data = Array.from({ length: 46 }, (_, i) => { - const date = new Date() - date.setMinutes(date.getMinutes() - i * 5) // Each point 5 minutes apart - - return { - timestamp: date.toISOString(), - standard_score: Math.floor(Math.random() * 100), - } - }).reverse() - - useEffect(() => { - setTimeout(() => { - setIsLoading(false) - }, 1000) - }, []) - - return ( -
- - - - Standard Bar Chart - - - } - title="No data to show" - description="It may take up to 24 hours for data to refresh" - /> - } - loadingState={} - > -
- `${value}k`, - width: 80, - }} - isFullHeight={true} - /> -
-
- - - - - Timestamp - Standard Score - - - - {data.slice(0, 3).map((item) => ( - - - {format(new Date(item.timestamp), 'MMM d, yyyy h:mm a')} - - {item.standard_score} - - ))} - -
-
-
-
-
- ) -} diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx deleted file mode 100644 index 7e9ec446ef2e0..0000000000000 --- a/apps/design-system/__registry__/index.tsx +++ /dev/null @@ -1,3738 +0,0 @@ -// @ts-nocheck -// This file is autogenerated by scripts/build-registry.ts -// Do not edit this file directly. -import * as React from "react" - -export const Index: Record = { - "default": { - "CollapsibleCardSection": { - name: "CollapsibleCardSection", - type: "components:fragment", - registryDependencies: undefined, - component: React.lazy(() => import("@/../../packages/ui-patterns/src/CollapsibleCardSection")), - source: "", - files: ["registry/default//CollapsibleCardSection.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "ConfirmationModal": { - name: "ConfirmationModal", - type: "components:fragment", - registryDependencies: undefined, - component: React.lazy(() => import("@/../../packages/ui-patterns/src/Dialogs/ConfirmationModal")), - source: "", - files: ["registry/default//Dialogs/ConfirmationModal.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "EmptyStatePresentational": { - name: "EmptyStatePresentational", - type: "components:fragment", - registryDependencies: undefined, - component: React.lazy(() => import("@/../../packages/ui-patterns/src/EmptyStatePresentational")), - source: "", - files: ["registry/default//EmptyStatePresentational/index.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "TextConfirmModal": { - name: "TextConfirmModal", - type: "components:fragment", - registryDependencies: undefined, - component: React.lazy(() => import("@/../../packages/ui-patterns/src/Dialogs/TextConfirmModal")), - source: "", - files: ["registry/default//Dialogs/TextConfirmModal.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "PageContainer": { - name: "PageContainer", - type: "components:fragment", - registryDependencies: undefined, - component: React.lazy(() => import("@/../../packages/ui-patterns/src/PageContainer")), - source: "", - files: ["registry/default//PageContainer/index.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "PageHeader": { - name: "PageHeader", - type: "components:fragment", - registryDependencies: undefined, - component: React.lazy(() => import("@/../../packages/ui-patterns/src/PageHeader")), - source: "", - files: ["registry/default//PageHeader/index.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "PageBreadcrumbs": { - name: "PageBreadcrumbs", - type: "components:fragment", - registryDependencies: undefined, - component: React.lazy(() => import("@/../../packages/ui-patterns/src/PageBreadcrumbs")), - source: "", - files: ["registry/default//PageBreadcrumbs/index.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "PageNav": { - name: "PageNav", - type: "components:fragment", - registryDependencies: undefined, - component: React.lazy(() => import("@/../../packages/ui-patterns/src/PageNav")), - source: "", - files: ["registry/default//PageNav/index.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "PageSection": { - name: "PageSection", - type: "components:fragment", - registryDependencies: undefined, - component: React.lazy(() => import("@/../../packages/ui-patterns/src/PageSection")), - source: "", - files: ["registry/default//PageSection/index.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "StatusCode": { - name: "StatusCode", - type: "components:fragment", - registryDependencies: undefined, - component: React.lazy(() => import("@/../../packages/ui-patterns/src/StatusCode")), - source: "", - files: ["registry/default//StatusCode/index.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "accordion-demo": { - name: "accordion-demo", - type: "components:example", - registryDependencies: ["accordion"], - component: React.lazy(() => import("@/registry/default/example/accordion-demo")), - source: "", - files: ["registry/default/example/accordion-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "admonition-demo": { - name: "admonition-demo", - type: "components:example", - registryDependencies: ["admonition"], - component: React.lazy(() => import("@/registry/default/example/admonition-demo")), - source: "", - files: ["registry/default/example/admonition-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "admonition-responsive": { - name: "admonition-responsive", - type: "components:example", - registryDependencies: ["admonition"], - component: React.lazy(() => import("@/registry/default/example/admonition-responsive")), - source: "", - files: ["registry/default/example/admonition-responsive.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "admonition-button": { - name: "admonition-button", - type: "components:example", - registryDependencies: ["admonition"], - component: React.lazy(() => import("@/registry/default/example/admonition-button")), - source: "", - files: ["registry/default/example/admonition-button.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "admonition-button-split": { - name: "admonition-button-split", - type: "components:example", - registryDependencies: ["admonition","button","dropdown-menu"], - component: React.lazy(() => import("@/registry/default/example/admonition-button-split")), - source: "", - files: ["registry/default/example/admonition-button-split.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "admonition-description-only": { - name: "admonition-description-only", - type: "components:example", - registryDependencies: ["admonition"], - component: React.lazy(() => import("@/registry/default/example/admonition-description-only")), - source: "", - files: ["registry/default/example/admonition-description-only.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "admonition-warning": { - name: "admonition-warning", - type: "components:example", - registryDependencies: ["admonition"], - component: React.lazy(() => import("@/registry/default/example/admonition-warning")), - source: "", - files: ["registry/default/example/admonition-warning.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "admonition-success": { - name: "admonition-success", - type: "components:example", - registryDependencies: ["admonition"], - component: React.lazy(() => import("@/registry/default/example/admonition-success")), - source: "", - files: ["registry/default/example/admonition-success.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "admonition-destructive": { - name: "admonition-destructive", - type: "components:example", - registryDependencies: ["admonition"], - component: React.lazy(() => import("@/registry/default/example/admonition-destructive")), - source: "", - files: ["registry/default/example/admonition-destructive.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "admonition-sandwiched": { - name: "admonition-sandwiched", - type: "components:example", - registryDependencies: ["admonition"], - component: React.lazy(() => import("@/registry/default/example/admonition-sandwiched")), - source: "", - files: ["registry/default/example/admonition-sandwiched.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "alert-demo": { - name: "alert-demo", - type: "components:example", - registryDependencies: ["alert"], - component: React.lazy(() => import("@/registry/default/example/alert-demo")), - source: "", - files: ["registry/default/example/alert-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "collapsible-alert-demo": { - name: "collapsible-alert-demo", - type: "components:example", - registryDependencies: ["collapsible-alert"], - component: React.lazy(() => import("@/registry/default/example/collapsible-alert-demo")), - source: "", - files: ["registry/default/example/collapsible-alert-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "collapsible-card-section-demo": { - name: "collapsible-card-section-demo", - type: "components:example", - registryDependencies: ["collapsible-card-section"], - component: React.lazy(() => import("@/registry/default/example/collapsible-card-section-demo")), - source: "", - files: ["registry/default/example/collapsible-card-section-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "alert-dialog-demo": { - name: "alert-dialog-demo", - type: "components:example", - registryDependencies: ["alert-dialog","button"], - component: React.lazy(() => import("@/registry/default/example/alert-dialog-demo")), - source: "", - files: ["registry/default/example/alert-dialog-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "alert-dialog-close-only": { - name: "alert-dialog-close-only", - type: "components:example", - registryDependencies: ["alert-dialog","button"], - component: React.lazy(() => import("@/registry/default/example/alert-dialog-close-only")), - source: "", - files: ["registry/default/example/alert-dialog-close-only.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "alert-dialog-async": { - name: "alert-dialog-async", - type: "components:example", - registryDependencies: ["alert-dialog","button"], - component: React.lazy(() => import("@/registry/default/example/alert-dialog-async")), - source: "", - files: ["registry/default/example/alert-dialog-async.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "alert-dialog-async-error": { - name: "alert-dialog-async-error", - type: "components:example", - registryDependencies: ["alert-dialog","button","admonition"], - component: React.lazy(() => import("@/registry/default/example/alert-dialog-async-error")), - source: "", - files: ["registry/default/example/alert-dialog-async-error.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "alert-dialog-destructive": { - name: "alert-dialog-destructive", - type: "components:example", - registryDependencies: ["alert-dialog","button"], - component: React.lazy(() => import("@/registry/default/example/alert-dialog-destructive")), - source: "", - files: ["registry/default/example/alert-dialog-destructive.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "alert-dialog-warning": { - name: "alert-dialog-warning", - type: "components:example", - registryDependencies: ["alert-dialog","button"], - component: React.lazy(() => import("@/registry/default/example/alert-dialog-warning")), - source: "", - files: ["registry/default/example/alert-dialog-warning.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "aspect-ratio-demo": { - name: "aspect-ratio-demo", - type: "components:example", - registryDependencies: ["aspect-ratio"], - component: React.lazy(() => import("@/registry/default/example/aspect-ratio-demo")), - source: "", - files: ["registry/default/example/aspect-ratio-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "avatar-demo": { - name: "avatar-demo", - type: "components:example", - registryDependencies: ["avatar"], - component: React.lazy(() => import("@/registry/default/example/avatar-demo")), - source: "", - files: ["registry/default/example/avatar-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "badge-demo": { - name: "badge-demo", - type: "components:example", - registryDependencies: ["badge"], - component: React.lazy(() => import("@/registry/default/example/badge-demo")), - source: "", - files: ["registry/default/example/badge-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "badge-state": { - name: "badge-state", - type: "components:example", - registryDependencies: ["badge"], - component: React.lazy(() => import("@/registry/default/example/badge-state")), - source: "", - files: ["registry/default/example/badge-state.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "badge-secondary": { - name: "badge-secondary", - type: "components:example", - registryDependencies: ["badge"], - component: React.lazy(() => import("@/registry/default/example/badge-secondary")), - source: "", - files: ["registry/default/example/badge-secondary.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "breadcrumb-demo": { - name: "breadcrumb-demo", - type: "components:example", - registryDependencies: ["breadcrumb"], - component: React.lazy(() => import("@/registry/default/example/breadcrumb-demo")), - source: "", - files: ["registry/default/example/breadcrumb-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "breadcrumb-separator": { - name: "breadcrumb-separator", - type: "components:example", - registryDependencies: ["breadcrumb"], - component: React.lazy(() => import("@/registry/default/example/breadcrumb-separator")), - source: "", - files: ["registry/default/example/breadcrumb-separator.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "breadcrumb-dropdown": { - name: "breadcrumb-dropdown", - type: "components:example", - registryDependencies: ["breadcrumb","dropdown-menu"], - component: React.lazy(() => import("@/registry/default/example/breadcrumb-dropdown")), - source: "", - files: ["registry/default/example/breadcrumb-dropdown.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "breadcrumb-ellipsis": { - name: "breadcrumb-ellipsis", - type: "components:example", - registryDependencies: ["breadcrumb"], - component: React.lazy(() => import("@/registry/default/example/breadcrumb-ellipsis")), - source: "", - files: ["registry/default/example/breadcrumb-ellipsis.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "breadcrumb-link": { - name: "breadcrumb-link", - type: "components:example", - registryDependencies: ["breadcrumb"], - component: React.lazy(() => import("@/registry/default/example/breadcrumb-link")), - source: "", - files: ["registry/default/example/breadcrumb-link.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "breadcrumb-responsive": { - name: "breadcrumb-responsive", - type: "components:example", - registryDependencies: ["breadcrumb","button","drawer","dropdown-menu"], - component: React.lazy(() => import("@/registry/default/example/breadcrumb-responsive")), - source: "", - files: ["registry/default/example/breadcrumb-responsive.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-demo": { - name: "button-demo", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-demo")), - source: "", - files: ["registry/default/example/button-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-sizes": { - name: "button-sizes", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-sizes")), - source: "", - files: ["registry/default/example/button-sizes.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-default": { - name: "button-default", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-default")), - source: "", - files: ["registry/default/example/button-default.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-warning": { - name: "button-warning", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-warning")), - source: "", - files: ["registry/default/example/button-warning.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-secondary": { - name: "button-secondary", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-secondary")), - source: "", - files: ["registry/default/example/button-secondary.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-destructive": { - name: "button-destructive", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-destructive")), - source: "", - files: ["registry/default/example/button-destructive.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-outline": { - name: "button-outline", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-outline")), - source: "", - files: ["registry/default/example/button-outline.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-ghost": { - name: "button-ghost", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-ghost")), - source: "", - files: ["registry/default/example/button-ghost.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-link": { - name: "button-link", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-link")), - source: "", - files: ["registry/default/example/button-link.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-with-icon": { - name: "button-with-icon", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-with-icon")), - source: "", - files: ["registry/default/example/button-with-icon.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-loading": { - name: "button-loading", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-loading")), - source: "", - files: ["registry/default/example/button-loading.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-icon": { - name: "button-icon", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-icon")), - source: "", - files: ["registry/default/example/button-icon.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-as-child": { - name: "button-as-child", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/button-as-child")), - source: "", - files: ["registry/default/example/button-as-child.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "button-split-dropdown": { - name: "button-split-dropdown", - type: "components:example", - registryDependencies: ["button","dropdown-menu"], - component: React.lazy(() => import("@/registry/default/example/button-split-dropdown")), - source: "", - files: ["registry/default/example/button-split-dropdown.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "calendar-demo": { - name: "calendar-demo", - type: "components:example", - registryDependencies: ["calendar"], - component: React.lazy(() => import("@/registry/default/example/calendar-demo")), - source: "", - files: ["registry/default/example/calendar-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "calendar-form": { - name: "calendar-form", - type: "components:example", - registryDependencies: ["calendar","form","popover"], - component: React.lazy(() => import("@/registry/default/example/calendar-form")), - source: "", - files: ["registry/default/example/calendar-form.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "calendar-disabled-days-demo": { - name: "calendar-disabled-days-demo", - type: "components:example", - registryDependencies: ["calendar","form","popover"], - component: React.lazy(() => import("@/registry/default/example/calendar-disabled-days-demo")), - source: "", - files: ["registry/default/example/calendar-disabled-days-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "single-value-field-array-demo": { - name: "single-value-field-array-demo", - type: "components:example", - registryDependencies: ["button","form","input"], - component: React.lazy(() => import("@/registry/default/example/single-value-field-array-demo")), - source: "", - files: ["registry/default/example/single-value-field-array-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "checkbox-demo": { - name: "checkbox-demo", - type: "components:example", - registryDependencies: ["checkbox"], - component: React.lazy(() => import("@/registry/default/example/checkbox-demo")), - source: "", - files: ["registry/default/example/checkbox-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "checkbox-disabled": { - name: "checkbox-disabled", - type: "components:example", - registryDependencies: ["checkbox"], - component: React.lazy(() => import("@/registry/default/example/checkbox-disabled")), - source: "", - files: ["registry/default/example/checkbox-disabled.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "checkbox-form-multiple": { - name: "checkbox-form-multiple", - type: "components:example", - registryDependencies: ["checkbox","form"], - component: React.lazy(() => import("@/registry/default/example/checkbox-form-multiple")), - source: "", - files: ["registry/default/example/checkbox-form-multiple.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "checkbox-form-single": { - name: "checkbox-form-single", - type: "components:example", - registryDependencies: ["checkbox","form"], - component: React.lazy(() => import("@/registry/default/example/checkbox-form-single")), - source: "", - files: ["registry/default/example/checkbox-form-single.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "checkbox-with-text": { - name: "checkbox-with-text", - type: "components:example", - registryDependencies: ["checkbox"], - component: React.lazy(() => import("@/registry/default/example/checkbox-with-text")), - source: "", - files: ["registry/default/example/checkbox-with-text.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "collapsible-demo": { - name: "collapsible-demo", - type: "components:example", - registryDependencies: ["collapsible"], - component: React.lazy(() => import("@/registry/default/example/collapsible-demo")), - source: "", - files: ["registry/default/example/collapsible-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "combobox-demo": { - name: "combobox-demo", - type: "components:example", - registryDependencies: ["command"], - component: React.lazy(() => import("@/registry/default/example/combobox-demo")), - source: "", - files: ["registry/default/example/combobox-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "combobox-dropdown-menu": { - name: "combobox-dropdown-menu", - type: "components:example", - registryDependencies: ["command","dropdown-menu","button"], - component: React.lazy(() => import("@/registry/default/example/combobox-dropdown-menu")), - source: "", - files: ["registry/default/example/combobox-dropdown-menu.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "combobox-form": { - name: "combobox-form", - type: "components:example", - registryDependencies: ["command","form"], - component: React.lazy(() => import("@/registry/default/example/combobox-form")), - source: "", - files: ["registry/default/example/combobox-form.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "combobox-popover": { - name: "combobox-popover", - type: "components:example", - registryDependencies: ["combobox","popover"], - component: React.lazy(() => import("@/registry/default/example/combobox-popover")), - source: "", - files: ["registry/default/example/combobox-popover.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "combobox-responsive": { - name: "combobox-responsive", - type: "components:example", - registryDependencies: ["combobox","popover","drawer"], - component: React.lazy(() => import("@/registry/default/example/combobox-responsive")), - source: "", - files: ["registry/default/example/combobox-responsive.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "command-demo": { - name: "command-demo", - type: "components:example", - registryDependencies: ["command"], - component: React.lazy(() => import("@/registry/default/example/command-demo")), - source: "", - files: ["registry/default/example/command-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "command-dialog": { - name: "command-dialog", - type: "components:example", - registryDependencies: ["command","dialog"], - component: React.lazy(() => import("@/registry/default/example/command-dialog")), - source: "", - files: ["registry/default/example/command-dialog.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "commandmenu-demo": { - name: "commandmenu-demo", - type: "components:example", - registryDependencies: ["commandmenu"], - component: React.lazy(() => import("@/registry/default/example/commandmenu-demo")), - source: "", - files: ["registry/default/example/commandmenu-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "commandmenu-badge": { - name: "commandmenu-badge", - type: "components:example", - registryDependencies: ["commandmenu"], - component: React.lazy(() => import("@/registry/default/example/commandmenu-badge")), - source: "", - files: ["registry/default/example/commandmenu-badge.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "commandmenu-icon": { - name: "commandmenu-icon", - type: "components:example", - registryDependencies: ["commandmenu"], - component: React.lazy(() => import("@/registry/default/example/commandmenu-icon")), - source: "", - files: ["registry/default/example/commandmenu-icon.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "commandmenu-hidden": { - name: "commandmenu-hidden", - type: "components:example", - registryDependencies: ["commandmenu"], - component: React.lazy(() => import("@/registry/default/example/commandmenu-hidden")), - source: "", - files: ["registry/default/example/commandmenu-hidden.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "commandmenu-force": { - name: "commandmenu-force", - type: "components:example", - registryDependencies: ["commandmenu"], - component: React.lazy(() => import("@/registry/default/example/commandmenu-force")), - source: "", - files: ["registry/default/example/commandmenu-force.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "commandmenu-conditional": { - name: "commandmenu-conditional", - type: "components:example", - registryDependencies: ["commandmenu"], - component: React.lazy(() => import("@/registry/default/example/commandmenu-conditional")), - source: "", - files: ["registry/default/example/commandmenu-conditional.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "commandmenu-subpage": { - name: "commandmenu-subpage", - type: "components:example", - registryDependencies: ["commandmenu"], - component: React.lazy(() => import("@/registry/default/example/commandmenu-subpage")), - source: "", - files: ["registry/default/example/commandmenu-subpage.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "commandmenu-subpage-custom": { - name: "commandmenu-subpage-custom", - type: "components:example", - registryDependencies: ["commandmenu"], - component: React.lazy(() => import("@/registry/default/example/commandmenu-subpage-custom")), - source: "", - files: ["registry/default/example/commandmenu-subpage-custom.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "context-menu-demo": { - name: "context-menu-demo", - type: "components:example", - registryDependencies: ["context-menu"], - component: React.lazy(() => import("@/registry/default/example/context-menu-demo")), - source: "", - files: ["registry/default/example/context-menu-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "data-input-demo": { - name: "data-input-demo", - type: "components:example", - registryDependencies: ["data-input"], - component: React.lazy(() => import("@/registry/default/example/data-input-demo")), - source: "", - files: ["registry/default/example/data-input-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "data-input-with-copy": { - name: "data-input-with-copy", - type: "components:example", - registryDependencies: ["data-input"], - component: React.lazy(() => import("@/registry/default/example/data-input-with-copy")), - source: "", - files: ["registry/default/example/data-input-with-copy.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "data-input-with-copy-secret": { - name: "data-input-with-copy-secret", - type: "components:example", - registryDependencies: ["data-input"], - component: React.lazy(() => import("@/registry/default/example/data-input-with-copy-secret")), - source: "", - files: ["registry/default/example/data-input-with-copy-secret.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "data-input-with-reveal-copy": { - name: "data-input-with-reveal-copy", - type: "components:example", - registryDependencies: ["data-input"], - component: React.lazy(() => import("@/registry/default/example/data-input-with-reveal-copy")), - source: "", - files: ["registry/default/example/data-input-with-reveal-copy.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "data-input-with-reveal-copy-editable": { - name: "data-input-with-reveal-copy-editable", - type: "components:example", - registryDependencies: ["data-input"], - component: React.lazy(() => import("@/registry/default/example/data-input-with-reveal-copy-editable")), - source: "", - files: ["registry/default/example/data-input-with-reveal-copy-editable.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "data-input-with-reveal-copy-editable-empty": { - name: "data-input-with-reveal-copy-editable-empty", - type: "components:example", - registryDependencies: ["data-input"], - component: React.lazy(() => import("@/registry/default/example/data-input-with-reveal-copy-editable-empty")), - source: "", - files: ["registry/default/example/data-input-with-reveal-copy-editable-empty.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "date-picker-demo": { - name: "date-picker-demo", - type: "components:example", - registryDependencies: ["button","calendar","popover"], - component: React.lazy(() => import("@/registry/default/example/date-picker-demo")), - source: "", - files: ["registry/default/example/date-picker-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "date-picker-form": { - name: "date-picker-form", - type: "components:example", - registryDependencies: ["button","calendar","form","popover"], - component: React.lazy(() => import("@/registry/default/example/date-picker-form")), - source: "", - files: ["registry/default/example/date-picker-form.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "date-picker-with-presets": { - name: "date-picker-with-presets", - type: "components:example", - registryDependencies: ["button","calendar","popover","select"], - component: React.lazy(() => import("@/registry/default/example/date-picker-with-presets")), - source: "", - files: ["registry/default/example/date-picker-with-presets.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "date-picker-with-range": { - name: "date-picker-with-range", - type: "components:example", - registryDependencies: ["button","calendar","popover"], - component: React.lazy(() => import("@/registry/default/example/date-picker-with-range")), - source: "", - files: ["registry/default/example/date-picker-with-range.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "dialog-demo": { - name: "dialog-demo", - type: "components:example", - registryDependencies: ["dialog"], - component: React.lazy(() => import("@/registry/default/example/dialog-demo")), - source: "", - files: ["registry/default/example/dialog-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "dialog-close-button": { - name: "dialog-close-button", - type: "components:example", - registryDependencies: ["dialog","button"], - component: React.lazy(() => import("@/registry/default/example/dialog-close-button")), - source: "", - files: ["registry/default/example/dialog-close-button.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "dialog-centered-off": { - name: "dialog-centered-off", - type: "components:example", - registryDependencies: ["dialog","button"], - component: React.lazy(() => import("@/registry/default/example/dialog-centered-off")), - source: "", - files: ["registry/default/example/dialog-centered-off.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "drawer-demo": { - name: "drawer-demo", - type: "components:example", - registryDependencies: ["drawer"], - component: React.lazy(() => import("@/registry/default/example/drawer-demo")), - source: "", - files: ["registry/default/example/drawer-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "drawer-dialog": { - name: "drawer-dialog", - type: "components:example", - registryDependencies: ["drawer","dialog"], - component: React.lazy(() => import("@/registry/default/example/drawer-dialog")), - source: "", - files: ["registry/default/example/drawer-dialog.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "dropdown-menu-demo": { - name: "dropdown-menu-demo", - type: "components:example", - registryDependencies: ["dropdown-menu"], - component: React.lazy(() => import("@/registry/default/example/dropdown-menu-demo")), - source: "", - files: ["registry/default/example/dropdown-menu-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "dropdown-menu-checkboxes": { - name: "dropdown-menu-checkboxes", - type: "components:example", - registryDependencies: ["dropdown-menu","checkbox"], - component: React.lazy(() => import("@/registry/default/example/dropdown-menu-checkboxes")), - source: "", - files: ["registry/default/example/dropdown-menu-checkboxes.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "dropdown-menu-radio-group": { - name: "dropdown-menu-radio-group", - type: "components:example", - registryDependencies: ["dropdown-menu","radio-group"], - component: React.lazy(() => import("@/registry/default/example/dropdown-menu-radio-group")), - source: "", - files: ["registry/default/example/dropdown-menu-radio-group.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "filter-bar-demo": { - name: "filter-bar-demo", - type: "components:example", - registryDependencies: ["filter-bar"], - component: React.lazy(() => import("@/registry/default/example/filter-bar-demo")), - source: "", - files: ["registry/default/example/filter-bar-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "hover-card-demo": { - name: "hover-card-demo", - type: "components:example", - registryDependencies: ["hover-card"], - component: React.lazy(() => import("@/registry/default/example/hover-card-demo")), - source: "", - files: ["registry/default/example/hover-card-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-demo": { - name: "input-demo", - type: "components:example", - registryDependencies: ["input"], - component: React.lazy(() => import("@/registry/default/example/input-demo")), - source: "", - files: ["registry/default/example/input-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-disabled": { - name: "input-disabled", - type: "components:example", - registryDependencies: ["input"], - component: React.lazy(() => import("@/registry/default/example/input-disabled")), - source: "", - files: ["registry/default/example/input-disabled.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-file": { - name: "input-file", - type: "components:example", - registryDependencies: ["input"], - component: React.lazy(() => import("@/registry/default/example/input-file")), - source: "", - files: ["registry/default/example/input-file.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-form": { - name: "input-form", - type: "components:example", - registryDependencies: ["input","button","form"], - component: React.lazy(() => import("@/registry/default/example/input-form")), - source: "", - files: ["registry/default/example/input-form.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-with-button": { - name: "input-with-button", - type: "components:example", - registryDependencies: ["input","button"], - component: React.lazy(() => import("@/registry/default/example/input-with-button")), - source: "", - files: ["registry/default/example/input-with-button.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-with-label": { - name: "input-with-label", - type: "components:example", - registryDependencies: ["input","button","label"], - component: React.lazy(() => import("@/registry/default/example/input-with-label")), - source: "", - files: ["registry/default/example/input-with-label.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-with-text": { - name: "input-with-text", - type: "components:example", - registryDependencies: ["input","button","label"], - component: React.lazy(() => import("@/registry/default/example/input-with-text")), - source: "", - files: ["registry/default/example/input-with-text.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-otp-demo": { - name: "input-otp-demo", - type: "components:example", - registryDependencies: ["input-otp"], - component: React.lazy(() => import("@/registry/default/example/input-otp-demo")), - source: "", - files: ["registry/default/example/input-otp-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-otp-pattern": { - name: "input-otp-pattern", - type: "components:example", - registryDependencies: ["input-otp"], - component: React.lazy(() => import("@/registry/default/example/input-otp-pattern")), - source: "", - files: ["registry/default/example/input-otp-pattern.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-otp-separator": { - name: "input-otp-separator", - type: "components:example", - registryDependencies: ["input-otp"], - component: React.lazy(() => import("@/registry/default/example/input-otp-separator")), - source: "", - files: ["registry/default/example/input-otp-separator.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-otp-controlled": { - name: "input-otp-controlled", - type: "components:example", - registryDependencies: ["input-otp"], - component: React.lazy(() => import("@/registry/default/example/input-otp-controlled")), - source: "", - files: ["registry/default/example/input-otp-controlled.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "input-otp-form": { - name: "input-otp-form", - type: "components:example", - registryDependencies: ["input-otp","form"], - component: React.lazy(() => import("@/registry/default/example/input-otp-form")), - source: "", - files: ["registry/default/example/input-otp-form.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "keyboard-shortcut-demo": { - name: "keyboard-shortcut-demo", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/keyboard-shortcut-demo")), - source: "", - files: ["registry/default/example/keyboard-shortcut-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "keyboard-shortcut-pill": { - name: "keyboard-shortcut-pill", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/keyboard-shortcut-pill")), - source: "", - files: ["registry/default/example/keyboard-shortcut-pill.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "keyboard-shortcut-inline": { - name: "keyboard-shortcut-inline", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/keyboard-shortcut-inline")), - source: "", - files: ["registry/default/example/keyboard-shortcut-inline.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "label-demo": { - name: "label-demo", - type: "components:example", - registryDependencies: ["label"], - component: React.lazy(() => import("@/registry/default/example/label-demo")), - source: "", - files: ["registry/default/example/label-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "menubar-demo": { - name: "menubar-demo", - type: "components:example", - registryDependencies: ["menubar"], - component: React.lazy(() => import("@/registry/default/example/menubar-demo")), - source: "", - files: ["registry/default/example/menubar-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "navigation-menu-demo": { - name: "navigation-menu-demo", - type: "components:example", - registryDependencies: ["navigation-menu"], - component: React.lazy(() => import("@/registry/default/example/navigation-menu-demo")), - source: "", - files: ["registry/default/example/navigation-menu-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "navigation-menu-responsive": { - name: "navigation-menu-responsive", - type: "components:example", - registryDependencies: ["navigation-menu"], - component: React.lazy(() => import("@/registry/default/example/navigation-menu-responsive")), - source: "", - files: ["registry/default/example/navigation-menu-responsive.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "nav-menu-demo": { - name: "nav-menu-demo", - type: "components:example", - registryDependencies: ["nav-menu"], - component: React.lazy(() => import("@/registry/default/example/nav-menu-demo")), - source: "", - files: ["registry/default/example/nav-menu-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "nav-menu-badges": { - name: "nav-menu-badges", - type: "components:example", - registryDependencies: ["nav-menu"], - component: React.lazy(() => import("@/registry/default/example/nav-menu-badges")), - source: "", - files: ["registry/default/example/nav-menu-badges.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "nav-menu-icons": { - name: "nav-menu-icons", - type: "components:example", - registryDependencies: ["nav-menu"], - component: React.lazy(() => import("@/registry/default/example/nav-menu-icons")), - source: "", - files: ["registry/default/example/nav-menu-icons.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "popover-demo": { - name: "popover-demo", - type: "components:example", - registryDependencies: ["popover"], - component: React.lazy(() => import("@/registry/default/example/popover-demo")), - source: "", - files: ["registry/default/example/popover-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "progress-demo": { - name: "progress-demo", - type: "components:example", - registryDependencies: ["progress"], - component: React.lazy(() => import("@/registry/default/example/progress-demo")), - source: "", - files: ["registry/default/example/progress-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "radio-group-demo": { - name: "radio-group-demo", - type: "components:example", - registryDependencies: ["radio-group"], - component: React.lazy(() => import("@/registry/default/example/radio-group-demo")), - source: "", - files: ["registry/default/example/radio-group-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "radio-group-form": { - name: "radio-group-form", - type: "components:example", - registryDependencies: ["radio-group","form"], - component: React.lazy(() => import("@/registry/default/example/radio-group-form")), - source: "", - files: ["registry/default/example/radio-group-form.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "radio-group-stacked-demo": { - name: "radio-group-stacked-demo", - type: "components:example", - registryDependencies: ["radio-group"], - component: React.lazy(() => import("@/registry/default/example/radio-group-stacked-demo")), - source: "", - files: ["registry/default/example/radio-group-stacked-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "radio-group-stacked-form": { - name: "radio-group-stacked-form", - type: "components:example", - registryDependencies: ["radio-group","form"], - component: React.lazy(() => import("@/registry/default/example/radio-group-stacked-form")), - source: "", - files: ["registry/default/example/radio-group-stacked-form.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "radio-group-card-demo": { - name: "radio-group-card-demo", - type: "components:example", - registryDependencies: ["radio-group"], - component: React.lazy(() => import("@/registry/default/example/radio-group-card-demo")), - source: "", - files: ["registry/default/example/radio-group-card-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "radio-group-card-form": { - name: "radio-group-card-form", - type: "components:example", - registryDependencies: ["radio-group","form"], - component: React.lazy(() => import("@/registry/default/example/radio-group-card-form")), - source: "", - files: ["registry/default/example/radio-group-card-form.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "radio-group-card-with-children": { - name: "radio-group-card-with-children", - type: "components:example", - registryDependencies: ["radio-group","form"], - component: React.lazy(() => import("@/registry/default/example/radio-group-card-with-children")), - source: "", - files: ["registry/default/example/radio-group-card-with-children.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "resizable-demo": { - name: "resizable-demo", - type: "components:example", - registryDependencies: ["resizable"], - component: React.lazy(() => import("@/registry/default/example/resizable-demo")), - source: "", - files: ["registry/default/example/resizable-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "resizable-demo-with-handle": { - name: "resizable-demo-with-handle", - type: "components:example", - registryDependencies: ["resizable"], - component: React.lazy(() => import("@/registry/default/example/resizable-demo-with-handle")), - source: "", - files: ["registry/default/example/resizable-demo-with-handle.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "resizable-vertical": { - name: "resizable-vertical", - type: "components:example", - registryDependencies: ["resizable"], - component: React.lazy(() => import("@/registry/default/example/resizable-vertical")), - source: "", - files: ["registry/default/example/resizable-vertical.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "resizable-handle": { - name: "resizable-handle", - type: "components:example", - registryDependencies: ["resizable"], - component: React.lazy(() => import("@/registry/default/example/resizable-handle")), - source: "", - files: ["registry/default/example/resizable-handle.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "scroll-area-demo": { - name: "scroll-area-demo", - type: "components:example", - registryDependencies: ["scroll-area"], - component: React.lazy(() => import("@/registry/default/example/scroll-area-demo")), - source: "", - files: ["registry/default/example/scroll-area-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "scroll-area-horizontal-demo": { - name: "scroll-area-horizontal-demo", - type: "components:example", - registryDependencies: ["scroll-area"], - component: React.lazy(() => import("@/registry/default/example/scroll-area-horizontal-demo")), - source: "", - files: ["registry/default/example/scroll-area-horizontal-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "select-demo": { - name: "select-demo", - type: "components:example", - registryDependencies: ["select"], - component: React.lazy(() => import("@/registry/default/example/select-demo")), - source: "", - files: ["registry/default/example/select-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "select-scrollable": { - name: "select-scrollable", - type: "components:example", - registryDependencies: ["select"], - component: React.lazy(() => import("@/registry/default/example/select-scrollable")), - source: "", - files: ["registry/default/example/select-scrollable.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "select-form": { - name: "select-form", - type: "components:example", - registryDependencies: ["select"], - component: React.lazy(() => import("@/registry/default/example/select-form")), - source: "", - files: ["registry/default/example/select-form.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "separator-demo": { - name: "separator-demo", - type: "components:example", - registryDependencies: ["separator"], - component: React.lazy(() => import("@/registry/default/example/separator-demo")), - source: "", - files: ["registry/default/example/separator-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "sheet-confirm-on-close-demo": { - name: "sheet-confirm-on-close-demo", - type: "components:example", - registryDependencies: ["alert-dialog","button","input","label","separator","sheet"], - component: React.lazy(() => import("@/registry/default/example/sheet-confirm-on-close-demo")), - source: "", - files: ["registry/default/example/sheet-confirm-on-close-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "sheet-demo": { - name: "sheet-demo", - type: "components:example", - registryDependencies: ["sheet"], - component: React.lazy(() => import("@/registry/default/example/sheet-demo")), - source: "", - files: ["registry/default/example/sheet-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "sheet-nonmodal": { - name: "sheet-nonmodal", - type: "components:example", - registryDependencies: ["sheet"], - component: React.lazy(() => import("@/registry/default/example/sheet-nonmodal")), - source: "", - files: ["registry/default/example/sheet-nonmodal.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "sheet-side": { - name: "sheet-side", - type: "components:example", - registryDependencies: ["sheet"], - component: React.lazy(() => import("@/registry/default/example/sheet-side")), - source: "", - files: ["registry/default/example/sheet-side.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "skeleton-demo": { - name: "skeleton-demo", - type: "components:example", - registryDependencies: ["skeleton"], - component: React.lazy(() => import("@/registry/default/example/skeleton-demo")), - source: "", - files: ["registry/default/example/skeleton-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "skeleton-card": { - name: "skeleton-card", - type: "components:example", - registryDependencies: ["skeleton"], - component: React.lazy(() => import("@/registry/default/example/skeleton-card")), - source: "", - files: ["registry/default/example/skeleton-card.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "slider-demo": { - name: "slider-demo", - type: "components:example", - registryDependencies: ["slider"], - component: React.lazy(() => import("@/registry/default/example/slider-demo")), - source: "", - files: ["registry/default/example/slider-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "slider-minMax-range": { - name: "slider-minMax-range", - type: "components:example", - registryDependencies: ["slider"], - component: React.lazy(() => import("@/registry/default/example/slider-minMax-range")), - source: "", - files: ["registry/default/example/slider-minMax-range.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "sonner-demo": { - name: "sonner-demo", - type: "components:example", - registryDependencies: ["sonner"], - component: React.lazy(() => import("@/registry/default/example/sonner-demo")), - source: "", - files: ["registry/default/example/sonner-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "sonner-types": { - name: "sonner-types", - type: "components:example", - registryDependencies: ["sonner"], - component: React.lazy(() => import("@/registry/default/example/sonner-types")), - source: "", - files: ["registry/default/example/sonner-types.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "sonner-upload": { - name: "sonner-upload", - type: "components:example", - registryDependencies: ["sonner"], - component: React.lazy(() => import("@/registry/default/example/sonner-upload")), - source: "", - files: ["registry/default/example/sonner-upload.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "success-check-demo": { - name: "success-check-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/success-check-demo")), - source: "", - files: ["registry/default/example/success-check-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "success-check-selected": { - name: "success-check-selected", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/success-check-selected")), - source: "", - files: ["registry/default/example/success-check-selected.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "success-check-progress": { - name: "success-check-progress", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/success-check-progress")), - source: "", - files: ["registry/default/example/success-check-progress.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "switch-demo": { - name: "switch-demo", - type: "components:example", - registryDependencies: ["switch"], - component: React.lazy(() => import("@/registry/default/example/switch-demo")), - source: "", - files: ["registry/default/example/switch-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "switch-form": { - name: "switch-form", - type: "components:example", - registryDependencies: ["switch","form"], - component: React.lazy(() => import("@/registry/default/example/switch-form")), - source: "", - files: ["registry/default/example/switch-form.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "table-cross-link": { - name: "table-cross-link", - type: "components:example", - registryDependencies: ["table"], - component: React.lazy(() => import("@/registry/default/example/table-cross-link")), - source: "", - files: ["registry/default/example/table-cross-link.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "table-demo": { - name: "table-demo", - type: "components:example", - registryDependencies: ["table"], - component: React.lazy(() => import("@/registry/default/example/table-demo")), - source: "", - files: ["registry/default/example/table-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "table-actions": { - name: "table-actions", - type: "components:example", - registryDependencies: ["table"], - component: React.lazy(() => import("@/registry/default/example/table-actions")), - source: "", - files: ["registry/default/example/table-actions.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "table-icons": { - name: "table-icons", - type: "components:example", - registryDependencies: ["table"], - component: React.lazy(() => import("@/registry/default/example/table-icons")), - source: "", - files: ["registry/default/example/table-icons.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "table-row-link": { - name: "table-row-link", - type: "components:example", - registryDependencies: ["table"], - component: React.lazy(() => import("@/registry/default/example/table-row-link")), - source: "", - files: ["registry/default/example/table-row-link.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "table-row-link-actions": { - name: "table-row-link-actions", - type: "components:example", - registryDependencies: ["table"], - component: React.lazy(() => import("@/registry/default/example/table-row-link-actions")), - source: "", - files: ["registry/default/example/table-row-link-actions.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "table-sort": { - name: "table-sort", - type: "components:example", - registryDependencies: ["table"], - component: React.lazy(() => import("@/registry/default/example/table-sort")), - source: "", - files: ["registry/default/example/table-sort.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "tabs-demo": { - name: "tabs-demo", - type: "components:example", - registryDependencies: ["tabs"], - component: React.lazy(() => import("@/registry/default/example/tabs-demo")), - source: "", - files: ["registry/default/example/tabs-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "textarea-demo": { - name: "textarea-demo", - type: "components:example", - registryDependencies: ["textarea"], - component: React.lazy(() => import("@/registry/default/example/textarea-demo")), - source: "", - files: ["registry/default/example/textarea-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "textarea-disabled": { - name: "textarea-disabled", - type: "components:example", - registryDependencies: ["textarea"], - component: React.lazy(() => import("@/registry/default/example/textarea-disabled")), - source: "", - files: ["registry/default/example/textarea-disabled.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "textarea-form": { - name: "textarea-form", - type: "components:example", - registryDependencies: ["textarea","form"], - component: React.lazy(() => import("@/registry/default/example/textarea-form")), - source: "", - files: ["registry/default/example/textarea-form.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "textarea-with-button": { - name: "textarea-with-button", - type: "components:example", - registryDependencies: ["textarea","button"], - component: React.lazy(() => import("@/registry/default/example/textarea-with-button")), - source: "", - files: ["registry/default/example/textarea-with-button.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "textarea-with-label": { - name: "textarea-with-label", - type: "components:example", - registryDependencies: ["textarea","label"], - component: React.lazy(() => import("@/registry/default/example/textarea-with-label")), - source: "", - files: ["registry/default/example/textarea-with-label.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "textarea-with-text": { - name: "textarea-with-text", - type: "components:example", - registryDependencies: ["textarea","label"], - component: React.lazy(() => import("@/registry/default/example/textarea-with-text")), - source: "", - files: ["registry/default/example/textarea-with-text.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-group-demo": { - name: "toggle-group-demo", - type: "components:example", - registryDependencies: ["toggle-group"], - component: React.lazy(() => import("@/registry/default/example/toggle-group-demo")), - source: "", - files: ["registry/default/example/toggle-group-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-group-disabled": { - name: "toggle-group-disabled", - type: "components:example", - registryDependencies: ["toggle-group"], - component: React.lazy(() => import("@/registry/default/example/toggle-group-disabled")), - source: "", - files: ["registry/default/example/toggle-group-disabled.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-group-lg": { - name: "toggle-group-lg", - type: "components:example", - registryDependencies: ["toggle-group"], - component: React.lazy(() => import("@/registry/default/example/toggle-group-lg")), - source: "", - files: ["registry/default/example/toggle-group-lg.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-group-outline": { - name: "toggle-group-outline", - type: "components:example", - registryDependencies: ["toggle-group"], - component: React.lazy(() => import("@/registry/default/example/toggle-group-outline")), - source: "", - files: ["registry/default/example/toggle-group-outline.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-group-sm": { - name: "toggle-group-sm", - type: "components:example", - registryDependencies: ["toggle-group"], - component: React.lazy(() => import("@/registry/default/example/toggle-group-sm")), - source: "", - files: ["registry/default/example/toggle-group-sm.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-group-single": { - name: "toggle-group-single", - type: "components:example", - registryDependencies: ["toggle-group"], - component: React.lazy(() => import("@/registry/default/example/toggle-group-single")), - source: "", - files: ["registry/default/example/toggle-group-single.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-demo": { - name: "toggle-demo", - type: "components:example", - registryDependencies: ["toggle"], - component: React.lazy(() => import("@/registry/default/example/toggle-demo")), - source: "", - files: ["registry/default/example/toggle-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-disabled": { - name: "toggle-disabled", - type: "components:example", - registryDependencies: ["toggle"], - component: React.lazy(() => import("@/registry/default/example/toggle-disabled")), - source: "", - files: ["registry/default/example/toggle-disabled.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-lg": { - name: "toggle-lg", - type: "components:example", - registryDependencies: ["toggle"], - component: React.lazy(() => import("@/registry/default/example/toggle-lg")), - source: "", - files: ["registry/default/example/toggle-lg.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-outline": { - name: "toggle-outline", - type: "components:example", - registryDependencies: ["toggle"], - component: React.lazy(() => import("@/registry/default/example/toggle-outline")), - source: "", - files: ["registry/default/example/toggle-outline.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-sm": { - name: "toggle-sm", - type: "components:example", - registryDependencies: ["toggle"], - component: React.lazy(() => import("@/registry/default/example/toggle-sm")), - source: "", - files: ["registry/default/example/toggle-sm.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toggle-with-text": { - name: "toggle-with-text", - type: "components:example", - registryDependencies: ["toggle"], - component: React.lazy(() => import("@/registry/default/example/toggle-with-text")), - source: "", - files: ["registry/default/example/toggle-with-text.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "tooltip-demo": { - name: "tooltip-demo", - type: "components:example", - registryDependencies: ["tooltip"], - component: React.lazy(() => import("@/registry/default/example/tooltip-demo")), - source: "", - files: ["registry/default/example/tooltip-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-blockquote": { - name: "typography-blockquote", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-blockquote")), - source: "", - files: ["registry/default/example/typography-blockquote.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-demo": { - name: "typography-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-demo")), - source: "", - files: ["registry/default/example/typography-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-h1": { - name: "typography-h1", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-h1")), - source: "", - files: ["registry/default/example/typography-h1.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-h2": { - name: "typography-h2", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-h2")), - source: "", - files: ["registry/default/example/typography-h2.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-h3": { - name: "typography-h3", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-h3")), - source: "", - files: ["registry/default/example/typography-h3.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-h4": { - name: "typography-h4", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-h4")), - source: "", - files: ["registry/default/example/typography-h4.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-inline-code": { - name: "typography-inline-code", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-inline-code")), - source: "", - files: ["registry/default/example/typography-inline-code.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-large": { - name: "typography-large", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-large")), - source: "", - files: ["registry/default/example/typography-large.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-lead": { - name: "typography-lead", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-lead")), - source: "", - files: ["registry/default/example/typography-lead.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-list": { - name: "typography-list", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-list")), - source: "", - files: ["registry/default/example/typography-list.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-muted": { - name: "typography-muted", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-muted")), - source: "", - files: ["registry/default/example/typography-muted.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-p": { - name: "typography-p", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-p")), - source: "", - files: ["registry/default/example/typography-p.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-small": { - name: "typography-small", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-small")), - source: "", - files: ["registry/default/example/typography-small.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "typography-table": { - name: "typography-table", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/typography-table")), - source: "", - files: ["registry/default/example/typography-table.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "mode-toggle": { - name: "mode-toggle", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/mode-toggle")), - source: "", - files: ["registry/default/example/mode-toggle.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "text-confirm-dialog-demo": { - name: "text-confirm-dialog-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/text-confirm-dialog-demo")), - source: "", - files: ["registry/default/example/text-confirm-dialog-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "text-confirm-dialog-with-size": { - name: "text-confirm-dialog-with-size", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/text-confirm-dialog-with-size")), - source: "", - files: ["registry/default/example/text-confirm-dialog-with-size.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "text-confirm-dialog-with-children": { - name: "text-confirm-dialog-with-children", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/text-confirm-dialog-with-children")), - source: "", - files: ["registry/default/example/text-confirm-dialog-with-children.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "text-confirm-dialog-with-cancel-button": { - name: "text-confirm-dialog-with-cancel-button", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/text-confirm-dialog-with-cancel-button")), - source: "", - files: ["registry/default/example/text-confirm-dialog-with-cancel-button.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "form-item-layout-demo": { - name: "form-item-layout-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/form-item-layout-demo")), - source: "", - files: ["registry/default/example/form-item-layout-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "key-value-field-array-demo": { - name: "key-value-field-array-demo", - type: "components:example", - registryDependencies: ["button","form","input"], - component: React.lazy(() => import("@/registry/default/example/key-value-field-array-demo")), - source: "", - files: ["registry/default/example/key-value-field-array-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-choice-card": { - name: "field-choice-card", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-choice-card")), - source: "", - files: ["registry/default/example/field-choice-card.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-checkbox": { - name: "field-checkbox", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-checkbox")), - source: "", - files: ["registry/default/example/field-checkbox.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-demo": { - name: "field-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-demo")), - source: "", - files: ["registry/default/example/field-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-fieldset": { - name: "field-fieldset", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-fieldset")), - source: "", - files: ["registry/default/example/field-fieldset.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-input": { - name: "field-input", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-input")), - source: "", - files: ["registry/default/example/field-input.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-group": { - name: "field-group", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-group")), - source: "", - files: ["registry/default/example/field-group.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-radio": { - name: "field-radio", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-radio")), - source: "", - files: ["registry/default/example/field-radio.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-responsive": { - name: "field-responsive", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-responsive")), - source: "", - files: ["registry/default/example/field-responsive.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-select": { - name: "field-select", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-select")), - source: "", - files: ["registry/default/example/field-select.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-slider": { - name: "field-slider", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-slider")), - source: "", - files: ["registry/default/example/field-slider.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-switch": { - name: "field-switch", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-switch")), - source: "", - files: ["registry/default/example/field-switch.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "field-textarea": { - name: "field-textarea", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/field-textarea")), - source: "", - files: ["registry/default/example/field-textarea.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "form-patterns-pagelayout": { - name: "form-patterns-pagelayout", - type: "components:example", - registryDependencies: ["form","card","input","button","select","switch","checkbox","textarea","radio-group","calendar","popover","multi-select"], - component: React.lazy(() => import("@/registry/default/example/form-patterns-pagelayout")), - source: "", - files: ["registry/default/example/form-patterns-pagelayout.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "form-patterns-sidepanel": { - name: "form-patterns-sidepanel", - type: "components:example", - registryDependencies: ["form","sheet","input","button","select","switch","checkbox","textarea","radio-group","calendar","popover","multi-select"], - component: React.lazy(() => import("@/registry/default/example/form-patterns-sidepanel")), - source: "", - files: ["registry/default/example/form-patterns-sidepanel.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "form-item-layout-with-select": { - name: "form-item-layout-with-select", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/form-item-layout-with-select")), - source: "", - files: ["registry/default/example/form-item-layout-with-select.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "form-item-layout-with-horizontal": { - name: "form-item-layout-with-horizontal", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/form-item-layout-with-horizontal")), - source: "", - files: ["registry/default/example/form-item-layout-with-horizontal.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "form-item-layout-with-switch": { - name: "form-item-layout-with-switch", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/form-item-layout-with-switch")), - source: "", - files: ["registry/default/example/form-item-layout-with-switch.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "form-item-layout-with-checkbox": { - name: "form-item-layout-with-checkbox", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/form-item-layout-with-checkbox")), - source: "", - files: ["registry/default/example/form-item-layout-with-switch.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "form-item-layout-with-checkbox-list": { - name: "form-item-layout-with-checkbox-list", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/form-item-layout-with-checkbox-list")), - source: "", - files: ["registry/default/example/form-item-layout-with-switch.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "form-item-layout-before-label": { - name: "form-item-layout-before-label", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/form-item-layout-before-label")), - source: "", - files: ["registry/default/example/form-item-layout-before-label.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "form-item-layout-after-label": { - name: "form-item-layout-after-label", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/form-item-layout-after-label")), - source: "", - files: ["registry/default/example/form-item-layout-after-label.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "info-tooltip-demo": { - name: "info-tooltip-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/info-tooltip-demo")), - source: "", - files: ["registry/default/example/info-tooltip-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "skip-to-content-demo": { - name: "skip-to-content-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/skip-to-content-demo")), - source: "", - files: ["registry/default/example/skip-to-content-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-container-demo": { - name: "page-container-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-container-demo")), - source: "", - files: ["registry/default/example/page-container-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-breadcrumbs-demo": { - name: "page-breadcrumbs-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-breadcrumbs-demo")), - source: "", - files: ["registry/default/example/page-breadcrumbs-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-nav-demo": { - name: "page-nav-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-nav-demo")), - source: "", - files: ["registry/default/example/page-nav-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-layout-detail": { - name: "page-layout-detail", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-layout-detail")), - source: "", - files: ["registry/default/example/page-layout-detail.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-layout-edge-function": { - name: "page-layout-edge-function", - type: "components:example", - registryDependencies: ["filter-bar"], - component: React.lazy(() => import("@/registry/default/example/page-layout-edge-function")), - source: "", - files: ["registry/default/example/page-layout-edge-function.tsx","registry/default/example/page-layout-logs-content.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-layout-full-width": { - name: "page-layout-full-width", - type: "components:example", - registryDependencies: ["filter-bar"], - component: React.lazy(() => import("@/registry/default/example/page-layout-full-width")), - source: "", - files: ["registry/default/example/page-layout-full-width.tsx","registry/default/example/page-layout-logs-content.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-layout-list": { - name: "page-layout-list", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-layout-list")), - source: "", - files: ["registry/default/example/page-layout-list.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-layout-list-simple": { - name: "page-layout-list-simple", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-layout-list-simple")), - source: "", - files: ["registry/default/example/page-layout-list-simple.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-layout-settings": { - name: "page-layout-settings", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-layout-settings")), - source: "", - files: ["registry/default/example/page-layout-settings.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "connect-interstitial-demo": { - name: "connect-interstitial-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/connect-interstitial-demo")), - source: "", - files: ["registry/default/example/connect-interstitial-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "connect-interstitial-action-error": { - name: "connect-interstitial-action-error", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/connect-interstitial-action-error")), - source: "", - files: ["registry/default/example/connect-interstitial-action-error.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "connect-interstitial-logo-pair": { - name: "connect-interstitial-logo-pair", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-pair")), - source: "", - files: ["registry/default/example/connect-interstitial-logo-pair.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "connect-interstitial-logo-single": { - name: "connect-interstitial-logo-single", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-single")), - source: "", - files: ["registry/default/example/connect-interstitial-logo-single.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "connect-interstitial-logo-unknown": { - name: "connect-interstitial-logo-unknown", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-unknown")), - source: "", - files: ["registry/default/example/connect-interstitial-logo-unknown.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "connect-interstitial-logo-uploaded": { - name: "connect-interstitial-logo-uploaded", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/connect-interstitial-logo-uploaded")), - source: "", - files: ["registry/default/example/connect-interstitial-logo-uploaded.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-layout-auth-emails": { - name: "page-layout-auth-emails", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-layout-auth-emails")), - source: "", - files: ["registry/default/example/page-layout-auth-emails.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-header-demo": { - name: "page-header-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-header-demo")), - source: "", - files: ["registry/default/example/page-header-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-section-demo": { - name: "page-section-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-section-demo")), - source: "", - files: ["registry/default/example/page-section-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-section-horizontal": { - name: "page-section-horizontal", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-section-horizontal")), - source: "", - files: ["registry/default/example/page-section-horizontal.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-section-title-only": { - name: "page-section-title-only", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-section-title-only")), - source: "", - files: ["registry/default/example/page-section-title-only.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "page-section-with-aside": { - name: "page-section-with-aside", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/page-section-with-aside")), - source: "", - files: ["registry/default/example/page-section-with-aside.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "inner-side-menu-demo": { - name: "inner-side-menu-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/inner-side-menu-demo")), - source: "", - files: ["registry/default/example/inner-side-menu-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "inner-side-menu-static-titles": { - name: "inner-side-menu-static-titles", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/inner-side-menu-static-titles")), - source: "", - files: ["registry/default/example/inner-side-menu-static-titles.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "inner-side-menu-loading": { - name: "inner-side-menu-loading", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/inner-side-menu-loading")), - source: "", - files: ["registry/default/example/inner-side-menu-loading.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "inner-side-menu-multiple-sections": { - name: "inner-side-menu-multiple-sections", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/inner-side-menu-multiple-sections")), - source: "", - files: ["registry/default/example/inner-side-menu-multiple-sections.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "inner-side-menu-empty": { - name: "inner-side-menu-empty", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/inner-side-menu-empty")), - source: "", - files: ["registry/default/example/inner-side-menu-empty.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "inner-side-menu-with-search": { - name: "inner-side-menu-with-search", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/inner-side-menu-with-search")), - source: "", - files: ["registry/default/example/inner-side-menu-with-search.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toc-demo": { - name: "toc-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/toc-demo")), - source: "", - files: ["registry/default/example/toc-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "toc-single-demo": { - name: "toc-single-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/toc-single-demo")), - source: "", - files: ["registry/default/example/toc-single-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "multi-select-demo": { - name: "multi-select-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/multi-select-demo")), - source: "", - files: ["registry/default/example/multi-select-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "multi-select-disabled": { - name: "multi-select-disabled", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/multi-select-disabled")), - source: "", - files: ["registry/default/example/multi-select-disabled.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "multi-select-badge-limit-wrap": { - name: "multi-select-badge-limit-wrap", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/multi-select-badge-limit-wrap")), - source: "", - files: ["registry/default/example/multi-select-badge-limit-wrap.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "multi-select-badge-limit": { - name: "multi-select-badge-limit", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/multi-select-badge-limit")), - source: "", - files: ["registry/default/example/multi-select-badge-limit.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "multi-select-combobox": { - name: "multi-select-combobox", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/multi-select-combobox")), - source: "", - files: ["registry/default/example/multi-select-combobox.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "multi-select-combobox-creatable": { - name: "multi-select-combobox-creatable", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/multi-select-combobox-creatable")), - source: "", - files: ["registry/default/example/multi-select-combobox-creatable.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "multi-select-deletable-badge": { - name: "multi-select-deletable-badge", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/multi-select-deletable-badge")), - source: "", - files: ["registry/default/example/multi-select-deletable-badge.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "multi-select-inline-search-input": { - name: "multi-select-inline-search-input", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/multi-select-inline-search-input")), - source: "", - files: ["registry/default/example/multi-select-inline-search-input.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "multi-select-in-dialog": { - name: "multi-select-in-dialog", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/multi-select-in-dialog")), - source: "", - files: ["registry/default/example/multi-select-in-dialog.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "tree-view-demo": { - name: "tree-view-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/tree-view-demo")), - source: "", - files: ["registry/default/example/tree-view-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "tree-view-edit": { - name: "tree-view-edit", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/tree-view-edit")), - source: "", - files: ["registry/default/example/tree-view-edit.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "tree-view-directories": { - name: "tree-view-directories", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/tree-view-directories")), - source: "", - files: ["registry/default/example/tree-view-directories.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "tree-view-multi-select": { - name: "tree-view-multi-select", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/tree-view-multi-select")), - source: "", - files: ["registry/default/example/tree-view-multi-select.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "confirmation-modal-demo": { - name: "confirmation-modal-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/confirmation-modal-demo")), - source: "", - files: ["registry/default/example/confirmation-modal-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "assistant-chat-demo": { - name: "assistant-chat-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/assistant-chat-demo")), - source: "", - files: ["registry/default/example/assistant-chat-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "assistant-chat-commands": { - name: "assistant-chat-commands", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/assistant-chat-commands")), - source: "", - files: ["registry/default/example/assistant-chat-commands.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "color-usage-surface-www-and-docs": { - name: "color-usage-surface-www-and-docs", - type: "docs:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/color-usage-surface-www-and-docs")), - source: "", - files: ["registry/default/example/color-usage-surface-www-and-docs.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "color-usage-surface-studio": { - name: "color-usage-surface-studio", - type: "docs:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/color-usage-surface-studio")), - source: "", - files: ["registry/default/example/color-usage-surface-studio.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "color-usage-surface-studio-frame": { - name: "color-usage-surface-studio-frame", - type: "docs:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/color-usage-surface-studio-frame")), - source: "", - files: ["registry/default/example/color-usage-surface-studio-frame.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "chart-bar-demo": { - name: "chart-bar-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/chart-bar-demo")), - source: "", - files: ["registry/default/example/chart-bar-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "chart-bar-demo-grid": { - name: "chart-bar-demo-grid", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/chart-bar-demo-grid")), - source: "", - files: ["registry/default/example/chart-bar-demo-grid.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "chart-bar-demo-axis": { - name: "chart-bar-demo-axis", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/chart-bar-demo-axis")), - source: "", - files: ["registry/default/example/chart-bar-demo-axis.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "chart-bar-demo-tooltip": { - name: "chart-bar-demo-tooltip", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/chart-bar-demo-tooltip")), - source: "", - files: ["registry/default/example/chart-bar-demo-tooltip.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "chart-bar-demo-legend": { - name: "chart-bar-demo-legend", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/chart-bar-demo-legend")), - source: "", - files: ["registry/default/example/chart-bar-demo-legend.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "chart-tooltip-demo": { - name: "chart-tooltip-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/chart-tooltip-demo")), - source: "", - files: ["registry/default/example/chart-tooltip-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "expanding-textarea-demo": { - name: "expanding-textarea-demo", - type: "docs:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/expanding-textarea-demo")), - source: "", - files: ["registry/default/example/expanding-textarea-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "error-display-demo": { - name: "error-display-demo", - type: "components:example", - registryDependencies: ["error-display"], - component: React.lazy(() => import("@/registry/default/example/error-display-demo")), - source: "", - files: ["registry/default/example/error-display-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "error-display-with-children": { - name: "error-display-with-children", - type: "components:example", - registryDependencies: ["error-display"], - component: React.lazy(() => import("@/registry/default/example/error-display-with-children")), - source: "", - files: ["registry/default/example/error-display-with-children.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "logs-bar-chart": { - name: "logs-bar-chart", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/logs-bar-chart")), - source: "", - files: ["registry/default/example/logs-bar-chart.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "empty-state-missing-route": { - name: "empty-state-missing-route", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/empty-state-missing-route")), - source: "", - files: ["registry/default/example/empty-state-missing-route.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "empty-state-zero-items-table": { - name: "empty-state-zero-items-table", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/empty-state-zero-items-table")), - source: "", - files: ["registry/default/example/empty-state-zero-items-table.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "empty-state-presentational-demo": { - name: "empty-state-presentational-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/empty-state-presentational-demo")), - source: "", - files: ["registry/default/example/empty-state-presentational-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "empty-state-presentational-buttons": { - name: "empty-state-presentational-buttons", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/empty-state-presentational-buttons")), - source: "", - files: ["registry/default/example/empty-state-presentational-buttons.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "empty-state-presentational-icon": { - name: "empty-state-presentational-icon", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/empty-state-presentational-icon")), - source: "", - files: ["registry/default/example/empty-state-presentational-icon.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "empty-state-initial-state-informational": { - name: "empty-state-initial-state-informational", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/empty-state-initial-state-informational")), - source: "", - files: ["registry/default/example/empty-state-initial-state-informational.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "data-grid-demo": { - name: "data-grid-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/data-grid-demo")), - source: "", - files: ["registry/default/example/data-grid-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "data-grid-empty-state": { - name: "data-grid-empty-state", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/data-grid-empty-state")), - source: "", - files: ["registry/default/example/data-grid-empty-state.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "data-table-demo": { - name: "data-table-demo", - type: "components:example", - registryDependencies: ["table"], - component: React.lazy(() => import("@/registry/default/example/data-table-demo")), - source: "", - files: ["registry/default/example/data-table-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "metric-card": { - name: "metric-card", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/metric-card")), - source: "", - files: ["registry/default/example/metric-card.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "metric-card-minimal": { - name: "metric-card-minimal", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/metric-card-minimal")), - source: "", - files: ["registry/default/example/metric-card-minimal.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "metric-card-minimal-horizontal": { - name: "metric-card-minimal-horizontal", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/metric-card-minimal-horizontal")), - source: "", - files: ["registry/default/example/metric-card-minimal-horizontal.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "metric-card-with-icon-link-tooltip": { - name: "metric-card-with-icon-link-tooltip", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/metric-card-with-icon-link-tooltip")), - source: "", - files: ["registry/default/example/metric-card-with-icon-link-tooltip.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "mermaid-demo": { - name: "mermaid-demo", - type: "components:example", - registryDependencies: ["mermaid"], - component: React.lazy(() => import("@/registry/default/example/mermaid-demo")), - source: "", - files: ["registry/default/example/mermaid-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "mermaid-flowchart": { - name: "mermaid-flowchart", - type: "components:example", - registryDependencies: ["mermaid"], - component: React.lazy(() => import("@/registry/default/example/mermaid-flowchart")), - source: "", - files: ["registry/default/example/mermaid-flowchart.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "mermaid-er-diagram": { - name: "mermaid-er-diagram", - type: "components:example", - registryDependencies: ["mermaid"], - component: React.lazy(() => import("@/registry/default/example/mermaid-er-diagram")), - source: "", - files: ["registry/default/example/mermaid-er-diagram.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "mermaid-sequence-async": { - name: "mermaid-sequence-async", - type: "components:example", - registryDependencies: ["mermaid"], - component: React.lazy(() => import("@/registry/default/example/mermaid-sequence-async")), - source: "", - files: ["registry/default/example/mermaid-sequence-async.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "mermaid-sequence-sync": { - name: "mermaid-sequence-sync", - type: "components:example", - registryDependencies: ["mermaid"], - component: React.lazy(() => import("@/registry/default/example/mermaid-sequence-sync")), - source: "", - files: ["registry/default/example/mermaid-sequence-sync.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "mermaid-oauth-flow": { - name: "mermaid-oauth-flow", - type: "components:example", - registryDependencies: ["mermaid"], - component: React.lazy(() => import("@/registry/default/example/mermaid-oauth-flow")), - source: "", - files: ["registry/default/example/mermaid-oauth-flow.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "mermaid-flowchart-subgraph": { - name: "mermaid-flowchart-subgraph", - type: "components:example", - registryDependencies: ["mermaid"], - component: React.lazy(() => import("@/registry/default/example/mermaid-flowchart-subgraph")), - source: "", - files: ["registry/default/example/mermaid-flowchart-subgraph.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "mermaid-sequence-api": { - name: "mermaid-sequence-api", - type: "components:example", - registryDependencies: ["mermaid"], - component: React.lazy(() => import("@/registry/default/example/mermaid-sequence-api")), - source: "", - files: ["registry/default/example/mermaid-sequence-api.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "mermaid-er-simple": { - name: "mermaid-er-simple", - type: "components:example", - registryDependencies: ["mermaid"], - component: React.lazy(() => import("@/registry/default/example/mermaid-er-simple")), - source: "", - files: ["registry/default/example/mermaid-er-simple.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "mermaid-basic": { - name: "mermaid-basic", - type: "components:example", - registryDependencies: ["mermaid"], - component: React.lazy(() => import("@/registry/default/example/mermaid-basic")), - source: "", - files: ["registry/default/example/mermaid-basic.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-full-example": { - name: "markdown-full-example", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-full-example")), - source: "", - files: ["registry/default/example/markdown-full-example.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-customization": { - name: "markdown-customization", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-customization")), - source: "", - files: ["registry/default/example/markdown-customization.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-headings": { - name: "markdown-headings", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-headings")), - source: "", - files: ["registry/default/example/markdown-headings.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-paragraphs": { - name: "markdown-paragraphs", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-paragraphs")), - source: "", - files: ["registry/default/example/markdown-paragraphs.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-lists": { - name: "markdown-lists", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-lists")), - source: "", - files: ["registry/default/example/markdown-lists.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-links": { - name: "markdown-links", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-links")), - source: "", - files: ["registry/default/example/markdown-links.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-inline-code": { - name: "markdown-inline-code", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-inline-code")), - source: "", - files: ["registry/default/example/markdown-inline-code.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-blockquotes": { - name: "markdown-blockquotes", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-blockquotes")), - source: "", - files: ["registry/default/example/markdown-blockquotes.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-code-blocks": { - name: "markdown-code-blocks", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-code-blocks")), - source: "", - files: ["registry/default/example/markdown-code-blocks.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-tables": { - name: "markdown-tables", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-tables")), - source: "", - files: ["registry/default/example/markdown-tables.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-images": { - name: "markdown-images", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-images")), - source: "", - files: ["registry/default/example/markdown-images.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-horizontal-rules": { - name: "markdown-horizontal-rules", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-horizontal-rules")), - source: "", - files: ["registry/default/example/markdown-horizontal-rules.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-quote-component": { - name: "markdown-quote-component", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-quote-component")), - source: "", - files: ["registry/default/example/markdown-quote-component.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "markdown-avatar-component": { - name: "markdown-avatar-component", - type: "components:example", - registryDependencies: ["markdown"], - component: React.lazy(() => import("@/registry/default/example/markdown-avatar-component")), - source: "", - files: ["registry/default/example/markdown-avatar-component.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "status-code-demo": { - name: "status-code-demo", - type: "components:example", - registryDependencies: undefined, - component: React.lazy(() => import("@/registry/default/example/status-code-demo")), - source: "", - files: ["registry/default/example/status-code-demo.tsx"], - category: "undefined", - subcategory: "undefined", - chunks: [] - }, - "chart-bar-interactive": { - name: "chart-bar-interactive", - type: "components:block", - registryDependencies: ["card","chart"], - component: React.lazy(() => import("@/registry/default/block/chart-bar-interactive")), - source: "__registry__/default/block/chart-bar-interactive.tsx", - files: ["registry/default/block/chart-bar-interactive.tsx"], - category: "Charts", - subcategory: "Bar", - chunks: [] - }, - "chart-composed-demo": { - name: "chart-composed-demo", - type: "components:block", - registryDependencies: ["chart"], - component: React.lazy(() => import("@/registry/default/block/chart-composed-demo")), - source: "__registry__/default/block/chart-composed-demo.tsx", - files: ["registry/default/block/chart-composed-demo.tsx"], - category: "Charts", - subcategory: "Composed", - chunks: [] - }, - "chart-composed-basic": { - name: "chart-composed-basic", - type: "components:block", - registryDependencies: ["chart"], - component: React.lazy(() => import("@/registry/default/block/chart-composed-basic")), - source: "__registry__/default/block/chart-composed-basic.tsx", - files: ["registry/default/block/chart-composed-basic.tsx"], - category: "Charts", - subcategory: "Composed", - chunks: [] - }, - "chart-composed-states": { - name: "chart-composed-states", - type: "components:block", - registryDependencies: ["chart"], - component: React.lazy(() => import("@/registry/default/block/chart-composed-states")), - source: "__registry__/default/block/chart-composed-states.tsx", - files: ["registry/default/block/chart-composed-states.tsx"], - category: "Charts", - subcategory: "Composed", - chunks: [] - }, - "chart-composed-table": { - name: "chart-composed-table", - type: "components:block", - registryDependencies: ["chart"], - component: React.lazy(() => import("@/registry/default/block/chart-composed-table")), - source: "__registry__/default/block/chart-composed-table.tsx", - files: ["registry/default/block/chart-composed-table.tsx"], - category: "Charts", - subcategory: "Composed", - chunks: [] - }, - "chart-composed-metrics": { - name: "chart-composed-metrics", - type: "components:block", - registryDependencies: ["chart"], - component: React.lazy(() => import("@/registry/default/block/chart-composed-metrics")), - source: "__registry__/default/block/chart-composed-metrics.tsx", - files: ["registry/default/block/chart-composed-metrics.tsx"], - category: "Charts", - subcategory: "Composed", - chunks: [] - }, - "chart-composed-actions": { - name: "chart-composed-actions", - type: "components:block", - registryDependencies: ["chart"], - component: React.lazy(() => import("@/registry/default/block/chart-composed-actions")), - source: "__registry__/default/block/chart-composed-actions.tsx", - files: ["registry/default/block/chart-composed-actions.tsx"], - category: "Charts", - subcategory: "Composed", - chunks: [] - }, - "copy-button-verbs": { - name: "copy-button-verbs", - type: "components:example", - registryDependencies: ["button"], - component: React.lazy(() => import("@/registry/default/example/copy-button-verbs")), - source: "", - files: ["registry/default/example/copy-button-verbs.tsx"], - category: "Getting Started", - subcategory: "Copywriting", - chunks: [] - }, - "copy-form-labels": { - name: "copy-form-labels", - type: "components:example", - registryDependencies: ["form"], - component: React.lazy(() => import("@/registry/default/example/copy-form-labels")), - source: "", - files: ["registry/default/example/copy-form-labels.tsx"], - category: "Getting Started", - subcategory: "Copywriting", - chunks: [] - }, - "copy-error-messages": { - name: "copy-error-messages", - type: "components:example", - registryDependencies: ["form"], - component: React.lazy(() => import("@/registry/default/example/copy-error-messages")), - source: "", - files: ["registry/default/example/copy-error-messages.tsx"], - category: "Getting Started", - subcategory: "Copywriting", - chunks: [] - }, - "copy-success-messages": { - name: "copy-success-messages", - type: "components:example", - registryDependencies: ["form"], - component: React.lazy(() => import("@/registry/default/example/copy-success-messages")), - source: "", - files: ["registry/default/example/copy-success-messages.tsx"], - category: "Getting Started", - subcategory: "Copywriting", - chunks: [] - }, - "copy-tooltips": { - name: "copy-tooltips", - type: "components:example", - registryDependencies: ["tooltip"], - component: React.lazy(() => import("@/registry/default/example/copy-tooltips")), - source: "", - files: ["registry/default/example/copy-tooltips.tsx"], - category: "Getting Started", - subcategory: "Copywriting", - chunks: [] - }, - "copy-loading-states": { - name: "copy-loading-states", - type: "components:example", - registryDependencies: ["loading-state"], - component: React.lazy(() => import("@/registry/default/example/copy-loading-states")), - source: "", - files: ["registry/default/example/copy-loading-states.tsx"], - category: "Getting Started", - subcategory: "Copywriting", - chunks: [] - }, - "copy-empty-states": { - name: "copy-empty-states", - type: "components:example", - registryDependencies: ["empty-state"], - component: React.lazy(() => import("@/registry/default/example/copy-empty-states")), - source: "", - files: ["registry/default/example/copy-empty-states.tsx"], - category: "Getting Started", - subcategory: "Copywriting", - chunks: [] - }, - "copy-confirmations": { - name: "copy-confirmations", - type: "components:example", - registryDependencies: ["confirmation"], - component: React.lazy(() => import("@/registry/default/example/copy-confirmations")), - source: "", - files: ["registry/default/example/copy-confirmations.tsx"], - category: "Getting Started", - subcategory: "Copywriting", - chunks: [] - }, - }, -} diff --git a/apps/design-system/eslint.config.cjs b/apps/design-system/eslint.config.cjs index 4f1bc5e227550..75f8057119c0b 100644 --- a/apps/design-system/eslint.config.cjs +++ b/apps/design-system/eslint.config.cjs @@ -3,8 +3,9 @@ const supabaseConfig = require('eslint-config-supabase/next') module.exports = defineConfig([ supabaseConfig, + { ignores: ['__registry__'] }, { - files: ['registry/**/*.tsx', '__registry__/**/*.tsx', 'app/**/*.tsx'], + files: ['registry/**/*.tsx', 'app/**/*.tsx'], rules: { 'no-restricted-exports': 'off', }, diff --git a/apps/design-system/package.json b/apps/design-system/package.json index 8b8da975e3653..2bc337203573b 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -5,16 +5,17 @@ "type": "module", "scripts": { "preinstall": "npx only-allow pnpm", - "dev": "run-p --race dev:*", + "dev": "pnpm run generate:registry && run-p --race dev:*", "dev:next": "next dev --turbopack --port 3003", "dev:content": "contentlayer2 dev", - "build": "pnpm run content:build && pnpm run build:registry && next build --turbopack", - "build:registry": "tsx ./scripts/build-registry.mts && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache", + "build": "pnpm run build:registry && pnpm run content:build && next build --turbopack", + "generate:registry": "tsx ./scripts/build-registry.mts", + "build:registry": "pnpm run generate:registry && prettier --log-level silent --write \"registry/**/*.{ts,tsx,mdx}\" --cache", "start": "next start", "lint": "eslint .", "content:build": "contentlayer2 build", - "clean": "rimraf .next .turbo tsconfig.tsbuildinfo", - "typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json" + "clean": "rimraf .next .turbo tsconfig.tsbuildinfo __registry__", + "typecheck": "pnpm run generate:registry && contentlayer2 build && tsc --noEmit -p tsconfig.json" }, "dependencies": { "@hookform/resolvers": "^3.1.1", diff --git a/apps/design-system/scripts/build-registry.mts b/apps/design-system/scripts/build-registry.mts index f645b05842bd9..a95df6b54027a 100644 --- a/apps/design-system/scripts/build-registry.mts +++ b/apps/design-system/scripts/build-registry.mts @@ -1,5 +1,5 @@ // @sts-nocheck -import { existsSync, promises as fs } from 'fs' +import { promises as fs } from 'fs' import { tmpdir } from 'os' import path from 'path' import { cwd } from 'process' @@ -191,13 +191,10 @@ export const Index: Record = { }) ) - // // Write the source file for blocks only. + // Write the source file for blocks only. sourceFilename = `__registry__/${style.name}/${type}/${item.name}.tsx` const sourcePath = path.join(process.cwd(), sourceFilename) - if (!existsSync(sourcePath)) { - await fs.mkdir(sourcePath, { recursive: true }) - } - + await fs.mkdir(path.dirname(sourcePath), { recursive: true }) rimraf.sync(sourcePath) await fs.writeFile(sourcePath, sourceFile.getText()) } @@ -273,8 +270,10 @@ export const Index: Record = { // await fs.writeFile(path.join(REGISTRY_PATH, 'index.json'), registryJson, 'utf8') // Write style index. - rimraf.sync(path.join(process.cwd(), '__registry__/index.tsx')) - await fs.writeFile(path.join(process.cwd(), '__registry__/index.tsx'), index) + const indexPath = path.join(process.cwd(), '__registry__/index.tsx') + await fs.mkdir(path.dirname(indexPath), { recursive: true }) + rimraf.sync(indexPath) + await fs.writeFile(indexPath, index) } // ---------------------------------------------------------------------------- diff --git a/apps/studio/.github/eslint-rule-baselines.json b/apps/studio/.github/eslint-rule-baselines.json index 20ebe9716740d..26a486ececdf2 100644 --- a/apps/studio/.github/eslint-rule-baselines.json +++ b/apps/studio/.github/eslint-rule-baselines.json @@ -72,8 +72,8 @@ "components/interfaces/Reports/ReportBlock/ChartBlock.tsx": 1, "components/interfaces/Reports/ReportFilterBar.tsx": 1, "components/interfaces/Reports/Reports.tsx": 1, - "components/interfaces/SQLEditor/AskAIWidget.tsx": 1, - "components/interfaces/SQLEditor/InlineWidget.tsx": 2, + "components/ui/AIEditor/AskAIWidget.tsx": 1, + "components/ui/AIEditor/InlineWidget.tsx": 2, "components/interfaces/SQLEditor/MoveQueryModal.tsx": 1, "components/interfaces/SQLEditor/OngoingQueriesPanel.tsx": 1, "components/interfaces/SQLEditor/useSnippetIdentity.ts": 1, @@ -754,7 +754,7 @@ "components/interfaces/Reports/ReportStickyNav.tsx": 1, "components/interfaces/Reports/ReportWidget.tsx": 1, "components/interfaces/Reports/Reports.tsx": 1, - "components/interfaces/SQLEditor/InlineWidget.tsx": 1, + "components/ui/AIEditor/InlineWidget.tsx": 1, "components/interfaces/SQLEditor/SQLTemplates/SQLExamples.tsx": 1, "components/interfaces/SQLEditor/SQLTemplates/SQLTemplates.tsx": 1, "components/interfaces/SQLEditor/UtilityPanel/ReadOnlyBadge.tsx": 1, diff --git a/apps/studio/components/interfaces/Explorer/ExplorerChatToolbar.tsx b/apps/studio/components/interfaces/Explorer/ExplorerChatToolbar.tsx index 0444012d883ca..14edcaf9f6dfc 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerChatToolbar.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerChatToolbar.tsx @@ -1,4 +1,4 @@ -import { Clipboard, MessageSquare, MoreVertical, Settings } from 'lucide-react' +import { Clipboard, MessageSquare, MoreVertical, Settings, Trash } from 'lucide-react' import { useState } from 'react' import { toast } from 'sonner' import { @@ -9,6 +9,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from 'ui' +import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { ExplorerToolbar, @@ -41,6 +42,7 @@ export const ExplorerChatToolbar = ({ const snap = useAiAssistantStateSnapshot() const chat = snap.chats[chatId] const [isOptInModalOpen, setIsOptInModalOpen] = useState(false) + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) const handleCopyChatId = () => { copyToClipboard(chatId, () => toast.success(`Copied chat ID for ${chat?.name}`)) @@ -50,6 +52,12 @@ export const ExplorerChatToolbar = ({ if (name.trim()) snap.renameChat(chatId, name.trim()) } + const handleDeleteChat = () => { + snap.deleteChat(chatId) + setIsDeleteModalOpen(false) + toast.success(`Deleted "${chat?.name}"`) + } + useShortcut(SHORTCUT_IDS.AI_ASSISTANT_COPY_CHAT_ID, handleCopyChatId, { enabled: shortcutsEnabled && !isChatLoading, }) @@ -83,7 +91,6 @@ export const ExplorerChatToolbar = ({ sequence={SHORTCUT_DEFINITIONS[SHORTCUT_IDS.AI_ASSISTANT_COPY_CHAT_ID].sequence} /> - setIsOptInModalOpen(true)} @@ -98,10 +105,16 @@ export const ExplorerChatToolbar = ({ } /> + + setIsDeleteModalOpen(true)}> + + Delete chat + + + + setIsDeleteModalOpen(false)} + onConfirm={handleDeleteChat} + > +

+ This will permanently delete this chat and its message history. This action cannot be + undone. +

+
) } diff --git a/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx b/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx index adc39e12076bd..b0fb890f53ac8 100644 --- a/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx +++ b/apps/studio/components/interfaces/Explorer/ExplorerNotebookTab.tsx @@ -41,7 +41,7 @@ export const ExplorerNotebookTab = () => { const currentNotebook = useCurrentNotebook() const { name, content } = currentNotebook?.notebook ?? {} const cells = content?.cells ?? [] - const queryCellIds = cells.filter(isQueryCell).map((cell) => cell.id) + const queryCellIds = cells.filter(isQueryCell).map((cell) => cell._id) const [isRunningNotebook, setIsRunningNotebook] = useState(false) const queryCellRefs = useRef(new Map()) @@ -82,7 +82,7 @@ export const ExplorerNotebookTab = () => { if (!notebookId) return const cell = type === 'markdown' ? createMarkdownCellSkeleton() : createQueryCellSkeleton() - const lastCellId = cells[cells.length - 1]?.id + const lastCellId = cells[cells.length - 1]?._id snap.insertCellAfter({ id: notebookId, cellId: lastCellId, cell }) } @@ -133,22 +133,22 @@ export const ExplorerNotebookTab = () => { <> cell.id)} + items={cells.map((cell) => cell._id)} strategy={verticalListSortingStrategy} >
{cells.map((cell) => isQueryCell(cell) ? ( { - if (instance) queryCellRefs.current.set(cell.id, instance) - else queryCellRefs.current.delete(cell.id) + if (instance) queryCellRefs.current.set(cell._id, instance) + else queryCellRefs.current.delete(cell._id) }} /> ) : ( - + ) )}
diff --git a/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx b/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx index 3cbf89238be8d..df8dc8860f813 100644 --- a/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx +++ b/apps/studio/components/interfaces/Explorer/MarkdownCell.tsx @@ -41,7 +41,7 @@ export const MarkdownCell = ({ cell }: MarkdownCellProps) => { const notebookId = currentNotebook?.notebook.id if (!notebookId) return - const nextCells = cells.map((c) => (c.id === cellId ? { ...c, text } : c)) + const nextCells = cells.map((c) => (c._id === cellId ? { ...c, text } : c)) snap.updateCells({ id: notebookId, cells: nextCells }) setIsEditing(false) } @@ -50,9 +50,9 @@ export const MarkdownCell = ({ cell }: MarkdownCellProps) => { return ( } - gripDropdownContent={} + id={cell._id} + actions={} + gripDropdownContent={} gripClassName="mt-1.5 opacity-0 group-hover:opacity-100 has-[[data-state=open]]:opacity-100 transition" > {isEditing ? ( @@ -84,10 +84,10 @@ export const MarkdownCell = ({ cell }: MarkdownCellProps) => { ].join(' && ') ) editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => - handleUpdateMarkdownRef.current(cell.id, valueRef.current) + handleUpdateMarkdownRef.current(cell._id, valueRef.current) ) editor.onDidBlurEditorWidget(() => - handleUpdateMarkdownRef.current(cell.id, valueRef.current) + handleUpdateMarkdownRef.current(cell._id, valueRef.current) ) }} /> @@ -100,7 +100,7 @@ export const MarkdownCell = ({ cell }: MarkdownCellProps) => { diff --git a/apps/studio/components/interfaces/Explorer/MoveCellDropdownContent.tsx b/apps/studio/components/interfaces/Explorer/MoveCellDropdownContent.tsx index 5fb0e392ba77c..9cc1632a111ae 100644 --- a/apps/studio/components/interfaces/Explorer/MoveCellDropdownContent.tsx +++ b/apps/studio/components/interfaces/Explorer/MoveCellDropdownContent.tsx @@ -12,7 +12,7 @@ export const MoveCellDropdownContent = ({ cellId }: MoveCellDropdownContentProps const currentNotebook = useCurrentNotebook() const cells = currentNotebook?.notebook.content?.cells ?? [] - const currentIndex = cells.findIndex((c) => c.id === cellId) + const currentIndex = cells.findIndex((c) => c._id === cellId) const isFirstCell = currentIndex <= 0 const isLastCell = currentIndex === -1 || currentIndex === cells.length - 1 diff --git a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.tsx b/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.tsx deleted file mode 100644 index 5d79e016bef5a..0000000000000 --- a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { NotebookPen } from 'lucide-react' -import { useState } from 'react' -import { Button, cn } from 'ui' - -import { - formatNotebookDiffSummary, - getEntryKey, - isEntryExpandedByDefault, - summarizeNotebookDiff, -} from './NotebookPreview.utils' -import { NotebookPreviewCell } from './NotebookPreviewCell' -import type { NotebookCellDiffEntry } from '@/data/content/notebooks/notebook-operations' - -export interface NotebookPreviewProps { - entries: NotebookCellDiffEntry[] - mode: 'create' | 'update' - /** The notebook's name, shown in the card header. Falls back to a generic label. */ - title?: string - className?: string -} - -const VISIBLE_ENTRY_LIMIT = 5 - -const FALLBACK_TITLE = { - create: 'New notebook', - update: 'Notebook changes', -} as const - -/** - * Read-only preview of a proposed notebook create/update, rendered from a pre-computed diff. - * Pure presentational component: no data fetching, no approval or notebook-editor state โ€” see - * `deriveNotebookDiff` for how `entries` is produced. - */ -export const NotebookPreview = ({ entries, mode, title, className }: NotebookPreviewProps) => { - const [isShowingAllEntries, setIsShowingAllEntries] = useState(false) - const [expandedOverrides, setExpandedOverrides] = useState>({}) - - const summary = summarizeNotebookDiff(entries, mode) - const visibleEntries = isShowingAllEntries ? entries : entries.slice(0, VISIBLE_ENTRY_LIMIT) - const hiddenCount = entries.length - visibleEntries.length - - const isExpanded = (entry: NotebookCellDiffEntry) => - expandedOverrides[getEntryKey(entry)] ?? isEntryExpandedByDefault(entry) - - const hasEntries = entries.length > 0 - const areAllExpanded = visibleEntries.every(isExpanded) - - const toggleAll = () => - setExpandedOverrides( - Object.fromEntries(entries.map((entry) => [getEntryKey(entry), !areAllExpanded])) - ) - - return ( -
-
- - - {title ?? FALLBACK_TITLE[mode]} - - - {formatNotebookDiffSummary(summary)} - - {hasEntries && ( - - )} -
-
- {visibleEntries.map((entry) => { - const key = getEntryKey(entry) - return ( - - setExpandedOverrides((prev) => ({ ...prev, [key]: open })) - } - /> - ) - })} -
- {hiddenCount > 0 && ( - - )} -
- ) -} diff --git a/apps/studio/components/interfaces/Explorer/QueryCell/QueryCell.utils.test.ts b/apps/studio/components/interfaces/Explorer/QueryCell/QueryCell.utils.test.ts index 1d11993ee232a..2480bdc709a69 100644 --- a/apps/studio/components/interfaces/Explorer/QueryCell/QueryCell.utils.test.ts +++ b/apps/studio/components/interfaces/Explorer/QueryCell/QueryCell.utils.test.ts @@ -16,7 +16,7 @@ import { untrustedLogSql } from '@/data/logs/safe-analytics-sql' const CHART: ChartConfig = { type: 'bar', x_column: 'day', - y_columns: ['signups'], + y_series: ['signups'], cumulative: false, scale: 'linear', show_labels: true, @@ -24,7 +24,7 @@ const CHART: ChartConfig = { const DATABASE_CELL: QueryCell = { _tag: 'database_cell', - id: 'cell-1', + _id: 'cell-1', title: 'Signups', view: 'chart', chart: CHART, @@ -35,7 +35,7 @@ const DATABASE_CELL: QueryCell = { const LOG_CELL: QueryCell = { _tag: 'log_cell', - id: 'cell-2', + _id: 'cell-2', title: 'Edge errors', view: 'table', chart: CHART, @@ -66,7 +66,7 @@ describe('changeCellSource', () => { expect(next).toEqual({ _tag: 'log_cell', - id: 'cell-1', + _id: 'cell-1', title: 'Signups', view: 'chart', chart: CHART, @@ -80,7 +80,7 @@ describe('changeCellSource', () => { expect(next).toEqual({ _tag: 'database_cell', - id: 'cell-2', + _id: 'cell-2', title: 'Edge errors', view: 'table', chart: CHART, @@ -138,7 +138,7 @@ describe('cloneQueryCell', () => { const clone = cloneQueryCell(DATABASE_CELL) expect(clone).toEqual(DATABASE_CELL) - expect(clone.chart?.y_columns).not.toBe(DATABASE_CELL.chart?.y_columns) + expect(clone.chart?.y_series).not.toBe(DATABASE_CELL.chart?.y_series) }) }) diff --git a/apps/studio/components/interfaces/Explorer/QueryCell/QueryCell.utils.ts b/apps/studio/components/interfaces/Explorer/QueryCell/QueryCell.utils.ts index ca6cb3edb57c4..5eabf93bd831d 100644 --- a/apps/studio/components/interfaces/Explorer/QueryCell/QueryCell.utils.ts +++ b/apps/studio/components/interfaces/Explorer/QueryCell/QueryCell.utils.ts @@ -17,13 +17,13 @@ export const DEFAULT_CELL_ROW_LIMIT = 100 * Valtio snapshots are deep-readonly. Readonly properties assign to mutable ones, so only * the array needs rebuilding to turn a snapshot's chart back into a writable config. */ -type ReadonlyChartConfig = Omit & { - readonly y_columns: readonly string[] +type ReadonlyChartConfig = Omit & { + readonly y_series: readonly string[] } export const cloneChartConfig = ( chart: ReadonlyChartConfig | undefined -): ChartConfig | undefined => (chart ? { ...chart, y_columns: [...chart.y_columns] } : undefined) +): ChartConfig | undefined => (chart ? { ...chart, y_series: [...chart.y_series] } : undefined) /** The display state a query cell hands the shared editor. */ // `view` is already defaulted to 'table' by the domain transform, so there is nothing to @@ -35,7 +35,7 @@ export const getCellDisplay = (cell: Snapshot): QueryDisplay => ({ /** Fields every query cell carries, copied out of a snapshot so the result is writable. */ const copyQueryCellBase = (cell: Snapshot) => ({ - id: cell.id, + _id: cell._id, title: cell.title, view: cell.view, chart: cloneChartConfig(cell.chart), diff --git a/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx index dec953401334f..5293f0f3ecc3b 100644 --- a/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx @@ -53,7 +53,7 @@ export const QueryCell = forwardRef(function snap.updateCell({ id: notebookId, - cellId: cell.id, + cellId: cell._id, updater: (candidate) => (isQueryCell(candidate) ? updater(candidate) : candidate), }) } @@ -89,14 +89,14 @@ export const QueryCell = forwardRef(function return ( } - gripDropdownContent={} + id={cell._id} + actions={} + gripDropdownContent={} gripClassName="mt-2 opacity-0 group-hover:opacity-100 has-[[data-state=open]]:opacity-100 transition" > void } -const getLogScaleDisabledReason = (y_columns: string[]) => { - if (y_columns.length === 0) return 'Select a column for the Y axis first' - if (y_columns.length > 1) return 'Only available with a single Y axis column' +const getLogScaleDisabledReason = (y_series: string[]) => { + if (y_series.length === 0) return 'Select a column for the Y axis first' + if (y_series.length > 1) return 'Only available with a single Y axis column' return 'Data contains zero or negative values' } @@ -56,22 +56,22 @@ export const DisplaySettingsButton = ({ const { type = 'bar', x_column, - y_columns = [], + y_series = [], cumulative = false, show_labels = false, scale = 'linear', } = chart ?? {} const hasNonPositiveValues = useMemo( - () => checkHasNonPositiveValues(result?.rows ?? [], y_columns[0]), - [result, y_columns] + () => checkHasNonPositiveValues(result?.rows ?? [], y_series[0]), + [result, y_series] ) // Logarithmic scale only applies to a single series. const canToggleLogScale = useMemo(() => { - if (y_columns.length !== 1 || !result || (result.rows ?? []).length === 0) return false + if (y_series.length !== 1 || !result || (result.rows ?? []).length === 0) return false return !hasNonPositiveValues - }, [hasNonPositiveValues, result, y_columns.length]) + }, [hasNonPositiveValues, result, y_series.length]) const onChangeView = (view: 'table' | 'chart') => { onChange({ ...display, view }) @@ -83,7 +83,7 @@ export const DisplaySettingsButton = ({ chart: { type: chart?.type ?? 'bar', x_column: chart?.x_column ?? '', - y_columns: chart?.y_columns ?? [], + y_series: chart?.y_series ?? [], cumulative: chart?.cumulative ?? false, scale: chart?.scale ?? 'linear', show_labels: chart?.show_labels ?? false, @@ -97,10 +97,10 @@ export const DisplaySettingsButton = ({ }) useEffect(() => { - if (scale === 'log' && (hasNonPositiveValues || y_columns.length > 1)) { + if (scale === 'log' && (hasNonPositiveValues || y_series.length > 1)) { resetToLinearScale() } - }, [hasNonPositiveValues, scale, y_columns.length]) + }, [hasNonPositiveValues, scale, y_series.length]) return ( @@ -187,16 +187,16 @@ export const DisplaySettingsButton = ({ className="[&>div:first-child]:xl:w-3/5" > { - if (values.length > MAX_CHART_Y_COLUMNS) return - onUpdateChartConfig({ y_columns: values }) + if (values.length > MAX_CHART_Y_SERIES) return + onUpdateChartConfig({ y_series: values }) }} className="w-full" > = MAX_CHART_Y_COLUMNS && !y_columns.includes(x) - } + disabled={y_series.length >= MAX_CHART_Y_SERIES && !y_series.includes(x)} > {x} @@ -249,7 +247,7 @@ export const DisplaySettingsButton = ({ {!canToggleLogScale && ( - {getLogScaleDisabledReason(y_columns)} + {getLogScaleDisabledReason(y_series)} )} diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx index 52958c120b795..09787e596c77f 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx @@ -12,7 +12,7 @@ interface QueryResultChartProps { result?: QueryResult } -const Y_COLUMN_COLORS = [ +const Y_SERIES_COLORS = [ 'hsl(var(--brand-default))', 'hsl(var(--chart-blue))', 'hsl(var(--chart-3))', @@ -25,36 +25,36 @@ const toChartValue = (value: unknown): string | number => { } export const QueryResultChart = ({ chart, result }: QueryResultChartProps) => { - const { type, x_column, y_columns = [], cumulative, show_labels, scale } = chart ?? {} + const { type, x_column, y_series = [], cumulative, show_labels, scale } = chart ?? {} - const hasConfig = !!x_column && y_columns.length > 0 + const hasConfig = !!x_column && y_series.length > 0 // Logarithmic scale only makes sense for a single series โ€” DisplaySettingsButton // resets `scale` to linear once a second Y column is added - const effectiveScale = y_columns.length > 1 ? 'linear' : scale + const effectiveScale = y_series.length > 1 ? 'linear' : scale const chartConfig: ChartSeriesConfig = useMemo( () => - y_columns.reduce((acc, key, index) => { - acc[key] = { label: key, color: Y_COLUMN_COLORS[index] } + y_series.reduce((acc, key, index) => { + acc[key] = { label: key, color: Y_SERIES_COLORS[index] } return acc }, {} as ChartSeriesConfig), - [y_columns] + [y_series] ) const chartRows = useMemo(() => { const xKey = x_column ?? '' return (result?.rows ?? []).map((row) => { const chartRow: Record = { [xKey]: toChartValue(row[xKey]) } - y_columns.forEach((yKey) => { + y_series.forEach((yKey) => { chartRow[yKey] = toChartValue(row[yKey]) }) return chartRow }) - }, [result, x_column, y_columns]) + }, [result, x_column, y_series]) const cumulativeResults = useMemo( - () => getCumulativeResults({ rows: chartRows }, { yKey: y_columns }), - [chartRows, y_columns] + () => getCumulativeResults({ rows: chartRows }, { yKey: y_series }), + [chartRows, y_series] ) const resultToRender = cumulative ? cumulativeResults : chartRows @@ -81,43 +81,45 @@ export const QueryResultChart = ({ chart, result }: QueryResultChartProps) => { } return ( - - - - {type === 'bar' && ( - - )} - {type === 'line' && ( - - )} + + + +
+ {type === 'bar' && ( + + )} + {type === 'line' && ( + + )} +
diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx index 50985ecf9e98e..8ddfc51f5d8de 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx @@ -1,7 +1,16 @@ import { acceptUntrustedSql, untrustedSql, type UntrustedSqlFragment } from '@supabase/pg-meta' import { useFlag } from 'common' import { CodeSquare, Eye, EyeOff, Play } from 'lucide-react' -import { forwardRef, useImperativeHandle, useState, type ReactNode } from 'react' +import type { editor as monacoEditor } from 'monaco-editor' +import { + forwardRef, + useEffect, + useEffectEvent, + useImperativeHandle, + useRef, + useState, + type ReactNode, +} from 'react' import { Button, cn } from 'ui' import { resolveLogTimeRange } from '../../QuerySources/LogTimeRange.utils' @@ -23,7 +32,10 @@ import { type QueryDisplay, type QueryResult } from '../types' import { DisplaySettingsButton } from './DisplaySettingsButton' import { QueryResultRenderer } from './QueryResultRenderer' import { QuerySourceMenu } from './QuerySourceMenu' +import { useQueryEditorAi } from './useQueryEditorAi' import { LegacyLogsRewriteBanner } from '@/components/interfaces/Settings/Logs/LegacyLogsRewriteBanner' +import { ResizableAIWidget } from '@/components/ui/AIEditor/ResizableAIWidget' +import { getEditorSelectionParts, type EditorSelection } from '@/components/ui/AIEditor/utils' import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' import { DiffEditor } from '@/components/ui/DiffEditor' import { @@ -45,15 +57,25 @@ import { import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query' import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation' import { applyAutoLimit } from '@/data/sql/utils' -import { type LegacyLogsRewriteProposal } from '@/hooks/analytics/useLegacyLogsRewrite' import { useLatest } from '@/hooks/misc/useLatest' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { detectOS } from '@/lib/helpers' import { wrapWithRoleImpersonation } from '@/lib/role-impersonation' import { isRoleImpersonationEnabled, type RoleImpersonationController, } from '@/state/role-impersonation-state' +const generatePlaceholder = (os: string | undefined) => + `Hit ${os === 'macos' ? 'CMD+SHIFT+K' : 'CTRL+SHIFT+K'} to generate query or just start typing` + +type PendingProposal = { + original: string + modified: string + label: string + prompt?: string +} + /** * The query this editor is showing, tagged by backend. The tag correlates the SQL's * dialect brand with that backend's parameters, so a single `_tag` check inside @@ -71,7 +93,11 @@ export type ExplorerQueryModel = uncheckedSql: UntrustedLogSqlFragment } & LogsSourceParameters) -export type QueryEditorProps = { +export type QueryEditorHandle = { + run: () => Promise +} + +type QueryEditorProps = { id: string variant: 'embedded' | 'viewport' title: string @@ -93,10 +119,6 @@ export type QueryEditorProps = { onRun?: () => void } -export type QueryEditorHandle = { - run: () => Promise -} - /** * Shared query editor used by query tabs, notebook cells, and other Explorer surfaces. * The consuming surface owns persistence and surrounding chrome; this component owns @@ -125,6 +147,7 @@ export const QueryEditor = forwardRef(funct }: QueryEditorProps, ref ) { + const os = detectOS() const sql = query.uncheckedSql const sqlRef = useLatest(sql) const onSqlCommitRef = useLatest(onSqlCommit) @@ -138,7 +161,17 @@ export const QueryEditor = forwardRef(funct const databaseIdentifier = query._tag === 'database' ? query.database_identifier : undefined const [showQuery, setShowQuery] = useState(true) - const [rewriteProposal, setRewriteProposal] = useState(null) + const [promptInput, setPromptInput] = useState('') + const [pendingProposal, setPendingProposal] = useState(null) + const pendingProposalRef = useLatest(pendingProposal) + + const editorInstanceRef = useRef(null) + const [promptState, setPromptState] = useState<(EditorSelection & { isOpen: boolean }) | null>( + null + ) + + const dialect = query._tag === 'logs' ? 'clickhouse' : 'postgres' + const { requestCompletion, isCompletionLoading } = useQueryEditorAi({ dialect }) const { data: databases, isPending: isLoadingDatabases } = useReadReplicasQuery( { projectRef: project?.ref }, @@ -173,7 +206,7 @@ export const QueryEditor = forwardRef(funct * Postgres SQL cannot reach the analytics wire or vice versa. */ const handleRunQuery = async (rawSql: string = sql) => { - if (!project || isBusy || rewriteProposal || isRunDisabled || rawSql.trim().length === 0) return + if (!project || isBusy || pendingProposal || isRunDisabled || rawSql.trim().length === 0) return onRun?.() onSqlCommit?.(rawSql) @@ -219,21 +252,55 @@ export const QueryEditor = forwardRef(funct }).catch(() => {}) } - const acceptRewrite = () => { - if (!rewriteProposal) return - if (sql === rewriteProposal.original) { - onSqlChange(rewriteProposal.modified) - onSqlCommit?.(rewriteProposal.modified) + const acceptSqlProposal = () => { + if (!pendingProposal) return + if (sql === pendingProposal.original) { + onSqlChange(pendingProposal.modified) + onSqlCommit?.(pendingProposal.modified) } - setRewriteProposal(null) + setPendingProposal(null) } - const discardRewrite = () => setRewriteProposal(null) + const discardSqlProposal = () => setPendingProposal(null) - useImperativeHandle(ref, () => ({ run: () => handleRunQuery() })) + const closePrompt = () => { + setPromptState(null) + setPromptInput('') + } + + const handleEscapeKey = useEffectEvent((e: KeyboardEvent) => { + if (e.key !== 'Escape') return + closePrompt() + editorInstanceRef.current?.focus() + }) + + const handleGenerateSql = async (prompt: string) => { + if (!promptState) return + + const { original, modified } = await requestCompletion(prompt, promptState) + if (original !== undefined && modified !== undefined) { + setPendingProposal({ + original, + modified, + label: 'Review the suggested SQL edit before accepting it', + prompt, + }) + closePrompt() + } + } const Shell = variant === 'viewport' ? ExplorerQueryViewport : ExplorerQuery + useImperativeHandle(ref, () => ({ run: () => handleRunQuery() })) + + useEffect(() => { + if (!promptState?.isOpen) return + const node = editorInstanceRef.current?.getDomNode() + if (!node) return + node.addEventListener('keydown', handleEscapeKey) + return () => node.removeEventListener('keydown', handleEscapeKey) + }, [promptState?.isOpen]) + return ( @@ -245,10 +312,10 @@ export const QueryEditor = forwardRef(funct {toolbarActions} {onSourceChange && ( { - setRewriteProposal(null) + setPendingProposal(null) onSourceChange(source) }} rowLimit={rowLimit} @@ -267,7 +334,7 @@ export const QueryEditor = forwardRef(funct )} : } - disabled={rewriteProposal !== null} + disabled={pendingProposal !== null} tooltip={showQuery ? 'Hide query' : 'Show query'} onClick={() => setShowQuery((value) => !value)} /> @@ -278,7 +345,7 @@ export const QueryEditor = forwardRef(funct disabled={ isLoadingProject || isExecuting || - rewriteProposal !== null || + pendingProposal !== null || isRunDisabled || sql.trim().length === 0 } @@ -295,8 +362,14 @@ export const QueryEditor = forwardRef(funct isLogsSource={query._tag === 'logs'} sql={sql} readSql={() => sqlRef.current} - onProposal={setRewriteProposal} - hidden={rewriteProposal !== null} + onProposal={({ original, modified }) => + setPendingProposal({ + original, + modified, + label: 'Review the ClickHouse SQL rewrite before accepting it', + }) + } + hidden={pendingProposal !== null} /> (funct id={`explorer-query-${id}`} language="pgsql" value={sql} - placeholder="select * from your_table limit 100;" + placeholder={!promptState?.isOpen ? generatePlaceholder(os) : ''} placeholderClassName="top-[13px]" className={variant === 'embedded' ? 'h-44' : undefined} actions={{ runQuery: { enabled: !isRunDisabled, callback: handleRunQuery } }} options={{ minimap: { enabled: false }, padding: { top: 8 } }} onInputChange={(value) => onSqlChange(value ?? '')} - onMount={(editor) => { + onMount={(editor, monaco) => { editor.onDidBlurEditorWidget(() => onSqlCommitRef.current?.(sqlRef.current)) + editorInstanceRef.current = editor + + editor.addAction({ + id: 'generate-sql', + label: 'Generate SQL', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KeyK], + run: () => { + if (pendingProposalRef.current) return + const selectionParts = getEditorSelectionParts(editor) + if (selectionParts) setPromptState({ isOpen: true, ...selectionParts }) + }, + }) }} /> - {rewriteProposal && ( + + {promptState?.isOpen && editorInstanceRef.current && !pendingProposal && ( + + )} + + {pendingProposal && (
- - Review the ClickHouse SQL rewrite before accepting it - +
+

{pendingProposal.label}

+ {pendingProposal.prompt && ( +

+ Prompt: {pendingProposal.prompt} +

+ )} +
- -
@@ -333,8 +439,8 @@ export const QueryEditor = forwardRef(funct
diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/useQueryEditorAi.ts b/apps/studio/components/interfaces/Explorer/QueryEditor/useQueryEditorAi.ts new file mode 100644 index 0000000000000..a8b7ad9b64c61 --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/useQueryEditorAi.ts @@ -0,0 +1,97 @@ +import { useState } from 'react' +import { toast } from 'sonner' +import * as z from 'zod' + +import { + assembleCompletionDiff, + buildCompletionRequestBody, + type SqlDialect, +} from '@/components/interfaces/SQLEditor/SQLEditor.utils' +import { type EditorSelection } from '@/components/ui/AIEditor/utils' +import { constructHeaders } from '@/data/fetchers' +import { stripSqlCodeFences } from '@/data/logs/logs-sql-rewrite' +import { useLogsAttributeKeys } from '@/hooks/analytics/useLogsAttributeKeys' +import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { BASE_PATH } from '@/lib/constants' +import { formatSql } from '@/lib/formatSql' +import { getErrorMessage } from '@/lib/get-error-message' + +export function useQueryEditorAi({ dialect }: { dialect: SqlDialect }) { + const { data: project } = useSelectedProjectQuery() + const { data: organization } = useSelectedOrganizationQuery() + const { fetchAttributeKeys } = useLogsAttributeKeys() + + const [isCompletionLoading, setIsCompletionLoading] = useState(false) + + const isClickhouse = dialect === 'clickhouse' + + const requestCompletion = async ( + prompt: string, + context: Pick + ) => { + setIsCompletionLoading(true) + try { + const [headerData, availableKeys] = await Promise.all([ + constructHeaders(), + isClickhouse + ? fetchAttributeKeys(context.beforeSelection + context.selection + context.afterSelection) + : undefined, + ]) + const authorizationHeader = headerData.get('Authorization') + + const response = await fetch(`${BASE_PATH}/api/ai/code/complete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(authorizationHeader ? { Authorization: authorizationHeader } : {}), + }, + body: JSON.stringify( + buildCompletionRequestBody({ + projectRef: project?.ref, + connectionString: project?.connectionString, + orgSlug: organization?.slug, + dialect, + options: { + completionMetadata: { + textBeforeCursor: context.beforeSelection, + textAfterCursor: context.afterSelection, + language: 'pgsql', + prompt, + selection: context.selection, + ...(availableKeys ? { availableKeys } : {}), + }, + }, + }) + ), + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(errorText || 'Failed to generate completion') + } + + const text = z.string().parse(await response.json()) + + const meta = { + textBeforeCursor: context.beforeSelection, + textAfterCursor: context.afterSelection, + selection: context.selection, + } + + const { original, modified } = assembleCompletionDiff( + meta, + isClickhouse ? stripSqlCodeFences(text) : text + ) + + return { original, modified: isClickhouse ? modified : formatSql(modified) } + } catch (error) { + toast.error(`Failed to generate SQL: ${getErrorMessage(error, 'Unknown error')}`) + return { original: undefined, modified: undefined } + } finally { + setIsCompletionLoading(false) + } + } + + return { requestCompletion, isCompletionLoading } +} diff --git a/apps/studio/components/interfaces/Explorer/__tests__/ExplorerNotebookTab.test.tsx b/apps/studio/components/interfaces/Explorer/__tests__/ExplorerNotebookTab.test.tsx index 882d79dfb94d6..71425a3fb5eb2 100644 --- a/apps/studio/components/interfaces/Explorer/__tests__/ExplorerNotebookTab.test.tsx +++ b/apps/studio/components/interfaces/Explorer/__tests__/ExplorerNotebookTab.test.tsx @@ -43,7 +43,7 @@ const NOTEBOOK_ID = 'notebook-test' const databaseCell = createQueryCellSkeleton({ sql: 'select 1' }) const logCell = { _tag: 'log_cell' as const, - id: 'log-cell-1', + _id: 'log-cell-1', view: 'table' as const, chart: undefined, unchecked_sql: untrustedLogSql('select * from edge_logs limit 1'), diff --git a/apps/studio/components/interfaces/Explorer/utils.ts b/apps/studio/components/interfaces/Explorer/utils.ts index 70caeba255f24..92a4cc2dcdbaa 100644 --- a/apps/studio/components/interfaces/Explorer/utils.ts +++ b/apps/studio/components/interfaces/Explorer/utils.ts @@ -1,12 +1,12 @@ import { untrustedSql } from '@supabase/pg-meta' import { DEFAULT_CELL_ROW_LIMIT } from './QueryCell/QueryCell.utils' -import { generateUuid } from '@/lib/api/snippets.browser' +import { generateDraftId } from '@/data/content/notebooks/notebook-schema' export const createQueryCellSkeleton = ({ sql }: { sql?: string } = {}) => { return { _tag: 'database_cell' as const, - id: generateUuid(), + _id: generateDraftId(), view: 'table' as const, chart: undefined, unchecked_sql: untrustedSql(sql ?? ''), @@ -26,7 +26,7 @@ export const createMarkdownCellSkeleton = ({ } = {}) => { return { _tag: 'markdown_cell' as const, - id: generateUuid(), + _id: generateDraftId(), text: content, } } diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditorEditorPanel.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditorEditorPanel.tsx index b42c6b9778560..3872e5fa531ab 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditorEditorPanel.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditorEditorPanel.tsx @@ -12,7 +12,7 @@ import { useSqlEditorSnippet, useSqlEditorUi, } from './SQLEditorControllers' -import ResizableAIWidget from '@/components/ui/AIEditor/ResizableAIWidget' +import { ResizableAIWidget } from '@/components/ui/AIEditor/ResizableAIWidget' import { acceptUntrustedLogsSql } from '@/data/logs/safe-analytics-sql' import { detectOS } from '@/lib/helpers' diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx index 75ed7b5d2bb21..7e2f1920e3fbe 100644 --- a/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx @@ -36,9 +36,10 @@ export interface ExplorerLayoutProps extends ComponentProps { - const [section, setSection] = useState() const tabs = useTabsStateSnapshot() + const [section, setSection] = useState() + const activeTab = tabs.activeTab ? tabs.tabsMap[tabs.activeTab] : undefined const isActiveExplorerTab = activeTab !== undefined && editorEntityTypes.explorer.includes(activeTab.type) diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx index da7ce2a25a3f0..e25e74a697d81 100644 --- a/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx @@ -46,7 +46,7 @@ export const ExplorerNavHome = ({

- Recent + Recently updated

Nothing edited yet

diff --git a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.test.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.test.tsx similarity index 73% rename from apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.test.tsx rename to apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.test.tsx index e12d35d52b173..b796ea2e5e0c9 100644 --- a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.test.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.test.tsx @@ -1,14 +1,15 @@ import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { describe, expect, it } from 'vitest' -import { NotebookPreview } from './NotebookPreview' +import { AssistantNotebookPreview } from './AssistantNotebookPreview' import type { NotebookCellDiffEntry } from '@/data/content/notebooks/notebook-operations' import type { AgentCell, CellWire } from '@/data/content/notebooks/notebook-schema' import { customRender as render } from '@/tests/lib/custom-render' const wireMarkdownCell = (id: string, text: string): CellWire => ({ _tag: 'markdown_cell', - id, + _id: id, text, }) @@ -16,7 +17,7 @@ const agentMarkdownCell = (text: string): AgentCell => ({ _tag: 'markdown_cell', const wireDatabaseCell = (id: string, database_identifier?: string): CellWire => ({ _tag: 'database_cell', - id, + _id: id, sql: 'select 1', row_limit: 100, database_identifier, @@ -29,18 +30,20 @@ const agentDatabaseCell = (database_identifier?: string): AgentCell => ({ database_identifier, }) -describe('NotebookPreview', () => { +describe('AssistantNotebookPreview', () => { // The whole safety argument for this feature reduces to this: agent-authored markdown text // is rendered as literal source (via CodeBlock), never interpreted into real DOM nodes. A // future refactor that swaps in would break this silently. - it('never renders agent-authored cell content as real img/link/src DOM nodes', () => { + it('never renders agent-authored cell content as real img/link/src DOM nodes', async () => { + const user = userEvent.setup() const adversarialText = '![x](https://evil.example/) [y](https://evil.example/) ' const entries: NotebookCellDiffEntry[] = [ { _tag: 'added', cell: agentMarkdownCell(adversarialText), operationIndex: 0 }, ] - const { container } = render() + const { container } = render() + await user.click(screen.getByRole('button', { name: 'Added Markdown cell' })) expect(container.querySelectorAll('img')).toHaveLength(0) expect(container.querySelectorAll('[href]')).toHaveLength(0) @@ -53,9 +56,11 @@ describe('NotebookPreview', () => { { _tag: 'unchanged', cell: wireMarkdownCell('b', 'two') }, ] - render() + render() + expect(screen.getByRole('toolbar', { name: 'Notebook toolbar' })).toBeInTheDocument() expect(screen.getByText('2 cells')).toBeInTheDocument() + expect(screen.getByText('New notebook')).toBeInTheDocument() }) it('surfaces a metadata-only change on a replaced cell even when the sql is unchanged', () => { @@ -68,9 +73,12 @@ describe('NotebookPreview', () => { }, ] - render() + render() expect(screen.getByText('Database: primary โ†’ Database: replica-3')).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'Replaced Query: Untitled query' }) + ).toHaveTextContent('Database: primary โ†’ Database: replica-3') }) it('hides entries past the limit behind a "Show N more" button', async () => { @@ -79,7 +87,7 @@ describe('NotebookPreview', () => { cell: wireMarkdownCell(`cell-${index}`, `text-${index}`), })) - render() + render() expect(screen.getByText('Show 2 more cells')).toBeInTheDocument() }) diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.tsx new file mode 100644 index 0000000000000..959378d65e559 --- /dev/null +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.tsx @@ -0,0 +1,101 @@ +import { NotebookText } from 'lucide-react' +import { useState } from 'react' +import { Button, cn } from 'ui' + +import { + formatNotebookDiffSummary, + getEntryKey, + summarizeNotebookDiff, +} from './AssistantNotebookPreview.utils' +import { AssistantNotebookPreviewCell } from './AssistantNotebookPreviewCell' +import { + ExplorerToolbar, + ExplorerToolbarActions, + ExplorerToolbarIcon, + ExplorerToolbarTitle, +} from '@/components/interfaces/Explorer/ExplorerToolbar' +import type { NotebookCellDiffEntry } from '@/data/content/notebooks/notebook-operations' + +export interface AssistantNotebookPreviewProps { + entries: NotebookCellDiffEntry[] + mode: 'create' | 'update' + /** The notebook's name, shown in the toolbar. Falls back to a generic label. */ + title?: string + className?: string +} + +const VISIBLE_ENTRY_LIMIT = 5 + +const FALLBACK_TITLE = { + create: 'New notebook', + update: 'Notebook changes', +} as const + +/** + * Read-only minified notebook for assistant create/update previews. Composes the same + * Explorer toolbar as notebook tabs; the surrounding `Confirm` card owns the frame. + * Pure presentational: no data fetching, no approval or notebook-editor state โ€” see + * `deriveNotebookDiff` for how `entries` is produced. + */ +export const AssistantNotebookPreview = ({ + entries, + mode, + title, + className, +}: AssistantNotebookPreviewProps) => { + const [isShowingAllEntries, setIsShowingAllEntries] = useState(false) + const [expandedOverrides, setExpandedOverrides] = useState>({}) + + const summary = summarizeNotebookDiff(entries, mode) + const visibleEntries = isShowingAllEntries ? entries : entries.slice(0, VISIBLE_ENTRY_LIMIT) + const hiddenCount = entries.length - visibleEntries.length + + const isExpanded = (entry: NotebookCellDiffEntry) => + expandedOverrides[getEntryKey(entry)] === true + + return ( +
+ + + + + {title ?? FALLBACK_TITLE[mode]} + + + {formatNotebookDiffSummary(summary)} + + + +
+
+
+ {visibleEntries.map((entry) => { + const key = getEntryKey(entry) + return ( + + setExpandedOverrides((prev) => ({ ...prev, [key]: open })) + } + /> + ) + })} +
+ {hiddenCount > 0 && ( + + )} +
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.test.ts b/apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.utils.test.ts similarity index 83% rename from apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.test.ts rename to apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.utils.test.ts index 5f4523daea56a..9a7536d73bf9a 100644 --- a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.test.ts +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.utils.test.ts @@ -7,15 +7,16 @@ import { getCellLabel, getCellMetadataLine, getEntryKey, + getEntryMetadataLine, summarizeNotebookDiff, -} from './NotebookPreview.utils' +} from './AssistantNotebookPreview.utils' import type { NotebookCellDiffEntry } from '@/data/content/notebooks/notebook-operations' import type { AgentCell, CellWire } from '@/data/content/notebooks/notebook-schema' import { isoDateTimeString } from '@/lib/iso-datetime' const wireMarkdownCell = (id: string, text = 'hello'): CellWire => ({ _tag: 'markdown_cell', - id, + _id: id, text, }) @@ -23,7 +24,7 @@ const agentMarkdownCell = (text = 'hello'): AgentCell => ({ _tag: 'markdown_cell const wireDatabaseCell = (id: string, title?: string, database_identifier?: string): CellWire => ({ _tag: 'database_cell', - id, + _id: id, title, sql: 'select 1', row_limit: 100, @@ -32,11 +33,18 @@ const wireDatabaseCell = (id: string, title?: string, database_identifier?: stri const wireLogCell = (id: string): CellWire => ({ _tag: 'log_cell', - id, + _id: id, sql: 'select 1', time_range: { _tag: 'relative_time_range', unit: 'day', amount: 7 }, }) +const agentDatabaseCell = (database_identifier?: string): AgentCell => ({ + _tag: 'database_cell', + sql: 'select 1', + row_limit: 100, + database_identifier, +}) + describe('getEntryKey', () => { it('keys unchanged, removed, and moved entries off the cell id', () => { expect(getEntryKey({ _tag: 'unchanged', cell: wireMarkdownCell('cell-1') })).toBe('cell-1') @@ -128,6 +136,39 @@ describe('getCellMetadataLine', () => { }) }) +describe('getEntryMetadataLine', () => { + it('uses the cell metadata for non-replaced entries', () => { + expect( + getEntryMetadataLine({ + _tag: 'unchanged', + cell: wireDatabaseCell('cell-1', 'Signups', 'replica-3'), + }) + ).toBe('Database: replica-3') + }) + + it('returns a before โ†’ after pair when a replacement changes only metadata', () => { + expect( + getEntryMetadataLine({ + _tag: 'replaced', + before: wireDatabaseCell('cell-1', 'Signups', 'primary'), + after: agentDatabaseCell('replica-3'), + operationIndex: 0, + }) + ).toBe('Database: primary โ†’ Database: replica-3') + }) + + it('returns a single line when replacement metadata is unchanged', () => { + expect( + getEntryMetadataLine({ + _tag: 'replaced', + before: wireDatabaseCell('cell-1', 'Signups', 'primary'), + after: agentDatabaseCell('primary'), + operationIndex: 0, + }) + ).toBe('Database: primary') + }) +}) + describe('summarizeNotebookDiff / formatNotebookDiffSummary', () => { it('formats create mode with pluralized cell count', () => { const entries: NotebookCellDiffEntry[] = [ diff --git a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.ts b/apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.utils.ts similarity index 75% rename from apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.ts rename to apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.utils.ts index 3d40260a6e87b..b8aefa9bd82f8 100644 --- a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreview.utils.ts +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreview.utils.ts @@ -1,33 +1,27 @@ import dayjs from 'dayjs' import type { CodeBlockLang } from 'ui-patterns/CodeBlock' -import type { NotebookCellDiffEntry } from '@/data/content/notebooks/notebook-operations' -import type { AgentCell, CellWire, TimeRange } from '@/data/content/notebooks/notebook-schema' +import type { + NotebookCellDiffEntry, + OperationResultCell, +} from '@/data/content/notebooks/notebook-operations' +import type { TimeRange } from '@/data/content/notebooks/notebook-schema' -/** React key for a diff entry. Added/replaced cells have no `id`, so they key off the operation. */ +/** React key for a diff entry. Added/replaced cells have no `_id`, so they key off the operation. */ export function getEntryKey(entry: NotebookCellDiffEntry): string { switch (entry._tag) { case 'unchanged': case 'removed': case 'moved': - return entry.cell.id + return entry.cell._id case 'added': case 'replaced': return `op-${entry.operationIndex}` } } -/** - * Whether a diff entry starts expanded. Only the entries the user has to actually read to - * decide โ€” the ones whose content the assistant is proposing โ€” open on their own; unchanged, - * moved, and removed cells stay as single rows until asked for. - */ -export function isEntryExpandedByDefault(entry: NotebookCellDiffEntry): boolean { - return entry._tag === 'added' || entry._tag === 'replaced' -} - /** Human label for a collapsed/badge row. */ -export function getCellLabel(cell: CellWire | AgentCell): string { +export function getCellLabel(cell: OperationResultCell): string { switch (cell._tag) { case 'markdown_cell': return 'Markdown cell' @@ -38,7 +32,7 @@ export function getCellLabel(cell: CellWire | AgentCell): string { } /** The cell's underlying source text, regardless of backend. */ -export function getCellSourceText(cell: CellWire | AgentCell): string { +export function getCellSourceText(cell: OperationResultCell): string { switch (cell._tag) { case 'markdown_cell': return cell.text @@ -49,7 +43,7 @@ export function getCellSourceText(cell: CellWire | AgentCell): string { } /** Language for rendering the cell's source via `CodeBlock`. */ -export function getCellCodeBlockLanguage(cell: CellWire | AgentCell): CodeBlockLang { +export function getCellCodeBlockLanguage(cell: OperationResultCell): CodeBlockLang { switch (cell._tag) { case 'markdown_cell': return 'markdown' @@ -60,7 +54,7 @@ export function getCellCodeBlockLanguage(cell: CellWire | AgentCell): CodeBlockL } /** Monaco language id for rendering the cell's source via `DiffEditor`. */ -export function getCellMonacoLanguage(cell: CellWire | AgentCell): string { +export function getCellMonacoLanguage(cell: OperationResultCell): string { switch (cell._tag) { case 'markdown_cell': return 'markdown' @@ -85,7 +79,7 @@ export function formatTimeRange(range: TimeRange): string { * when the cell has none. A `replace_cell` can change only this and leave `sql` identical, * so it's compared independently of the source text rather than folded into it. */ -export function getCellMetadataLine(cell: CellWire | AgentCell): string | null { +export function getCellMetadataLine(cell: OperationResultCell): string | null { switch (cell._tag) { case 'markdown_cell': return null @@ -96,6 +90,18 @@ export function getCellMetadataLine(cell: CellWire | AgentCell): string | null { } } +/** Header metadata for a diff row, including a before โ†’ after pair on replacements. */ +export function getEntryMetadataLine(entry: NotebookCellDiffEntry): string | null { + if (entry._tag !== 'replaced') { + return getCellMetadataLine(entry.cell) + } + + const beforeMetadata = getCellMetadataLine(entry.before) + const afterMetadata = getCellMetadataLine(entry.after) + if (beforeMetadata === afterMetadata) return afterMetadata + return `${beforeMetadata ?? 'No metadata'} โ†’ ${afterMetadata ?? 'No metadata'}` +} + export type NotebookDiffSummary = | { mode: 'create'; cellCount: number } | { mode: 'update'; counts: { added: number; removed: number; replaced: number; moved: number } } diff --git a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreviewCell.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreviewCell.tsx similarity index 67% rename from apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreviewCell.tsx rename to apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreviewCell.tsx index 67153618e7b15..908bd0be12456 100644 --- a/apps/studio/components/interfaces/Explorer/NotebookPreview/NotebookPreviewCell.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantNotebookPreviewCell.tsx @@ -1,4 +1,4 @@ -import { ChevronRight } from 'lucide-react' +import { ChevronRight, FileText, SquareCode } from 'lucide-react' import { cn, Collapsible, @@ -13,18 +13,20 @@ import { CodeBlock } from 'ui-patterns/CodeBlock' import { getCellCodeBlockLanguage, getCellLabel, - getCellMetadataLine, getCellMonacoLanguage, getCellSourceText, -} from './NotebookPreview.utils' + getEntryMetadataLine, +} from './AssistantNotebookPreview.utils' import { DiffEditor } from '@/components/ui/DiffEditor' import type { NotebookCellDiffEntry } from '@/data/content/notebooks/notebook-operations' import type { AgentCell, CellWire } from '@/data/content/notebooks/notebook-schema' -export interface NotebookPreviewCellProps { +export interface AssistantNotebookPreviewCellProps { entry: NotebookCellDiffEntry isExpanded: boolean onExpandedChange: (isExpanded: boolean) => void + /** Create previews omit per-row change glyphs โ€” every cell is an add. */ + mode: 'create' | 'update' } /** @@ -47,37 +49,46 @@ function getEntryCell(entry: NotebookCellDiffEntry): CellWire | AgentCell { } /** One row of the diff card: a header line that collapses to a single row, plus its content. */ -export const NotebookPreviewCell = ({ +export const AssistantNotebookPreviewCell = ({ entry, isExpanded, onExpandedChange, -}: NotebookPreviewCellProps) => { + mode, +}: AssistantNotebookPreviewCellProps) => { const marker = CHANGE_MARKERS[entry._tag] const isRemoved = entry._tag === 'removed' - const label = getCellLabel(getEntryCell(entry)) + const cell = getEntryCell(entry) + const label = getCellLabel(cell) + const metadata = getEntryMetadataLine(entry) return ( - + {mode === 'update' && } + {label} + {metadata && ( + + {metadata} + + )} -
+
@@ -108,44 +119,27 @@ const ChangeGlyph = ({ marker }: { marker: ChangeMarker }) => { ) } +/** Same icons the notebook tab uses for query vs markdown cells. */ +const CellTypeIcon = ({ cell }: { cell: CellWire | AgentCell }) => { + const Icon = cell._tag === 'markdown_cell' ? FileText : SquareCode + return +} + const CellBody = ({ entry }: { entry: NotebookCellDiffEntry }) => entry._tag === 'replaced' ? ( ) : ( - <> - - - + ) -/** - * A `replace_cell` can change only the source parameters (`database_identifier`, - * `time_range`) and leave `sql`/`text` identical โ€” the `DiffEditor` above would then show no - * change at all, so the metadata is compared independently and rendered as its own - * before โ†’ after line whenever it differs. - */ -const ReplacedCellBody = ({ before, after }: { before: CellWire; after: AgentCell }) => { - const beforeMetadata = getCellMetadataLine(before) - const afterMetadata = getCellMetadataLine(after) - - return ( - <> - - {beforeMetadata !== afterMetadata ? ( - - ) : ( - - )} - - ) -} +const ReplacedCellBody = ({ before, after }: { before: CellWire; after: AgentCell }) => ( + +) /** * The cell's source, always rendered as literal text via `CodeBlock` โ€” agent-authored markdown @@ -154,14 +148,11 @@ const ReplacedCellBody = ({ before, after }: { before: CellWire; after: AgentCel const CellSource = ({ cell }: { cell: CellWire | AgentCell }) => ( ) - -/** A plain-text metadata line for a query cell โ€” never rendered as a link or attribute. */ -const MetadataLine = ({ text }: { text: string | null }) => - text ?

{text}

: null diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.utils.test.ts b/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.utils.test.ts index b64eddf7e3e45..f5fc067f602cf 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.utils.test.ts +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.utils.test.ts @@ -22,7 +22,7 @@ describe('getAssistantQueryDisplay', () => { chart: { type: 'bar', x_column: 'day', - y_columns: ['signups'], + y_series: ['signups'], cumulative: false, scale: 'linear', show_labels: false, @@ -31,7 +31,7 @@ describe('getAssistantQueryDisplay', () => { }) it('keeps an empty y-axis list when only the x-axis is provided', () => { - expect(getAssistantQueryDisplay({ xAxis: 'day' }).chart?.y_columns).toEqual([]) + expect(getAssistantQueryDisplay({ xAxis: 'day' }).chart?.y_series).toEqual([]) }) }) diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.utils.ts b/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.utils.ts index a09fc6fba5875..930134aea1257 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.utils.ts +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.utils.ts @@ -25,7 +25,7 @@ export function getAssistantQueryDisplay({ ? { type: 'bar', x_column: xAxis ?? '', - y_columns: yAxis ? [yAxis] : [], + y_series: yAxis ? [yAxis] : [], cumulative: false, scale: 'linear', show_labels: false, diff --git a/apps/studio/components/ui/AIAssistantPanel/Confirm.tsx b/apps/studio/components/ui/AIAssistantPanel/Confirm.tsx index 8282bdce2ae3c..a3a7d26c975ec 100644 --- a/apps/studio/components/ui/AIAssistantPanel/Confirm.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/Confirm.tsx @@ -10,6 +10,8 @@ interface ConfirmFooterProps { confirmLabelLoading?: string isLoading?: boolean isDisabled?: boolean + /** Omit the confirm button so only Skip remains (unparseable / unapplyable previews). */ + denyOnly?: boolean /** Escape hatch for consumers that attach the bar directly under their own frame. */ className?: string onCancel?: () => void | Promise @@ -24,6 +26,7 @@ export const ConfirmFooter = ({ confirmLabelLoading = 'Working...', isLoading = false, isDisabled = false, + denyOnly = false, className, onCancel, onConfirm, @@ -33,20 +36,21 @@ export const ConfirmFooter = ({ return (
-
{message}
-
+
{message}
+
- + {!denyOnly && ( + + )}
) @@ -66,18 +70,21 @@ interface ConfirmProps { isLoading?: boolean /** * Children fill the remaining height of the card (e.g. `QueryEditor` in viewport - * mode). Omit for content-sized bodies like notebook previews. + * mode). Omit for content-sized bodies like notebook previews and Edge Function blocks. */ fill?: boolean className?: string + /** Omit the confirm button so only Skip remains. */ + denyOnly?: boolean onCancel?: () => void | Promise onConfirm?: () => void | Promise } /** * Card that wraps an assistant tool preview and optionally attaches a confirm footer - * below it. The card owns the frame; nested surfaces (QueryEditor viewport, unframed - * edge-function blocks) fill the body. + * below it. The card owns the frame; the footer hangs inset from the sides so it reads + * as tucked under the card. Nested surfaces (QueryEditor viewport, unframed notebook + * previews, Edge Function blocks) fill the body. */ export const Confirm = ({ children, @@ -90,6 +97,7 @@ export const Confirm = ({ isLoading = false, fill = false, className, + denyOnly = false, onCancel, onConfirm, }: PropsWithChildren) => { @@ -100,13 +108,14 @@ export const Confirm = ({ return (
-
+
{children}
{bar.show && ( @@ -117,6 +126,7 @@ export const Confirm = ({ confirmLabelLoading={confirmLabelLoading} isLoading={showLoading} isDisabled={!isApprovalRequested} + denyOnly={denyOnly} onCancel={onCancel} onConfirm={onConfirm} /> diff --git a/apps/studio/components/ui/AIAssistantPanel/Message.Parts.tsx b/apps/studio/components/ui/AIAssistantPanel/Message.Parts.tsx index 62cd84b4ba2b5..e6ea99e7623a4 100644 --- a/apps/studio/components/ui/AIAssistantPanel/Message.Parts.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/Message.Parts.tsx @@ -249,7 +249,11 @@ function MessagePartNotebookProposal({ return

{NOTEBOOK_FAILED_LABEL[mode]}

} - const approvalId = state === 'approval-requested' ? toolPart.approval?.id : undefined + const { confirmState, onApprove, onDeny } = getManualToolApprovalHandlers({ + state, + approval: toolPart.approval, + addToolApprovalResponse, + }) return ( addToolApprovalResponse?.({ id: approvalId, approved: true }) : undefined - } - onDeny={ - approvalId - ? () => addToolApprovalResponse?.({ id: approvalId, approved: false }) - : undefined - } + confirmState={confirmState} + onApprove={onApprove} + onDeny={onDeny} /> ) } diff --git a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx index ba4d765cc1756..5d591f90644da 100644 --- a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx @@ -25,8 +25,8 @@ const mockNotebookRow = (overrides: Partial> = {}) => ({ content: { schema_version: 1, cells: [ - { _tag: 'markdown_cell', id: 'cell-1', text: 'hello' }, - { _tag: 'markdown_cell', id: 'cell-2', text: 'world' }, + { _tag: 'markdown_cell', _id: 'cell-1', text: 'hello' }, + { _tag: 'markdown_cell', _id: 'cell-2', text: 'world' }, ], }, ...overrides, @@ -51,6 +51,7 @@ describe('NotebookProposalRenderer', () => { { /> ) + expect(screen.getByRole('toolbar', { name: 'Notebook toolbar' })).toBeInTheDocument() expect(screen.getByText('1 cell')).toBeInTheDocument() + expect(screen.getByText('Assistant wants to create this notebook')).toBeInTheDocument() await user.click(screen.getByRole('button', { name: 'Create' })) expect(onApprove).toHaveBeenCalledTimes(1) }) @@ -78,6 +81,7 @@ describe('NotebookProposalRenderer', () => { { { it('falls back to a raw-input admonition without dropping the confirm footer on a parse failure', async () => { const user = userEvent.setup() + const onApprove = vi.fn() const onDeny = vi.fn() render( ) expect(screen.getByText("Couldn't render a preview for this notebook")).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Create' })).not.toBeInTheDocument() + await user.click(screen.getByRole('button', { name: 'Skip' })) + expect(onDeny).toHaveBeenCalledTimes(1) + expect(onApprove).not.toHaveBeenCalled() + }) + + it('withholds Apply changes when the update cannot be applied as written', async () => { + const user = userEvent.setup() + const onApprove = vi.fn() + const onDeny = vi.fn() + mockContentItem(mockNotebookRow()) + + render( + + ) + + expect(await screen.findByText("This update can't be applied as written")).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Apply changes' })).not.toBeInTheDocument() await user.click(screen.getByRole('button', { name: 'Skip' })) expect(onDeny).toHaveBeenCalledTimes(1) + expect(onApprove).not.toHaveBeenCalled() }) it('renders an Open notebook link once output is available', () => { @@ -154,11 +192,25 @@ describe('NotebookProposalRenderer', () => { expect(link).toHaveAttribute('href', `/project/default/explorer/notebook/${NOTEBOOK_ID}`) }) - it('renders a muted skipped summary when output-denied', () => { + it('keeps the preview in the message after skip', () => { render( - + ) - expect(screen.getByText('Skipped notebook update')).toBeInTheDocument() + expect(screen.getByRole('toolbar', { name: 'Notebook toolbar' })).toBeInTheDocument() + expect(screen.getByText('New notebook')).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Create' })).not.toBeInTheDocument() + expect(screen.queryByText('Skipped notebook creation')).not.toBeInTheDocument() }) }) diff --git a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx index 9a9ea60455171..0f2c665de5106 100644 --- a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx @@ -1,17 +1,19 @@ import { useParams } from 'common' import { Loader2 } from 'lucide-react' import Link from 'next/link' -import { Button, cn } from 'ui' +import { type PropsWithChildren } from 'react' +import { Button } from 'ui' import { Admonition } from 'ui-patterns/Admonition' import { CodeBlock } from 'ui-patterns/CodeBlock' -import { ConfirmFooter } from './Confirm' +import { AssistantNotebookPreview } from './AssistantNotebookPreview' +import { Confirm } from './Confirm' +import { type ConfirmFooterApprovalState } from './Confirm.utils' import { createNotebookInputSchema, notebookToolOutputSchema, updateNotebookInputSchema, } from './Message.utils' -import { NotebookPreview } from '@/components/interfaces/Explorer/NotebookPreview/NotebookPreview' import { AlertError } from '@/components/ui/AlertError' import { deriveNotebookDiff, @@ -24,7 +26,7 @@ import { toWireNotebook } from '@/data/content/notebooks/notebook-schema' export type NotebookProposalMode = 'create' | 'update' // input-streaming and output-error are handled by the caller before this component is -// rendered โ€” see MessagePartCreateNotebook / MessagePartUpdateNotebook in Message.Parts.tsx. +// rendered โ€” see MessagePartNotebookProposal in Message.Parts.tsx. export type NotebookProposalState = | 'input-available' | 'approval-requested' @@ -37,25 +39,25 @@ export interface NotebookProposalRendererProps { state: NotebookProposalState input: unknown output: unknown + /** Result of `getManualToolApprovalConfirmState`. Drives the Confirm footer. */ + confirmState?: ConfirmFooterApprovalState onApprove?: () => void onDeny?: () => void } -type NotebookProposalStepProps = Omit +type NotebookProposalStepProps = Omit const MODE_COPY = { create: { confirmMessage: 'Assistant wants to create this notebook', confirmLabel: 'Create', confirmLabelLoading: 'Creating...', - skippedLabel: 'Skipped notebook creation', outputLabel: 'Notebook created', }, update: { confirmMessage: 'Assistant wants to update this notebook', confirmLabel: 'Apply changes', confirmLabelLoading: 'Applying changes...', - skippedLabel: 'Skipped notebook update', outputLabel: 'Notebook updated', }, } as const @@ -63,26 +65,31 @@ const MODE_COPY = { /** * Renders the create/update notebook tool across all approval states. Owns input parsing * (via the shared `agentNotebookSchema` / `notebookOperationsSchema`), the update-mode diff - * fetch, and the `expected_updated_at` version check. + * fetch, and the `expected_updated_at` version check. Wraps the preview in `Confirm` the + * same way `AssistantQueryCell` wraps `QueryEditor`. */ export const NotebookProposalRenderer = (props: NotebookProposalRendererProps) => { const { mode, state, output } = props - if (state === 'output-denied') { - return ( -

{MODE_COPY[mode].skippedLabel}

- ) - } - if (state === 'output-available') { return } - const { input, onApprove, onDeny } = props + const { input, confirmState, onApprove, onDeny } = props return mode === 'create' ? ( - + ) : ( - + ) } @@ -107,95 +114,91 @@ function NotebookOutputSummary({ mode, output }: { mode: NotebookProposalMode; o ) } -interface NotebookConfirmFooterProps { +interface NotebookConfirmProps { mode: NotebookProposalMode - state: NotebookProposalState + confirmState?: ConfirmFooterApprovalState message?: string confirmLabel?: string confirmLabelLoading?: string extraLoading?: boolean + denyOnly?: boolean onApprove?: () => void onDeny?: () => void } -/** - * `ConfirmFooter` now ships bare so the `Confirm` card can own the frame, so this renderer - * supplies its own flush-under-block border. The block above still has to square off its - * bottom corners and the two must not be gapped apart. - */ -const GLUED_TO_FOOTER = 'rounded-b-none' -const FLUSH_UNDER_BLOCK = 'border border-t-0 rounded-b-lg' - -function hasConfirmFooter(state: NotebookProposalState) { - return state === 'approval-requested' || state === 'approval-responded' -} - -/** The footer morphs (label + disabled) across approval-requested/approval-responded and is absent otherwise. */ -function NotebookConfirmFooter({ +/** Confirm card around a notebook preview, matching AssistantQueryCell / EdgeFunctionRenderer. */ +function NotebookConfirm({ mode, - state, + confirmState, message, confirmLabel, confirmLabelLoading, - extraLoading = false, + extraLoading, + denyOnly, onApprove, onDeny, -}: NotebookConfirmFooterProps) { - if (!hasConfirmFooter(state)) return null - + children, +}: PropsWithChildren) { const copy = MODE_COPY[mode] - const isApprovalRequested = state === 'approval-requested' return ( - + extraLoading={extraLoading} + denyOnly={denyOnly} + onCancel={onDeny} + onConfirm={denyOnly ? undefined : onApprove} + > + {children} + ) } function NotebookParseFailure({ mode, - state, + confirmState, input, - onApprove, onDeny, -}: Pick) { +}: Pick) { return ( -
- - - -
+ +
+ + +
+
) } -function CreateNotebookProposal({ state, input, onApprove, onDeny }: NotebookProposalStepProps) { +function CreateNotebookProposal({ + input, + confirmState, + onApprove, + onDeny, +}: NotebookProposalStepProps) { const parsedInput = createNotebookInputSchema.safeParse(input) if (!parsedInput.success) { return ( ) @@ -210,19 +213,23 @@ function CreateNotebookProposal({ state, input, onApprove, onDeny }: NotebookPro ) return ( -
- - -
+ + + ) } -function UpdateNotebookProposal({ state, input, onApprove, onDeny }: NotebookProposalStepProps) { +function UpdateNotebookProposal({ + input, + confirmState, + onApprove, + onDeny, +}: NotebookProposalStepProps) { const { ref } = useParams() const parsedInput = updateNotebookInputSchema.safeParse(input) @@ -242,9 +249,8 @@ function UpdateNotebookProposal({ state, input, onApprove, onDeny }: NotebookPro return ( ) @@ -263,12 +269,12 @@ function UpdateNotebookProposal({ state, input, onApprove, onDeny }: NotebookPro return (
- {(state === 'approval-requested' || state === 'approval-responded') && ( + {confirmState !== undefined && (
+
+
+ {oauthBlocks.title} +
+ {oauthBlocks.items.map((item, i) => ( + + ))} +
{platformBlocks.title} diff --git a/apps/ui-library/config/docs.ts b/apps/ui-library/config/docs.ts index e0585e27e78da..3e496318a66fb 100644 --- a/apps/ui-library/config/docs.ts +++ b/apps/ui-library/config/docs.ts @@ -36,6 +36,20 @@ export const platformBlocks: SidebarNavGroup = { ], } +export const oauthBlocks: SidebarNavGroup = { + title: 'OAuth', + items: [ + { + title: 'OAuth Consent', + supportedFrameworks: ['nextjs', 'react-router', 'tanstack', 'react'], + href: '/docs/nextjs/oauth-consent', + items: [], + new: true, + commandItemLabel: 'OAuth Consent', + }, + ], +} + // Component definitions with supported frameworks export const componentPages: SidebarNavGroup = { title: 'Components', @@ -133,6 +147,10 @@ export const COMMAND_ITEMS = [ label: item.commandItemLabel, href: item.href, })), + ...oauthBlocks.items.map((item) => ({ + label: item.commandItemLabel, + href: item.href, + })), ] // Framework titles for display diff --git a/apps/ui-library/content/docs/nextjs/oauth-consent.mdx b/apps/ui-library/content/docs/nextjs/oauth-consent.mdx new file mode 100644 index 0000000000000..ae473e57a1fd1 --- /dev/null +++ b/apps/ui-library/content/docs/nextjs/oauth-consent.mdx @@ -0,0 +1,77 @@ +--- +title: OAuth Consent +description: OAuth 2.1 consent block for Next.js +--- + + + +## Installation + + + +## Folder structure + +This block includes the [Supabase client](/library/docs/nextjs/client). If you already have one installed, you can skip overwriting it. + + + +## Usage + +This block installs an OAuth 2.1 consent route at `/oauth/consent`. It is designed for an app that already has authentication: it does not install sign-in, sign-up, or callback routes. + +When the visitor has no session, the consent screen redirects to `/auth/login` and preserves the original consent URL in the `next` query parameter. Update the `signInPath` prop in `app/oauth/consent/page.tsx` if your sign-in route is different. + +After sign-in, your login page must send the visitor back to the path in `next`; otherwise the OAuth flow stops at your login screen. The password-based auth and social auth blocks follow `next` automatically. For a custom login page, validate that `next` is a relative path before redirecting to it: + +```ts +// After sign-in succeeds: +const next = new URLSearchParams(window.location.search).get('next') +try { + const nextUrl = new URL(next ?? '/protected', window.location.origin) + router.push( + nextUrl.origin === window.location.origin + ? `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}` + : '/protected' + ) +} catch { + router.push('/protected') +} +``` + +Set the `productName` prop to replace the `Your product` placeholder in the consent header. + +Your middleware must let unauthenticated requests reach `/oauth/consent`. The middleware in `lib/supabase/middleware.ts` exempts only `/oauth/consent`. Middleware that redirects to a login page before the consent route runs drops the authorization and ends the OAuth flow at your login screen. + +### Getting started + +After installing the block, you'll have the following environment variables in your `.env.local` file: + +```env +NEXT_PUBLIC_SUPABASE_URL= +NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY= +``` + +- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=nextjs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). +- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running). + +### Configure the OAuth server + +Enable the OAuth server in the Supabase Dashboard under **Authentication** > **OAuth Server**, then set its authorization URL path to `/oauth/consent`. For local development, set the following in `supabase/config.toml`: + +```toml +[auth.oauth_server] +enabled = true +authorization_url_path = "/oauth/consent" +``` + +Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false`. Edit that section rather than adding a second one, which fails with `table oauth_server already exists`. + +The route expects the `authorization_id` query parameter that Supabase Auth supplies during the authorization flow. + +## Further reading + +- [OAuth Server](https://supabase.com/docs/guides/auth/oauth-server) +- [Supabase Auth](https://supabase.com/docs/guides/auth) diff --git a/apps/ui-library/content/docs/react-router/oauth-consent.mdx b/apps/ui-library/content/docs/react-router/oauth-consent.mdx new file mode 100644 index 0000000000000..755a79b25b175 --- /dev/null +++ b/apps/ui-library/content/docs/react-router/oauth-consent.mdx @@ -0,0 +1,77 @@ +--- +title: OAuth Consent +description: OAuth 2.1 consent block for React Router +--- + + + +## Installation + + + +## Folder structure + +This block includes the [Supabase client](/library/docs/react-router/client). If you already have one installed, you can skip overwriting it. + + + +## Usage + +This block installs an OAuth 2.1 consent route at `/oauth/consent`. It is designed for an app that already has authentication: it does not install sign-in, sign-up, or callback routes. + +When the visitor has no session, the consent screen redirects to `/auth/login` and preserves the original consent URL in the `next` query parameter. Update the `signInPath` prop in `app/routes/oauth.consent.tsx` if your sign-in route is different. + +After sign-in, your login route must send the visitor back to the path in `next`; otherwise the OAuth flow stops at your login screen. The password-based auth and social auth blocks follow `next` automatically. For a custom login route, pass `next` through the form (for example with a hidden input), validate that it is a relative path, and redirect to it from the action: + +```ts +// In the login action, after sign-in succeeds: +const origin = new URL(request.url).origin +const next = formData.get('next') +try { + const nextUrl = new URL(typeof next === 'string' ? next : '/protected', origin) + return redirect( + nextUrl.origin === origin + ? `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}` + : '/protected', + { headers } + ) +} catch { + return redirect('/protected', { headers }) +} +``` + +Set the `productName` prop to replace the `Your product` placeholder in the consent header. + +### Getting started + +After installing the block, you'll have the following environment variables in your `.env.local` file: + +```env +VITE_SUPABASE_URL= +VITE_SUPABASE_PUBLISHABLE_KEY= +``` + +- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). +- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running). + +### Configure the OAuth server + +Enable the OAuth server in the Supabase Dashboard under **Authentication** > **OAuth Server**, then set its authorization URL path to `/oauth/consent`. For local development, set the following in `supabase/config.toml`: + +```toml +[auth.oauth_server] +enabled = true +authorization_url_path = "/oauth/consent" +``` + +Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false`. Edit that section rather than adding a second one, which fails with `table oauth_server already exists`. + +The route expects the `authorization_id` query parameter that Supabase Auth supplies during the authorization flow. + +## Further reading + +- [OAuth Server](https://supabase.com/docs/guides/auth/oauth-server) +- [Supabase Auth](https://supabase.com/docs/guides/auth) diff --git a/apps/ui-library/content/docs/react/oauth-consent.mdx b/apps/ui-library/content/docs/react/oauth-consent.mdx new file mode 100644 index 0000000000000..b3caac21784f7 --- /dev/null +++ b/apps/ui-library/content/docs/react/oauth-consent.mdx @@ -0,0 +1,93 @@ +--- +title: OAuth Consent +description: OAuth 2.1 consent block for React single-page applications +--- + + + +## Installation + + + +## Folder structure + +This block includes the [Supabase client](/library/docs/react/client). If you already have one installed, you can skip overwriting it. + + + +## Usage + +This block provides an OAuth 2.1 consent component for an app that already has authentication. It does not install sign-in, sign-up, callback, or router-specific routes. + +Render the component from your existing `/oauth/consent` route and pass the `authorization_id` query parameter. Set `signInPath` to the sign-in route in your app: + +```tsx +import { OAuthConsent } from '@/components/oauth-consent' + +export function ConsentPage() { + const authorizationId = new URLSearchParams(window.location.search).get('authorization_id') + + return ( +
+ +
+ ) +} +``` + +When the visitor has no session, the consent screen redirects to `signInPath` and preserves the original consent URL in the `next` query parameter. After sign-in, your login page must send the visitor back to the path in `next`; otherwise the OAuth flow stops at your login screen. The password-based auth block follows `next` automatically. For a custom login page, validate that `next` is a relative path before redirecting to it: + +```ts +// After sign-in succeeds: +const next = new URLSearchParams(window.location.search).get('next') +try { + const nextUrl = new URL(next ?? '/protected', window.location.origin) + location.href = + nextUrl.origin === window.location.origin + ? `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}` + : '/protected' +} catch { + location.href = '/protected' +} +``` + +Set the `productName` prop to replace the `Your product` placeholder in the consent header. + +### Getting started + +After installing the block, you'll have the following environment variables in your `.env.local` file: + +```env +VITE_SUPABASE_URL= +VITE_SUPABASE_PUBLISHABLE_KEY= +``` + +- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). +- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running). + +### Configure the OAuth server + +Enable the OAuth server in the Supabase Dashboard under **Authentication** > **OAuth Server**, then set its authorization URL path to `/oauth/consent`. For local development, set the following in `supabase/config.toml`: + +```toml +[auth.oauth_server] +enabled = true +authorization_url_path = "/oauth/consent" +``` + +Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false`. Edit that section rather than adding a second one, which fails with `table oauth_server already exists`. + +The route expects the `authorization_id` query parameter that Supabase Auth supplies during the authorization flow. + +## Further reading + +- [OAuth Server](https://supabase.com/docs/guides/auth/oauth-server) +- [Supabase Auth](https://supabase.com/docs/guides/auth) diff --git a/apps/ui-library/content/docs/tanstack/oauth-consent.mdx b/apps/ui-library/content/docs/tanstack/oauth-consent.mdx new file mode 100644 index 0000000000000..01e57232cac3d --- /dev/null +++ b/apps/ui-library/content/docs/tanstack/oauth-consent.mdx @@ -0,0 +1,69 @@ +--- +title: OAuth Consent +description: OAuth 2.1 consent block for TanStack Start +--- + + + +## Installation + + + +## Folder structure + +This block includes the [Supabase client](/library/docs/tanstack/client). If you already have one installed, you can skip overwriting it. + + + +## Usage + +This block installs an OAuth 2.1 consent route at `/oauth/consent`. It is designed for an app that already has authentication: it does not install sign-in, sign-up, or callback routes. + +When the visitor has no session, the consent screen redirects to `/auth/login` and preserves the original consent URL in the `next` query parameter. Update the `signInPath` prop in `routes/oauth/consent.tsx` if your sign-in route is different. + +After sign-in, your login page must send the visitor back to the path in `next`; otherwise the OAuth flow stops at your login screen. The password-based auth and social auth blocks follow `next` automatically. For a custom login page, validate that `next` is a relative path before redirecting to it: + +```ts +// After sign-in succeeds: +const next = new URLSearchParams(window.location.search).get('next') +if (next?.startsWith('/') && !next.startsWith('//')) { + // `next` may point outside the typed route tree, so use a full navigation. + window.location.assign(next) +} +``` + +Set the `productName` prop to replace the `Your product` placeholder in the consent header. + +### Getting started + +After installing the block, you'll have the following environment variables in your `.env.local` file: + +```env +VITE_SUPABASE_URL= +VITE_SUPABASE_PUBLISHABLE_KEY= +``` + +- If you're using supabase.com, you can find these values in the [Connect modal](https://supabase.com/dashboard/project/_?showConnect=true&connectTab=frameworks&framework=react&using=vite&with=supabasejs) under App Frameworks or in your project's [API settings](https://supabase.com/dashboard/project/_/settings/api). +- If you're using a local instance of Supabase, you can find these values by running `supabase start` or `supabase status` (if you already have it running). + +### Configure the OAuth server + +Enable the OAuth server in the Supabase Dashboard under **Authentication** > **OAuth Server**, then set its authorization URL path to `/oauth/consent`. For local development, set the following in `supabase/config.toml`: + +```toml +[auth.oauth_server] +enabled = true +authorization_url_path = "/oauth/consent" +``` + +Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false`. Edit that section rather than adding a second one, which fails with `table oauth_server already exists`. + +The route expects the `authorization_id` query parameter that Supabase Auth supplies during the authorization flow. + +## Further reading + +- [OAuth Server](https://supabase.com/docs/guides/auth/oauth-server) +- [Supabase Auth](https://supabase.com/docs/guides/auth) diff --git a/apps/ui-library/public/r/current-user-avatar-nextjs.json b/apps/ui-library/public/r/current-user-avatar-nextjs.json index 44046fecd08c4..df4744d15128d 100644 --- a/apps/ui-library/public/r/current-user-avatar-nextjs.json +++ b/apps/ui-library/public/r/current-user-avatar-nextjs.json @@ -34,7 +34,7 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { diff --git a/apps/ui-library/public/r/dropzone-nextjs.json b/apps/ui-library/public/r/dropzone-nextjs.json index a7db28470b1a1..a3bdfc16603ad 100644 --- a/apps/ui-library/public/r/dropzone-nextjs.json +++ b/apps/ui-library/public/r/dropzone-nextjs.json @@ -31,7 +31,7 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { diff --git a/apps/ui-library/public/r/oauth-consent-nextjs.json b/apps/ui-library/public/r/oauth-consent-nextjs.json new file mode 100644 index 0000000000000..a7c139492f062 --- /dev/null +++ b/apps/ui-library/public/r/oauth-consent-nextjs.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "oauth-consent-nextjs", + "type": "registry:block", + "title": "OAuth Consent for Next.js", + "description": "OAuth 2.1 consent screen and route for an existing authenticated application.", + "dependencies": [ + "@supabase/ssr@latest", + "@supabase/supabase-js@latest" + ], + "registryDependencies": [ + "button", + "card", + "https://supabase.com/library/r/safe-next-path.json" + ], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport {\n useOAuthConsent,\n type OAuthConsentDecision,\n} from '@/registry/default/blocks/oauth-consent/hooks/use-oauth-consent'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nconst getInitial = (value: string) => value.trim().charAt(0).toUpperCase() || '?'\n\ninterface ConsentCardShellProps extends React.ComponentPropsWithoutRef<'div'> {\n clientName: string\n productName: string\n}\n\nfunction ConsentCardShell({\n clientName,\n productName,\n className,\n children,\n ...props\n}: ConsentCardShellProps) {\n return (\n
\n \n \n \n
\n {getInitial(clientName)}\n
\n
\n
\n {getInitial(productName)}\n
\n
\n
\n Authorize {clientName}\n Review what this client gets access to.\n
\n
\n {children}\n
\n
\n )\n}\n\nexport interface OAuthConsentCardProps extends React.ComponentPropsWithoutRef<'div'> {\n clientName: string\n productName?: string\n redirectUri: string\n email: string\n scopes?: string[]\n error?: string | null\n decision?: OAuthConsentDecision | null\n onApprove?: () => void\n onDeny?: () => void\n}\n\nexport function OAuthConsentCard({\n clientName,\n productName = 'Your product',\n redirectUri,\n email,\n scopes = [],\n error = null,\n decision = null,\n onApprove,\n onDeny,\n ...props\n}: OAuthConsentCardProps) {\n return (\n \n
\n
\n
Client
\n
{clientName}
\n
\n
\n
Redirects to
\n
{redirectUri}
\n
\n
\n
Signed in as
\n
{email}
\n
\n {scopes.length > 0 && (\n
\n
Scopes
\n
{scopes.join(', ')}
\n
\n )}\n
\n

\n Allow access only if you recognize this application. It can act on your behalf only within\n the requested permissions and the access you already have.\n

\n {error && (\n

\n {error}\n

\n )}\n
\n \n \n
\n
\n )\n}\n\ninterface OAuthConsentProps extends React.ComponentPropsWithoutRef<'div'> {\n authorizationId?: string | null\n signInPath?: string\n productName?: string\n}\n\nexport function OAuthConsent({\n authorizationId,\n signInPath = '/auth/login',\n productName = 'Your product',\n ...props\n}: OAuthConsentProps) {\n const { details, email, error, isLoading, decision, approve, deny } = useOAuthConsent({\n authorizationId,\n signInPath,\n })\n\n if (isLoading || !details || !email) {\n return (\n \n {isLoading ? (\n

\n Loading authorization request...\n

\n ) : (\n

\n {error ??\n 'Unable to load the authorization request. Start again from your OAuth client.'}\n

\n )}\n
\n )\n }\n\n return (\n void approve()}\n onDeny={() => void deny()}\n {...props}\n />\n )\n}\n", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "content": "import { isAuthSessionMissingError } from '@supabase/supabase-js'\nimport type { OAuthAuthorizationDetails } from '@supabase/supabase-js'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\n\nexport type OAuthConsentDecision = 'approve' | 'deny'\n\nexport interface UseOAuthConsentOptions {\n authorizationId?: string | null\n signInPath?: string\n}\n\nconst withNextParam = (path: string, next: string) => {\n const url = new URL(path, window.location.origin)\n const searchParams = new URLSearchParams(url.search)\n searchParams.set('next', next)\n url.search = searchParams.toString()\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nconst useOAuthConsent = ({\n authorizationId,\n signInPath = '/auth/login',\n}: UseOAuthConsentOptions) => {\n const [details, setDetails] = useState(null)\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(true)\n const [decision, setDecision] = useState(null)\n const isDeciding = useRef(false)\n\n useEffect(() => {\n let active = true\n\n const loadAuthorization = async () => {\n setIsLoading(true)\n setError(null)\n setDetails(null)\n setDecision(null)\n\n if (!authorizationId) {\n setError('This page needs an authorization_id. Start again from your OAuth client.')\n setIsLoading(false)\n return\n }\n\n const supabase = createClient()\n const {\n data: { user },\n error: userError,\n } = await supabase.auth.getUser()\n\n if (userError && !isAuthSessionMissingError(userError)) {\n if (active) {\n setError(userError.message)\n setIsLoading(false)\n }\n return\n }\n\n if (!user) {\n const next = `${window.location.pathname}${window.location.search}`\n if (active) {\n window.location.replace(withNextParam(safeNextPath(signInPath, '/auth/login'), next))\n }\n return\n }\n\n const { data, error } = await supabase.auth.oauth.getAuthorizationDetails(authorizationId)\n if (error) {\n if (active) {\n setError(error.message)\n setIsLoading(false)\n }\n return\n }\n\n if (!('authorization_id' in data)) {\n if (active) {\n window.location.replace(data.redirect_url)\n }\n return\n }\n\n if (active) {\n setDetails(data)\n setIsLoading(false)\n }\n }\n\n void loadAuthorization()\n return () => {\n active = false\n }\n }, [authorizationId, signInPath])\n\n const decide = useCallback(\n async (action: OAuthConsentDecision) => {\n if (!authorizationId || isDeciding.current) return\n\n isDeciding.current = true\n setDecision(action)\n setError(null)\n const supabase = createClient()\n const result =\n action === 'approve'\n ? await supabase.auth.oauth.approveAuthorization(authorizationId, {\n skipBrowserRedirect: true,\n })\n : await supabase.auth.oauth.denyAuthorization(authorizationId, {\n skipBrowserRedirect: true,\n })\n\n if (result.error) {\n setError(result.error.message)\n setDecision(null)\n isDeciding.current = false\n return\n }\n\n if (!result.data?.redirect_url) {\n setError('The server did not return a redirect. Start again from your OAuth client.')\n setDecision(null)\n isDeciding.current = false\n return\n }\n\n window.location.assign(result.data.redirect_url)\n },\n [authorizationId]\n )\n\n return {\n details,\n email: details?.user.email ?? null,\n error,\n isLoading,\n decision,\n approve: () => decide('approve'),\n deny: () => decide('deny'),\n }\n}\n\ntype UseOAuthConsentReturn = ReturnType\n\nexport { useOAuthConsent, type OAuthAuthorizationDetails, type UseOAuthConsentReturn }\n", + "type": "registry:hook" + }, + { + "path": "registry/default/blocks/oauth-consent-nextjs/app/oauth/consent/page.tsx", + "content": "import { OAuthConsent } from '@/registry/default/blocks/oauth-consent/components/oauth-consent'\n\nexport default async function ConsentPage({\n searchParams,\n}: {\n searchParams: Promise<{ authorization_id?: string }>\n}) {\n const { authorization_id } = await searchParams\n\n return (\n
\n \n
\n )\n}\n", + "type": "registry:page", + "target": "app/oauth/consent/page.tsx" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/client.ts", + "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/server.ts", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n const cookieStore = await cookies()\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll()\n },\n setAll(cookiesToSet) {\n try {\n cookiesToSet.forEach(({ name, value, options }) =>\n cookieStore.set(name, value, options)\n )\n } catch {\n // The `setAll` method was called from a Server Component.\n // This can be ignored if you have middleware refreshing\n // user sessions.\n }\n },\n },\n }\n )\n}\n", + "type": "registry:lib" + } + ], + "envVars": { + "NEXT_PUBLIC_SUPABASE_URL": "", + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" + }, + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` โ€” edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```\n\nYou'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`." +} \ No newline at end of file diff --git a/apps/ui-library/public/r/oauth-consent-react-router.json b/apps/ui-library/public/r/oauth-consent-react-router.json new file mode 100644 index 0000000000000..aa1bef8314b19 --- /dev/null +++ b/apps/ui-library/public/r/oauth-consent-react-router.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "oauth-consent-react-router", + "type": "registry:block", + "title": "OAuth Consent for React Router", + "description": "OAuth 2.1 consent screen and route for an existing authenticated application.", + "dependencies": [ + "@supabase/ssr@latest", + "@supabase/supabase-js@latest" + ], + "registryDependencies": [ + "button", + "card", + "https://supabase.com/library/r/safe-next-path.json" + ], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport {\n useOAuthConsent,\n type OAuthConsentDecision,\n} from '@/registry/default/blocks/oauth-consent/hooks/use-oauth-consent'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nconst getInitial = (value: string) => value.trim().charAt(0).toUpperCase() || '?'\n\ninterface ConsentCardShellProps extends React.ComponentPropsWithoutRef<'div'> {\n clientName: string\n productName: string\n}\n\nfunction ConsentCardShell({\n clientName,\n productName,\n className,\n children,\n ...props\n}: ConsentCardShellProps) {\n return (\n
\n \n \n \n
\n {getInitial(clientName)}\n
\n
\n
\n {getInitial(productName)}\n
\n
\n
\n Authorize {clientName}\n Review what this client gets access to.\n
\n
\n {children}\n
\n
\n )\n}\n\nexport interface OAuthConsentCardProps extends React.ComponentPropsWithoutRef<'div'> {\n clientName: string\n productName?: string\n redirectUri: string\n email: string\n scopes?: string[]\n error?: string | null\n decision?: OAuthConsentDecision | null\n onApprove?: () => void\n onDeny?: () => void\n}\n\nexport function OAuthConsentCard({\n clientName,\n productName = 'Your product',\n redirectUri,\n email,\n scopes = [],\n error = null,\n decision = null,\n onApprove,\n onDeny,\n ...props\n}: OAuthConsentCardProps) {\n return (\n \n
\n
\n
Client
\n
{clientName}
\n
\n
\n
Redirects to
\n
{redirectUri}
\n
\n
\n
Signed in as
\n
{email}
\n
\n {scopes.length > 0 && (\n
\n
Scopes
\n
{scopes.join(', ')}
\n
\n )}\n
\n

\n Allow access only if you recognize this application. It can act on your behalf only within\n the requested permissions and the access you already have.\n

\n {error && (\n

\n {error}\n

\n )}\n
\n \n \n
\n
\n )\n}\n\ninterface OAuthConsentProps extends React.ComponentPropsWithoutRef<'div'> {\n authorizationId?: string | null\n signInPath?: string\n productName?: string\n}\n\nexport function OAuthConsent({\n authorizationId,\n signInPath = '/auth/login',\n productName = 'Your product',\n ...props\n}: OAuthConsentProps) {\n const { details, email, error, isLoading, decision, approve, deny } = useOAuthConsent({\n authorizationId,\n signInPath,\n })\n\n if (isLoading || !details || !email) {\n return (\n \n {isLoading ? (\n

\n Loading authorization request...\n

\n ) : (\n

\n {error ??\n 'Unable to load the authorization request. Start again from your OAuth client.'}\n

\n )}\n
\n )\n }\n\n return (\n void approve()}\n onDeny={() => void deny()}\n {...props}\n />\n )\n}\n", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "content": "import { isAuthSessionMissingError } from '@supabase/supabase-js'\nimport type { OAuthAuthorizationDetails } from '@supabase/supabase-js'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/client'\n\nexport type OAuthConsentDecision = 'approve' | 'deny'\n\nexport interface UseOAuthConsentOptions {\n authorizationId?: string | null\n signInPath?: string\n}\n\nconst withNextParam = (path: string, next: string) => {\n const url = new URL(path, window.location.origin)\n const searchParams = new URLSearchParams(url.search)\n searchParams.set('next', next)\n url.search = searchParams.toString()\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nconst useOAuthConsent = ({\n authorizationId,\n signInPath = '/auth/login',\n}: UseOAuthConsentOptions) => {\n const [details, setDetails] = useState(null)\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(true)\n const [decision, setDecision] = useState(null)\n const isDeciding = useRef(false)\n\n useEffect(() => {\n let active = true\n\n const loadAuthorization = async () => {\n setIsLoading(true)\n setError(null)\n setDetails(null)\n setDecision(null)\n\n if (!authorizationId) {\n setError('This page needs an authorization_id. Start again from your OAuth client.')\n setIsLoading(false)\n return\n }\n\n const supabase = createClient()\n const {\n data: { user },\n error: userError,\n } = await supabase.auth.getUser()\n\n if (userError && !isAuthSessionMissingError(userError)) {\n if (active) {\n setError(userError.message)\n setIsLoading(false)\n }\n return\n }\n\n if (!user) {\n const next = `${window.location.pathname}${window.location.search}`\n if (active) {\n window.location.replace(withNextParam(safeNextPath(signInPath, '/auth/login'), next))\n }\n return\n }\n\n const { data, error } = await supabase.auth.oauth.getAuthorizationDetails(authorizationId)\n if (error) {\n if (active) {\n setError(error.message)\n setIsLoading(false)\n }\n return\n }\n\n if (!('authorization_id' in data)) {\n if (active) {\n window.location.replace(data.redirect_url)\n }\n return\n }\n\n if (active) {\n setDetails(data)\n setIsLoading(false)\n }\n }\n\n void loadAuthorization()\n return () => {\n active = false\n }\n }, [authorizationId, signInPath])\n\n const decide = useCallback(\n async (action: OAuthConsentDecision) => {\n if (!authorizationId || isDeciding.current) return\n\n isDeciding.current = true\n setDecision(action)\n setError(null)\n const supabase = createClient()\n const result =\n action === 'approve'\n ? await supabase.auth.oauth.approveAuthorization(authorizationId, {\n skipBrowserRedirect: true,\n })\n : await supabase.auth.oauth.denyAuthorization(authorizationId, {\n skipBrowserRedirect: true,\n })\n\n if (result.error) {\n setError(result.error.message)\n setDecision(null)\n isDeciding.current = false\n return\n }\n\n if (!result.data?.redirect_url) {\n setError('The server did not return a redirect. Start again from your OAuth client.')\n setDecision(null)\n isDeciding.current = false\n return\n }\n\n window.location.assign(result.data.redirect_url)\n },\n [authorizationId]\n )\n\n return {\n details,\n email: details?.user.email ?? null,\n error,\n isLoading,\n decision,\n approve: () => decide('approve'),\n deny: () => decide('deny'),\n }\n}\n\ntype UseOAuthConsentReturn = ReturnType\n\nexport { useOAuthConsent, type OAuthAuthorizationDetails, type UseOAuthConsentReturn }\n", + "type": "registry:hook" + }, + { + "path": "registry/default/blocks/oauth-consent-react-router/app/routes/oauth.consent.tsx", + "content": "import { useSearchParams } from 'react-router'\n\nimport { OAuthConsent } from '@/registry/default/blocks/oauth-consent/components/oauth-consent'\n\nexport default function ConsentRoute() {\n const [searchParams] = useSearchParams()\n\n return (\n
\n \n
\n )\n}\n", + "type": "registry:file", + "target": "app/routes/oauth.consent.tsx" + }, + { + "path": "registry/default/clients/react-router/lib/supabase/client.ts", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/react-router/lib/supabase/server.ts", + "content": "import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'\n\nexport function createClient(request: Request) {\n const headers = new Headers()\n\n const supabase = createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return parseCookieHeader(request.headers.get('Cookie') ?? '') as {\n name: string\n value: string\n }[]\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value, options }) =>\n headers.append('Set-Cookie', serializeCookieHeader(name, value, options))\n )\n },\n },\n }\n )\n\n return { supabase, headers }\n}\n", + "type": "registry:lib" + } + ], + "envVars": { + "VITE_SUPABASE_URL": "", + "VITE_SUPABASE_PUBLISHABLE_KEY": "" + }, + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` โ€” edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```\n\nYou'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." +} \ No newline at end of file diff --git a/apps/ui-library/public/r/oauth-consent-react.json b/apps/ui-library/public/r/oauth-consent-react.json new file mode 100644 index 0000000000000..6326997caf73c --- /dev/null +++ b/apps/ui-library/public/r/oauth-consent-react.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "oauth-consent-react", + "type": "registry:block", + "title": "OAuth Consent for React", + "description": "OAuth 2.1 consent component for an existing authenticated application.", + "dependencies": [ + "@supabase/supabase-js@latest" + ], + "registryDependencies": [ + "button", + "card", + "https://supabase.com/library/r/safe-next-path.json" + ], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport {\n useOAuthConsent,\n type OAuthConsentDecision,\n} from '@/registry/default/blocks/oauth-consent/hooks/use-oauth-consent'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nconst getInitial = (value: string) => value.trim().charAt(0).toUpperCase() || '?'\n\ninterface ConsentCardShellProps extends React.ComponentPropsWithoutRef<'div'> {\n clientName: string\n productName: string\n}\n\nfunction ConsentCardShell({\n clientName,\n productName,\n className,\n children,\n ...props\n}: ConsentCardShellProps) {\n return (\n
\n \n \n \n
\n {getInitial(clientName)}\n
\n
\n
\n {getInitial(productName)}\n
\n
\n
\n Authorize {clientName}\n Review what this client gets access to.\n
\n
\n {children}\n
\n
\n )\n}\n\nexport interface OAuthConsentCardProps extends React.ComponentPropsWithoutRef<'div'> {\n clientName: string\n productName?: string\n redirectUri: string\n email: string\n scopes?: string[]\n error?: string | null\n decision?: OAuthConsentDecision | null\n onApprove?: () => void\n onDeny?: () => void\n}\n\nexport function OAuthConsentCard({\n clientName,\n productName = 'Your product',\n redirectUri,\n email,\n scopes = [],\n error = null,\n decision = null,\n onApprove,\n onDeny,\n ...props\n}: OAuthConsentCardProps) {\n return (\n \n
\n
\n
Client
\n
{clientName}
\n
\n
\n
Redirects to
\n
{redirectUri}
\n
\n
\n
Signed in as
\n
{email}
\n
\n {scopes.length > 0 && (\n
\n
Scopes
\n
{scopes.join(', ')}
\n
\n )}\n
\n

\n Allow access only if you recognize this application. It can act on your behalf only within\n the requested permissions and the access you already have.\n

\n {error && (\n

\n {error}\n

\n )}\n
\n \n \n
\n
\n )\n}\n\ninterface OAuthConsentProps extends React.ComponentPropsWithoutRef<'div'> {\n authorizationId?: string | null\n signInPath?: string\n productName?: string\n}\n\nexport function OAuthConsent({\n authorizationId,\n signInPath = '/auth/login',\n productName = 'Your product',\n ...props\n}: OAuthConsentProps) {\n const { details, email, error, isLoading, decision, approve, deny } = useOAuthConsent({\n authorizationId,\n signInPath,\n })\n\n if (isLoading || !details || !email) {\n return (\n \n {isLoading ? (\n

\n Loading authorization request...\n

\n ) : (\n

\n {error ??\n 'Unable to load the authorization request. Start again from your OAuth client.'}\n

\n )}\n
\n )\n }\n\n return (\n void approve()}\n onDeny={() => void deny()}\n {...props}\n />\n )\n}\n", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "content": "import { isAuthSessionMissingError } from '@supabase/supabase-js'\nimport type { OAuthAuthorizationDetails } from '@supabase/supabase-js'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/react/lib/supabase/client'\n\nexport type OAuthConsentDecision = 'approve' | 'deny'\n\nexport interface UseOAuthConsentOptions {\n authorizationId?: string | null\n signInPath?: string\n}\n\nconst withNextParam = (path: string, next: string) => {\n const url = new URL(path, window.location.origin)\n const searchParams = new URLSearchParams(url.search)\n searchParams.set('next', next)\n url.search = searchParams.toString()\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nconst useOAuthConsent = ({\n authorizationId,\n signInPath = '/auth/login',\n}: UseOAuthConsentOptions) => {\n const [details, setDetails] = useState(null)\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(true)\n const [decision, setDecision] = useState(null)\n const isDeciding = useRef(false)\n\n useEffect(() => {\n let active = true\n\n const loadAuthorization = async () => {\n setIsLoading(true)\n setError(null)\n setDetails(null)\n setDecision(null)\n\n if (!authorizationId) {\n setError('This page needs an authorization_id. Start again from your OAuth client.')\n setIsLoading(false)\n return\n }\n\n const supabase = createClient()\n const {\n data: { user },\n error: userError,\n } = await supabase.auth.getUser()\n\n if (userError && !isAuthSessionMissingError(userError)) {\n if (active) {\n setError(userError.message)\n setIsLoading(false)\n }\n return\n }\n\n if (!user) {\n const next = `${window.location.pathname}${window.location.search}`\n if (active) {\n window.location.replace(withNextParam(safeNextPath(signInPath, '/auth/login'), next))\n }\n return\n }\n\n const { data, error } = await supabase.auth.oauth.getAuthorizationDetails(authorizationId)\n if (error) {\n if (active) {\n setError(error.message)\n setIsLoading(false)\n }\n return\n }\n\n if (!('authorization_id' in data)) {\n if (active) {\n window.location.replace(data.redirect_url)\n }\n return\n }\n\n if (active) {\n setDetails(data)\n setIsLoading(false)\n }\n }\n\n void loadAuthorization()\n return () => {\n active = false\n }\n }, [authorizationId, signInPath])\n\n const decide = useCallback(\n async (action: OAuthConsentDecision) => {\n if (!authorizationId || isDeciding.current) return\n\n isDeciding.current = true\n setDecision(action)\n setError(null)\n const supabase = createClient()\n const result =\n action === 'approve'\n ? await supabase.auth.oauth.approveAuthorization(authorizationId, {\n skipBrowserRedirect: true,\n })\n : await supabase.auth.oauth.denyAuthorization(authorizationId, {\n skipBrowserRedirect: true,\n })\n\n if (result.error) {\n setError(result.error.message)\n setDecision(null)\n isDeciding.current = false\n return\n }\n\n if (!result.data?.redirect_url) {\n setError('The server did not return a redirect. Start again from your OAuth client.')\n setDecision(null)\n isDeciding.current = false\n return\n }\n\n window.location.assign(result.data.redirect_url)\n },\n [authorizationId]\n )\n\n return {\n details,\n email: details?.user.email ?? null,\n error,\n isLoading,\n decision,\n approve: () => decide('approve'),\n deny: () => decide('deny'),\n }\n}\n\ntype UseOAuthConsentReturn = ReturnType\n\nexport { useOAuthConsent, type OAuthAuthorizationDetails, type UseOAuthConsentReturn }\n", + "type": "registry:hook" + }, + { + "path": "registry/default/clients/react/lib/supabase/client.ts", + "content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", + "type": "registry:lib" + } + ], + "envVars": { + "VITE_SUPABASE_URL": "", + "VITE_SUPABASE_PUBLISHABLE_KEY": "" + }, + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` โ€” edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```\n\nYou'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." +} \ No newline at end of file diff --git a/apps/ui-library/public/r/oauth-consent-tanstack.json b/apps/ui-library/public/r/oauth-consent-tanstack.json new file mode 100644 index 0000000000000..34cc335915f73 --- /dev/null +++ b/apps/ui-library/public/r/oauth-consent-tanstack.json @@ -0,0 +1,49 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "oauth-consent-tanstack", + "type": "registry:block", + "title": "OAuth Consent for TanStack Start", + "description": "OAuth 2.1 consent screen and route for an existing authenticated application.", + "dependencies": [ + "@supabase/ssr@latest", + "@supabase/supabase-js@latest" + ], + "registryDependencies": [ + "button", + "card", + "https://supabase.com/library/r/safe-next-path.json" + ], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport {\n useOAuthConsent,\n type OAuthConsentDecision,\n} from '@/registry/default/blocks/oauth-consent/hooks/use-oauth-consent'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nconst getInitial = (value: string) => value.trim().charAt(0).toUpperCase() || '?'\n\ninterface ConsentCardShellProps extends React.ComponentPropsWithoutRef<'div'> {\n clientName: string\n productName: string\n}\n\nfunction ConsentCardShell({\n clientName,\n productName,\n className,\n children,\n ...props\n}: ConsentCardShellProps) {\n return (\n
\n \n \n \n
\n {getInitial(clientName)}\n
\n
\n
\n {getInitial(productName)}\n
\n
\n
\n Authorize {clientName}\n Review what this client gets access to.\n
\n
\n {children}\n
\n
\n )\n}\n\nexport interface OAuthConsentCardProps extends React.ComponentPropsWithoutRef<'div'> {\n clientName: string\n productName?: string\n redirectUri: string\n email: string\n scopes?: string[]\n error?: string | null\n decision?: OAuthConsentDecision | null\n onApprove?: () => void\n onDeny?: () => void\n}\n\nexport function OAuthConsentCard({\n clientName,\n productName = 'Your product',\n redirectUri,\n email,\n scopes = [],\n error = null,\n decision = null,\n onApprove,\n onDeny,\n ...props\n}: OAuthConsentCardProps) {\n return (\n \n
\n
\n
Client
\n
{clientName}
\n
\n
\n
Redirects to
\n
{redirectUri}
\n
\n
\n
Signed in as
\n
{email}
\n
\n {scopes.length > 0 && (\n
\n
Scopes
\n
{scopes.join(', ')}
\n
\n )}\n
\n

\n Allow access only if you recognize this application. It can act on your behalf only within\n the requested permissions and the access you already have.\n

\n {error && (\n

\n {error}\n

\n )}\n
\n \n \n
\n
\n )\n}\n\ninterface OAuthConsentProps extends React.ComponentPropsWithoutRef<'div'> {\n authorizationId?: string | null\n signInPath?: string\n productName?: string\n}\n\nexport function OAuthConsent({\n authorizationId,\n signInPath = '/auth/login',\n productName = 'Your product',\n ...props\n}: OAuthConsentProps) {\n const { details, email, error, isLoading, decision, approve, deny } = useOAuthConsent({\n authorizationId,\n signInPath,\n })\n\n if (isLoading || !details || !email) {\n return (\n \n {isLoading ? (\n

\n Loading authorization request...\n

\n ) : (\n

\n {error ??\n 'Unable to load the authorization request. Start again from your OAuth client.'}\n

\n )}\n
\n )\n }\n\n return (\n void approve()}\n onDeny={() => void deny()}\n {...props}\n />\n )\n}\n", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "content": "import { isAuthSessionMissingError } from '@supabase/supabase-js'\nimport type { OAuthAuthorizationDetails } from '@supabase/supabase-js'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\n\nexport type OAuthConsentDecision = 'approve' | 'deny'\n\nexport interface UseOAuthConsentOptions {\n authorizationId?: string | null\n signInPath?: string\n}\n\nconst withNextParam = (path: string, next: string) => {\n const url = new URL(path, window.location.origin)\n const searchParams = new URLSearchParams(url.search)\n searchParams.set('next', next)\n url.search = searchParams.toString()\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nconst useOAuthConsent = ({\n authorizationId,\n signInPath = '/auth/login',\n}: UseOAuthConsentOptions) => {\n const [details, setDetails] = useState(null)\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(true)\n const [decision, setDecision] = useState(null)\n const isDeciding = useRef(false)\n\n useEffect(() => {\n let active = true\n\n const loadAuthorization = async () => {\n setIsLoading(true)\n setError(null)\n setDetails(null)\n setDecision(null)\n\n if (!authorizationId) {\n setError('This page needs an authorization_id. Start again from your OAuth client.')\n setIsLoading(false)\n return\n }\n\n const supabase = createClient()\n const {\n data: { user },\n error: userError,\n } = await supabase.auth.getUser()\n\n if (userError && !isAuthSessionMissingError(userError)) {\n if (active) {\n setError(userError.message)\n setIsLoading(false)\n }\n return\n }\n\n if (!user) {\n const next = `${window.location.pathname}${window.location.search}`\n if (active) {\n window.location.replace(withNextParam(safeNextPath(signInPath, '/auth/login'), next))\n }\n return\n }\n\n const { data, error } = await supabase.auth.oauth.getAuthorizationDetails(authorizationId)\n if (error) {\n if (active) {\n setError(error.message)\n setIsLoading(false)\n }\n return\n }\n\n if (!('authorization_id' in data)) {\n if (active) {\n window.location.replace(data.redirect_url)\n }\n return\n }\n\n if (active) {\n setDetails(data)\n setIsLoading(false)\n }\n }\n\n void loadAuthorization()\n return () => {\n active = false\n }\n }, [authorizationId, signInPath])\n\n const decide = useCallback(\n async (action: OAuthConsentDecision) => {\n if (!authorizationId || isDeciding.current) return\n\n isDeciding.current = true\n setDecision(action)\n setError(null)\n const supabase = createClient()\n const result =\n action === 'approve'\n ? await supabase.auth.oauth.approveAuthorization(authorizationId, {\n skipBrowserRedirect: true,\n })\n : await supabase.auth.oauth.denyAuthorization(authorizationId, {\n skipBrowserRedirect: true,\n })\n\n if (result.error) {\n setError(result.error.message)\n setDecision(null)\n isDeciding.current = false\n return\n }\n\n if (!result.data?.redirect_url) {\n setError('The server did not return a redirect. Start again from your OAuth client.')\n setDecision(null)\n isDeciding.current = false\n return\n }\n\n window.location.assign(result.data.redirect_url)\n },\n [authorizationId]\n )\n\n return {\n details,\n email: details?.user.email ?? null,\n error,\n isLoading,\n decision,\n approve: () => decide('approve'),\n deny: () => decide('deny'),\n }\n}\n\ntype UseOAuthConsentReturn = ReturnType\n\nexport { useOAuthConsent, type OAuthAuthorizationDetails, type UseOAuthConsentReturn }\n", + "type": "registry:hook" + }, + { + "path": "registry/default/blocks/oauth-consent-tanstack/routes/oauth/consent.tsx", + "content": "import { createFileRoute } from '@tanstack/react-router'\n\nimport { OAuthConsent } from '@/registry/default/blocks/oauth-consent/components/oauth-consent'\n\n// This route is installed into the consumer's routes directory. Their TanStack\n// route generator adds it to FileRoutesByPath after installation.\n// @ts-expect-error The local generated route tree does not include this block route.\nexport const Route = createFileRoute('/oauth/consent')({\n component: ConsentPage,\n validateSearch: (search) => ({\n authorization_id: typeof search.authorization_id === 'string' ? search.authorization_id : null,\n }),\n})\n\nfunction ConsentPage() {\n const { authorization_id } = Route.useSearch()\n\n return (\n
\n \n
\n )\n}\n", + "type": "registry:file", + "target": "routes/oauth/consent.tsx" + }, + { + "path": "registry/default/clients/tanstack/lib/supabase/client.ts", + "content": "/// \nimport { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n return createBrowserClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/tanstack/lib/supabase/server.ts", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { getCookies, setCookie } from '@tanstack/react-start/server'\n\nexport function createClient() {\n return createServerClient(\n process.env.VITE_SUPABASE_URL!,\n process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return Object.entries(getCookies()).map(\n ([name, value]) =>\n ({\n name,\n value,\n }) as { name: string; value: string }\n )\n },\n setAll(cookies) {\n cookies.forEach((cookie) => {\n setCookie(cookie.name, cookie.value)\n })\n },\n },\n }\n )\n}\n", + "type": "registry:lib" + } + ], + "envVars": { + "VITE_SUPABASE_URL": "", + "VITE_SUPABASE_PUBLISHABLE_KEY": "" + }, + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` โ€” edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```\n\nYou'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`." +} \ No newline at end of file diff --git a/apps/ui-library/public/r/password-based-auth-nextjs.json b/apps/ui-library/public/r/password-based-auth-nextjs.json index 787fc6ed2f04a..e4cfaeb897f77 100644 --- a/apps/ui-library/public/r/password-based-auth-nextjs.json +++ b/apps/ui-library/public/r/password-based-auth-nextjs.json @@ -12,7 +12,8 @@ "button", "card", "input", - "label" + "label", + "https://supabase.com/library/r/safe-next-path.json" ], "files": [ { @@ -41,7 +42,7 @@ }, { "path": "registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx", - "content": "'use client'\n\nimport { useRouter } from 'next/navigation'\nimport { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport Link from 'next/link'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const router = useRouter()\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n router.push('/protected')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Login\n Enter your email below to login to your account\n \n \n
\n
\n
\n \n setEmail(e.target.value)}\n />\n
\n
\n
\n \n \n Forgot your password?\n \n
\n setPassword(e.target.value)}\n />\n
\n {error &&

{error}

}\n \n
\n
\n Don't have an account?{' '}\n \n Sign up\n \n
\n
\n
\n
\n
\n )\n}\n", + "content": "'use client'\n\nimport { useRouter } from 'next/navigation'\nimport { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\nimport Link from 'next/link'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const router = useRouter()\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n const next = new URLSearchParams(window.location.search).get('next')\n router.push(safeNextPath(next, '/protected'))\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Login\n Enter your email below to login to your account\n \n \n
\n
\n
\n \n setEmail(e.target.value)}\n />\n
\n
\n
\n \n \n Forgot your password?\n \n
\n setPassword(e.target.value)}\n />\n
\n {error &&

{error}

}\n \n
\n
\n Don't have an account?{' '}\n \n Sign up\n \n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:component" }, { @@ -101,7 +102,7 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { diff --git a/apps/ui-library/public/r/password-based-auth-react-router.json b/apps/ui-library/public/r/password-based-auth-react-router.json index 0b3e87063b1cf..1474873cd46bb 100644 --- a/apps/ui-library/public/r/password-based-auth-react-router.json +++ b/apps/ui-library/public/r/password-based-auth-react-router.json @@ -14,7 +14,8 @@ "button", "card", "input", - "label" + "label", + "https://supabase.com/library/r/safe-next-path.json" ], "files": [ { @@ -37,7 +38,7 @@ }, { "path": "registry/default/blocks/password-based-auth-react-router/app/routes/login.tsx", - "content": "import { Link, redirect, useFetcher, type ActionFunctionArgs } from 'react-router'\n\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase, headers } = createClient(request)\n\n const formData = await request.formData()\n\n const email = formData.get('email') as string\n const password = formData.get('password') as string\n\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n\n if (error) {\n return {\n error: error instanceof Error ? error.message : 'An error occurred',\n }\n }\n\n // Update this route to redirect to an authenticated route. The user already has an active session.\n return redirect('/protected', { headers })\n}\n\nexport default function Login() {\n const fetcher = useFetcher()\n\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n
\n
\n
\n \n \n Login\n Enter your email below to login to your account\n \n \n \n
\n
\n \n \n
\n
\n
\n \n \n Forgot your password?\n \n
\n \n
\n {error &&

{error}

}\n \n
\n
\n Don't have an account?{' '}\n \n Sign up\n \n
\n
\n
\n
\n
\n
\n
\n )\n}\n", + "content": "import { Link, redirect, useFetcher, useSearchParams, type ActionFunctionArgs } from 'react-router'\n\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase, headers } = createClient(request)\n const origin = new URL(request.url).origin\n\n const formData = await request.formData()\n\n const email = formData.get('email') as string\n const password = formData.get('password') as string\n\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n\n if (error) {\n return {\n error: error instanceof Error ? error.message : 'An error occurred',\n }\n }\n\n // Update this route to redirect to an authenticated route. The user already has an active session.\n return redirect(safeNextPath(formData.get('next'), '/protected', origin), { headers })\n}\n\nexport default function Login() {\n const fetcher = useFetcher()\n const [searchParams] = useSearchParams()\n\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n
\n
\n
\n \n \n Login\n Enter your email below to login to your account\n \n \n \n \n
\n
\n \n \n
\n
\n
\n \n \n Forgot your password?\n \n
\n \n
\n {error &&

{error}

}\n \n
\n
\n Don't have an account?{' '}\n \n Sign up\n \n
\n
\n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:file", "target": "app/routes/login.tsx" }, diff --git a/apps/ui-library/public/r/password-based-auth-react.json b/apps/ui-library/public/r/password-based-auth-react.json index e9cfb105d24a4..afe60913e0228 100644 --- a/apps/ui-library/public/r/password-based-auth-react.json +++ b/apps/ui-library/public/r/password-based-auth-react.json @@ -11,12 +11,13 @@ "button", "card", "input", - "label" + "label", + "https://supabase.com/library/r/safe-next-path.json" ], "files": [ { "path": "registry/default/blocks/password-based-auth-react/components/login-form.tsx", - "content": "import { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/react/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const supabase = createClient()\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n location.href = '/protected'\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Login\n Enter your email below to login to your account\n \n \n
\n
\n
\n \n setEmail(e.target.value)}\n />\n
\n
\n
\n \n \n Forgot your password?\n \n
\n setPassword(e.target.value)}\n />\n
\n {error &&

{error}

}\n \n
\n
\n Don't have an account?{' '}\n \n Sign up\n \n
\n
\n
\n
\n
\n )\n}\n", + "content": "import { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/react/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const supabase = createClient()\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n const next = new URLSearchParams(window.location.search).get('next')\n location.href = safeNextPath(next, '/protected')\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Login\n Enter your email below to login to your account\n \n \n
\n
\n
\n \n setEmail(e.target.value)}\n />\n
\n
\n
\n \n \n Forgot your password?\n \n
\n setPassword(e.target.value)}\n />\n
\n {error &&

{error}

}\n \n
\n
\n Don't have an account?{' '}\n \n Sign up\n \n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:component" }, { diff --git a/apps/ui-library/public/r/password-based-auth-tanstack.json b/apps/ui-library/public/r/password-based-auth-tanstack.json index 3866d11932877..209f5534c1295 100644 --- a/apps/ui-library/public/r/password-based-auth-tanstack.json +++ b/apps/ui-library/public/r/password-based-auth-tanstack.json @@ -12,7 +12,8 @@ "button", "card", "input", - "label" + "label", + "https://supabase.com/library/r/safe-next-path.json" ], "files": [ { @@ -47,7 +48,7 @@ }, { "path": "registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx", - "content": "import { Link, useNavigate } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n const navigate = useNavigate()\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n // Update this route to redirect to an authenticated route. The user already has an active session.\n await navigate({ to: '/protected' })\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Login\n Enter your email below to login to your account\n \n \n
\n
\n
\n \n setEmail(e.target.value)}\n />\n
\n
\n
\n \n \n Forgot your password?\n \n
\n setPassword(e.target.value)}\n />\n
\n {error &&

{error}

}\n \n
\n
\n Don't have an account?{' '}\n \n Sign up\n \n
\n
\n
\n
\n
\n )\n}\n", + "content": "import { Link } from '@tanstack/react-router'\nimport { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\nimport { Input } from '@/registry/default/components/ui/input'\nimport { Label } from '@/registry/default/components/ui/label'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [email, setEmail] = useState('')\n const [password, setPassword] = useState('')\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithPassword({\n email,\n password,\n })\n if (error) throw error\n // Follow the `next` query parameter if it is a same-origin relative path, e.g. when\n // the OAuth consent screen sent the user here to sign in first. It may point outside\n // the typed route tree, so it needs a full navigation.\n const next = new URLSearchParams(window.location.search).get('next')\n window.location.assign(safeNextPath(next, '/protected'))\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n } finally {\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Login\n Enter your email below to login to your account\n \n \n
\n
\n
\n \n setEmail(e.target.value)}\n />\n
\n
\n
\n \n \n Forgot your password?\n \n
\n setPassword(e.target.value)}\n />\n
\n {error &&

{error}

}\n \n
\n
\n Don't have an account?{' '}\n \n Sign up\n \n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:component" }, { diff --git a/apps/ui-library/public/r/platform-kit-nextjs.json b/apps/ui-library/public/r/platform-kit-nextjs.json index 3cb976b10c3d9..32389fd3e053b 100644 --- a/apps/ui-library/public/r/platform-kit-nextjs.json +++ b/apps/ui-library/public/r/platform-kit-nextjs.json @@ -86,7 +86,7 @@ }, { "path": "registry/default/platform/platform-kit-nextjs/components/supabase-manager/index.tsx", - "content": "'use client'\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport {\n ChevronLeft,\n ChevronRight,\n Database,\n ExternalLink,\n HardDrive,\n KeyRound,\n Lightbulb,\n ScrollText,\n Shield,\n Users,\n} from 'lucide-react'\nimport Link from 'next/link'\nimport { ReactNode, useMemo, useState } from 'react'\n\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogTrigger,\n} from '@/registry/default/components/ui/dialog'\nimport {\n Drawer,\n DrawerContent,\n DrawerHeader,\n DrawerTitle,\n} from '@/registry/default/components/ui/drawer'\nimport {\n HoverCard,\n HoverCardContent,\n HoverCardTrigger,\n} from '@/registry/default/components/ui/hover-card'\nimport { LogoSupabase } from '@/registry/default/platform/platform-kit-nextjs/components/logo-supabase'\nimport { AuthManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/auth'\nimport { DatabaseManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/database'\nimport { LogsManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/logs'\nimport { SecretsManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/secrets'\nimport { StorageManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/storage'\nimport { SuggestionsManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/suggestions'\nimport { UsersManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/users'\nimport {\n SheetNavigationProvider,\n useSheetNavigation,\n} from '@/registry/default/platform/platform-kit-nextjs/contexts/SheetNavigationContext'\n\nconst queryClient = new QueryClient()\n\nfunction DialogView({ projectRef, isMobile }: { projectRef: string; isMobile?: boolean }) {\n const { stack, push, popTo, reset } = useSheetNavigation()\n\n const handleTopLevelNavigation = (title: string, component: ReactNode) => {\n if (stack.length === 1 && stack[0].title === title) {\n return\n }\n reset()\n push({ title, component })\n }\n\n const currentView = stack[stack.length - 1]\n const activeManager = stack.length > 0 ? stack[0].title : null\n\n const navigationItems = useMemo(\n () => [\n {\n title: 'Database',\n icon: Database,\n component: ,\n },\n {\n title: 'Storage',\n icon: HardDrive,\n component: ,\n },\n {\n title: 'Auth',\n icon: Shield,\n component: ,\n },\n {\n title: 'Users',\n icon: Users,\n component: ,\n },\n {\n title: 'Secrets',\n icon: KeyRound,\n component: ,\n },\n {\n title: 'Logs',\n icon: ScrollText,\n component: ,\n },\n {\n title: 'Suggestions',\n icon: Lightbulb,\n component: ,\n },\n ],\n [projectRef]\n )\n\n if (isMobile) {\n return (\n
\n {/* Content Area */}\n
\n
\n {currentView ? (\n currentView.component\n ) : (\n
\n

\n Select a manager from the bottom navigation to get started.\n

\n
\n )}\n
\n
\n\n {/* Bottom Navigation Bar */}\n
\n
\n
\n {navigationItems.map((item) => {\n const Icon = item.icon\n return (\n handleTopLevelNavigation(item.title, item.component)}\n >\n \n \n {item.title === 'Auth' ? 'Auth' : item.title}\n \n \n )\n })}\n
\n
\n
\n
\n )\n }\n\n return (\n
\n {/* Sidebar */}\n
\n
\n

Manage your back-end

\n
\n
\n {navigationItems.map((item) => {\n const Icon = item.icon\n return (\n handleTopLevelNavigation(item.title, item.component)}\n >\n \n {item.title === 'Auth' ? 'Authentication' : item.title}\n \n )\n })}\n
\n
\n \n \n \n \n Open in Supabase\n \n \n \n \n

About Supabase

\n

\n Access powerful back-end tools for database, auth, storage, and logs directly in\n Supabase.\n

\n \n
\n
\n
\n\n {/* Content Area */}\n
\n {/* Header with breadcrumbs */}\n
\n {stack.length > 1 && (\n popTo(stack.length - 2)}\n >\n \n Back\n \n )}\n {/* Breadcrumbs */}\n
\n {stack.map((item: { title: string }, index: number) => (\n
\n {index > 0 && }\n {index === stack.length - 1 ? (\n {item.title}\n ) : (\n \n )}\n
\n ))}\n
\n
\n\n
\n {currentView ? (\n currentView.component\n ) : (\n
\n

\n Select a manager from the sidebar to get started.\n

\n
\n )}\n
\n
\n
\n )\n}\n\nexport default function SupabaseManagerDialog({\n projectRef,\n open,\n onOpenChange,\n isMobile,\n}: {\n projectRef: string\n open: boolean\n onOpenChange: (open: boolean) => void\n isMobile: boolean\n}) {\n const content = (\n {}}\n initialStack={[\n {\n title: 'Database',\n component: ,\n },\n ]}\n >\n \n \n )\n\n if (!isMobile) {\n return (\n \n \n \n Manage your back-end\n {content}\n \n \n \n )\n }\n\n return (\n \n \n \n \n Manage your back-end\n \n {content}\n \n \n \n )\n}\n", + "content": "'use client'\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport {\n ChevronLeft,\n ChevronRight,\n Database,\n ExternalLink,\n HardDrive,\n KeyRound,\n Lightbulb,\n ScrollText,\n Shield,\n Users,\n} from 'lucide-react'\nimport Link from 'next/link'\nimport { ReactNode, useMemo, useState } from 'react'\n\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n DialogTrigger,\n} from '@/registry/default/components/ui/dialog'\nimport {\n Drawer,\n DrawerContent,\n DrawerHeader,\n DrawerTitle,\n} from '@/registry/default/components/ui/drawer'\nimport {\n HoverCard,\n HoverCardContent,\n HoverCardTrigger,\n} from '@/registry/default/components/ui/hover-card'\nimport { LogoSupabase } from '@/registry/default/platform/platform-kit-nextjs/components/logo-supabase'\nimport { AuthManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/auth'\nimport { DatabaseManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/database'\nimport { LogsManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/logs'\nimport { SecretsManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/secrets'\nimport { StorageManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/storage'\nimport { SuggestionsManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/suggestions'\nimport { UsersManager } from '@/registry/default/platform/platform-kit-nextjs/components/supabase-manager/users'\nimport {\n SheetNavigationProvider,\n useSheetNavigation,\n} from '@/registry/default/platform/platform-kit-nextjs/contexts/SheetNavigationContext'\n\nconst queryClient = new QueryClient()\n\nfunction DialogView({ projectRef, isMobile }: { projectRef: string; isMobile?: boolean }) {\n const { stack, push, popTo, reset } = useSheetNavigation()\n\n const handleTopLevelNavigation = (title: string, component: ReactNode) => {\n if (stack.length === 1 && stack[0].title === title) {\n return\n }\n reset()\n push({ title, component })\n }\n\n const currentView = stack[stack.length - 1]\n const activeManager = stack.length > 0 ? stack[0].title : null\n\n const navigationItems = useMemo(\n () => [\n {\n title: 'Database',\n icon: Database,\n component: ,\n },\n {\n title: 'Storage',\n icon: HardDrive,\n component: ,\n },\n {\n title: 'Auth',\n icon: Shield,\n component: ,\n },\n {\n title: 'Users',\n icon: Users,\n component: ,\n },\n {\n title: 'Secrets',\n icon: KeyRound,\n component: ,\n },\n {\n title: 'Logs',\n icon: ScrollText,\n component: ,\n },\n {\n title: 'Suggestions',\n icon: Lightbulb,\n component: ,\n },\n ],\n [projectRef]\n )\n\n if (isMobile) {\n return (\n
\n {/* Content Area */}\n
\n
\n {currentView ? (\n currentView.component\n ) : (\n
\n

\n Select a manager from the bottom navigation to get started.\n

\n
\n )}\n
\n
\n\n {/* Bottom Navigation Bar */}\n
\n
\n
\n {navigationItems.map((item) => {\n const Icon = item.icon\n return (\n handleTopLevelNavigation(item.title, item.component)}\n >\n \n \n {item.title === 'Auth' ? 'Auth' : item.title}\n \n \n )\n })}\n
\n
\n
\n
\n )\n }\n\n return (\n
\n {/* Sidebar */}\n
\n
\n

Manage your back-end

\n
\n
\n {navigationItems.map((item) => {\n const Icon = item.icon\n return (\n handleTopLevelNavigation(item.title, item.component)}\n >\n \n {item.title === 'Auth' ? 'Authentication' : item.title}\n \n )\n })}\n
\n
\n \n \n \n \n Open in Supabase\n \n \n \n \n

About Supabase

\n

\n Access powerful back-end tools for database, auth, storage, and logs directly in\n Supabase.\n

\n \n
\n
\n
\n\n {/* Content Area */}\n
\n {/* Header with breadcrumbs */}\n
\n {stack.length > 1 && (\n popTo(stack.length - 2)}\n >\n \n Back\n \n )}\n {/* Breadcrumbs */}\n
\n {stack.map((item: { title: string }, index: number) => (\n
\n {index > 0 && }\n {index === stack.length - 1 ? (\n {item.title}\n ) : (\n popTo(index)}\n className=\"hover:underline\"\n >\n {item.title}\n \n )}\n
\n ))}\n
\n
\n\n
\n {currentView ? (\n currentView.component\n ) : (\n
\n

\n Select a manager from the sidebar to get started.\n

\n
\n )}\n
\n
\n
\n )\n}\n\nexport default function SupabaseManagerDialog({\n projectRef,\n open,\n onOpenChange,\n isMobile,\n}: {\n projectRef: string\n open: boolean\n onOpenChange: (open: boolean) => void\n isMobile: boolean\n}) {\n const content = (\n {}}\n initialStack={[\n {\n title: 'Database',\n component: ,\n },\n ]}\n >\n \n \n )\n\n if (!isMobile) {\n return (\n \n \n \n Manage your back-end\n {content}\n \n \n \n )\n }\n\n return (\n \n \n \n \n Manage your back-end\n \n {content}\n \n \n \n )\n}\n", "type": "registry:component" }, { diff --git a/apps/ui-library/public/r/realtime-avatar-stack-nextjs.json b/apps/ui-library/public/r/realtime-avatar-stack-nextjs.json index dfbca00e99927..2338c698d3d62 100644 --- a/apps/ui-library/public/r/realtime-avatar-stack-nextjs.json +++ b/apps/ui-library/public/r/realtime-avatar-stack-nextjs.json @@ -45,7 +45,7 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { diff --git a/apps/ui-library/public/r/realtime-chat-nextjs.json b/apps/ui-library/public/r/realtime-chat-nextjs.json index 72385d8c17fb8..43b0298b6e420 100644 --- a/apps/ui-library/public/r/realtime-chat-nextjs.json +++ b/apps/ui-library/public/r/realtime-chat-nextjs.json @@ -41,7 +41,7 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { diff --git a/apps/ui-library/public/r/realtime-cursor-nextjs.json b/apps/ui-library/public/r/realtime-cursor-nextjs.json index 10d19899a200c..06ee4bd35f827 100644 --- a/apps/ui-library/public/r/realtime-cursor-nextjs.json +++ b/apps/ui-library/public/r/realtime-cursor-nextjs.json @@ -33,7 +33,7 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { diff --git a/apps/ui-library/public/r/realtime-flow-nextjs.json b/apps/ui-library/public/r/realtime-flow-nextjs.json index 7249b12d55a79..58a4f65ccc91b 100644 --- a/apps/ui-library/public/r/realtime-flow-nextjs.json +++ b/apps/ui-library/public/r/realtime-flow-nextjs.json @@ -31,7 +31,7 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { diff --git a/apps/ui-library/public/r/realtime-monaco-nextjs.json b/apps/ui-library/public/r/realtime-monaco-nextjs.json index cf141d4690bc2..11d0760c1ee74 100644 --- a/apps/ui-library/public/r/realtime-monaco-nextjs.json +++ b/apps/ui-library/public/r/realtime-monaco-nextjs.json @@ -30,7 +30,7 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { diff --git a/apps/ui-library/public/r/registry.json b/apps/ui-library/public/r/registry.json index 6968fddc64a05..4afb6f79ca526 100644 --- a/apps/ui-library/public/r/registry.json +++ b/apps/ui-library/public/r/registry.json @@ -3,6 +3,22 @@ "name": "Supabase Library", "homepage": "https://supabase.com/library", "items": [ + { + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "safe-next-path", + "type": "registry:lib", + "title": "Safe next path", + "description": "Validates same-origin paths before redirecting.", + "registryDependencies": [], + "dependencies": [], + "files": [ + { + "path": "registry/default/blocks/safe-next-path/lib/safe-next-path.ts", + "type": "registry:lib", + "target": "lib/safe-next-path.ts" + } + ] + }, { "name": "password-based-auth-nextjs", "type": "registry:block", @@ -12,7 +28,8 @@ "button", "card", "input", - "label" + "label", + "https://supabase.com/library/r/safe-next-path.json" ], "dependencies": [ "@supabase/ssr@latest", @@ -112,7 +129,8 @@ "button", "card", "input", - "label" + "label", + "https://supabase.com/library/r/safe-next-path.json" ], "files": [ { @@ -154,7 +172,8 @@ "button", "card", "input", - "label" + "label", + "https://supabase.com/library/r/safe-next-path.json" ], "dependencies": [ "@supabase/ssr@latest", @@ -232,7 +251,8 @@ "button", "card", "input", - "label" + "label", + "https://supabase.com/library/r/safe-next-path.json" ], "dependencies": [ "@supabase/ssr@latest", @@ -326,7 +346,8 @@ "description": "Social Auth flow for Nextjs and Supabase", "registryDependencies": [ "button", - "card" + "card", + "https://supabase.com/library/r/safe-next-path.json" ], "dependencies": [ "@supabase/ssr@latest", @@ -420,7 +441,8 @@ "description": "Social Auth flow for React Router and Supabase", "registryDependencies": [ "button", - "card" + "card", + "https://supabase.com/library/r/safe-next-path.json" ], "dependencies": [ "@supabase/ssr@latest", @@ -481,7 +503,8 @@ "description": "Social Auth flow for TanStack and Supabase", "registryDependencies": [ "button", - "card" + "card", + "https://supabase.com/library/r/safe-next-path.json" ], "dependencies": [ "@supabase/ssr@latest", @@ -1723,6 +1746,172 @@ } ] }, + { + "name": "oauth-consent-nextjs", + "type": "registry:block", + "title": "OAuth Consent for Next.js", + "description": "OAuth 2.1 consent screen and route for an existing authenticated application.", + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` โ€” edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```\n\nYou'll need to set the following environment variables in your project: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`.", + "registryDependencies": [ + "button", + "card", + "https://supabase.com/library/r/safe-next-path.json" + ], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "type": "registry:hook" + }, + { + "path": "registry/default/blocks/oauth-consent-nextjs/app/oauth/consent/page.tsx", + "type": "registry:page", + "target": "app/oauth/consent/page.tsx" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/client.ts", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/nextjs/lib/supabase/server.ts", + "type": "registry:lib" + } + ], + "dependencies": [ + "@supabase/ssr@latest", + "@supabase/supabase-js@latest" + ], + "envVars": { + "NEXT_PUBLIC_SUPABASE_URL": "", + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY": "" + } + }, + { + "name": "oauth-consent-react", + "type": "registry:block", + "title": "OAuth Consent for React", + "description": "OAuth 2.1 consent component for an existing authenticated application.", + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` โ€” edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```\n\nYou'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", + "registryDependencies": [ + "button", + "card", + "https://supabase.com/library/r/safe-next-path.json" + ], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "type": "registry:hook" + }, + { + "path": "registry/default/clients/react/lib/supabase/client.ts", + "type": "registry:lib" + } + ], + "dependencies": [ + "@supabase/supabase-js@latest" + ], + "envVars": { + "VITE_SUPABASE_URL": "", + "VITE_SUPABASE_PUBLISHABLE_KEY": "" + } + }, + { + "name": "oauth-consent-react-router", + "type": "registry:block", + "title": "OAuth Consent for React Router", + "description": "OAuth 2.1 consent screen and route for an existing authenticated application.", + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` โ€” edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```\n\nYou'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", + "registryDependencies": [ + "button", + "card", + "https://supabase.com/library/r/safe-next-path.json" + ], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "type": "registry:hook" + }, + { + "path": "registry/default/blocks/oauth-consent-react-router/app/routes/oauth.consent.tsx", + "type": "registry:file", + "target": "app/routes/oauth.consent.tsx" + }, + { + "path": "registry/default/clients/react-router/lib/supabase/client.ts", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/react-router/lib/supabase/server.ts", + "type": "registry:lib" + } + ], + "dependencies": [ + "@supabase/ssr@latest", + "@supabase/supabase-js@latest" + ], + "envVars": { + "VITE_SUPABASE_URL": "", + "VITE_SUPABASE_PUBLISHABLE_KEY": "" + } + }, + { + "name": "oauth-consent-tanstack", + "type": "registry:block", + "title": "OAuth Consent for TanStack Start", + "description": "OAuth 2.1 consent screen and route for an existing authenticated application.", + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` โ€” edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```\n\nYou'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.", + "registryDependencies": [ + "button", + "card", + "https://supabase.com/library/r/safe-next-path.json" + ], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "type": "registry:hook" + }, + { + "path": "registry/default/blocks/oauth-consent-tanstack/routes/oauth/consent.tsx", + "type": "registry:file", + "target": "routes/oauth/consent.tsx" + }, + { + "path": "registry/default/clients/tanstack/lib/supabase/client.ts", + "type": "registry:lib" + }, + { + "path": "registry/default/clients/tanstack/lib/supabase/server.ts", + "type": "registry:lib" + } + ], + "dependencies": [ + "@supabase/ssr@latest", + "@supabase/supabase-js@latest" + ], + "envVars": { + "VITE_SUPABASE_URL": "", + "VITE_SUPABASE_PUBLISHABLE_KEY": "" + } + }, { "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "tanstack-db-nextjs", @@ -2651,4 +2840,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/apps/ui-library/public/r/safe-next-path.json b/apps/ui-library/public/r/safe-next-path.json new file mode 100644 index 0000000000000..441364e56d0cc --- /dev/null +++ b/apps/ui-library/public/r/safe-next-path.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "safe-next-path", + "type": "registry:lib", + "title": "Safe next path", + "description": "Validates same-origin paths before redirecting.", + "dependencies": [], + "registryDependencies": [], + "files": [ + { + "path": "registry/default/blocks/safe-next-path/lib/safe-next-path.ts", + "content": "export const safeNextPath = (path: unknown, fallback = '/', origin?: string) => {\n if (typeof path !== 'string' || !path.startsWith('/')) return fallback\n\n const currentOrigin = origin ?? window.location.origin\n\n try {\n const url = new URL(path, currentOrigin)\n return url.origin === currentOrigin ? `${url.pathname}${url.search}${url.hash}` : fallback\n } catch {\n return fallback\n }\n}\n", + "type": "registry:lib", + "target": "lib/safe-next-path.ts" + } + ] +} \ No newline at end of file diff --git a/apps/ui-library/public/r/social-auth-nextjs.json b/apps/ui-library/public/r/social-auth-nextjs.json index 6463dd1b85eab..a49a2280c0e86 100644 --- a/apps/ui-library/public/r/social-auth-nextjs.json +++ b/apps/ui-library/public/r/social-auth-nextjs.json @@ -10,7 +10,8 @@ ], "registryDependencies": [ "button", - "card" + "card", + "https://supabase.com/library/r/safe-next-path.json" ], "files": [ { @@ -39,7 +40,7 @@ }, { "path": "registry/default/blocks/social-auth-nextjs/components/login-form.tsx", - "content": "'use client'\n\nimport { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${window.location.origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (error) throw error\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n
\n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n )\n}\n", + "content": "'use client'\n\nimport { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const next = new URLSearchParams(window.location.search).get('next')\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${window.location.origin}/auth/oauth?next=${encodeURIComponent(safeNextPath(next, '/protected'))}`,\n },\n })\n\n if (error) throw error\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n
\n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:component" }, { @@ -60,7 +61,7 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { diff --git a/apps/ui-library/public/r/social-auth-react-router.json b/apps/ui-library/public/r/social-auth-react-router.json index b6d80cfa45945..3956d8fe86b29 100644 --- a/apps/ui-library/public/r/social-auth-react-router.json +++ b/apps/ui-library/public/r/social-auth-react-router.json @@ -12,7 +12,8 @@ ], "registryDependencies": [ "button", - "card" + "card", + "https://supabase.com/library/r/safe-next-path.json" ], "files": [ { @@ -29,7 +30,7 @@ }, { "path": "registry/default/blocks/social-auth-react-router/app/routes/login.tsx", - "content": "import { redirect, useFetcher, type ActionFunctionArgs } from 'react-router'\n\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase } = createClient(request)\n const origin = new URL(request.url).origin\n\n const { data, error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (data.url) {\n return redirect(data.url)\n }\n\n if (error) {\n return {\n error: error instanceof Error ? error.message : 'An error occurred',\n }\n }\n}\n\nexport default function Login() {\n const fetcher = useFetcher()\n\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n
\n
\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n \n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n
\n
\n )\n}\n", + "content": "import { redirect, useFetcher, useSearchParams, type ActionFunctionArgs } from 'react-router'\n\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nexport const action = async ({ request }: ActionFunctionArgs) => {\n const { supabase, headers } = createClient(request)\n const origin = new URL(request.url).origin\n\n const formData = await request.formData()\n const next = safeNextPath(formData.get('next'), '/protected', origin)\n\n const { data, error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${origin}/auth/oauth?next=${encodeURIComponent(next)}`,\n },\n })\n\n if (data.url) {\n return redirect(data.url, { headers })\n }\n\n if (error) {\n return {\n error: error instanceof Error ? error.message : 'An error occurred',\n }\n }\n}\n\nexport default function Login() {\n const fetcher = useFetcher()\n const [searchParams] = useSearchParams()\n\n const error = fetcher.data?.error\n const loading = fetcher.state === 'submitting'\n\n return (\n
\n
\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n \n \n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:file", "target": "app/routes/login.tsx" }, diff --git a/apps/ui-library/public/r/social-auth-tanstack.json b/apps/ui-library/public/r/social-auth-tanstack.json index 4dddd3c74694a..6f5aae7f630dd 100644 --- a/apps/ui-library/public/r/social-auth-tanstack.json +++ b/apps/ui-library/public/r/social-auth-tanstack.json @@ -10,12 +10,13 @@ ], "registryDependencies": [ "button", - "card" + "card", + "https://supabase.com/library/r/safe-next-path.json" ], "files": [ { "path": "registry/default/blocks/social-auth-tanstack/components/login-form.tsx", - "content": "import { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${window.location.origin}/auth/oauth?next=/protected`,\n },\n })\n\n if (error) throw error\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n
\n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n )\n}\n", + "content": "import { useState } from 'react'\n\nimport { cn } from '@/lib/utils'\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nexport function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {\n const [error, setError] = useState(null)\n const [isLoading, setIsLoading] = useState(false)\n\n const handleSocialLogin = async (e: React.FormEvent) => {\n e.preventDefault()\n const supabase = createClient()\n setIsLoading(true)\n setError(null)\n\n try {\n const next = new URLSearchParams(window.location.search).get('next')\n const { error } = await supabase.auth.signInWithOAuth({\n provider: 'github',\n options: {\n redirectTo: `${window.location.origin}/auth/confirm?next=${encodeURIComponent(safeNextPath(next, '/protected'))}`,\n },\n })\n\n if (error) throw error\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : 'An error occurred')\n setIsLoading(false)\n }\n }\n\n return (\n
\n \n \n Welcome!\n Sign in to your account to continue\n \n \n
\n
\n {error &&

{error}

}\n \n
\n
\n
\n
\n
\n )\n}\n", "type": "registry:component" }, { diff --git a/apps/ui-library/public/r/supabase-client-nextjs.json b/apps/ui-library/public/r/supabase-client-nextjs.json index c7925d9f8d7ce..5e8fba07ebe33 100644 --- a/apps/ui-library/public/r/supabase-client-nextjs.json +++ b/apps/ui-library/public/r/supabase-client-nextjs.json @@ -17,7 +17,7 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { diff --git a/apps/ui-library/public/r/tanstack-db-nextjs.json b/apps/ui-library/public/r/tanstack-db-nextjs.json index aafa10ab5d047..af5db958533ff 100644 --- a/apps/ui-library/public/r/tanstack-db-nextjs.json +++ b/apps/ui-library/public/r/tanstack-db-nextjs.json @@ -26,7 +26,7 @@ }, { "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts", - "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth')\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", + "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n let supabaseResponse = NextResponse.next({\n request,\n })\n\n // With Fluid compute, don't put this client in a global environment\n // variable. Always create a new one on each request.\n const supabase = createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n {\n cookies: {\n getAll() {\n return request.cookies.getAll()\n },\n setAll(cookiesToSet) {\n cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n supabaseResponse = NextResponse.next({\n request,\n })\n cookiesToSet.forEach(({ name, value, options }) =>\n supabaseResponse.cookies.set(name, value, options)\n )\n },\n },\n }\n )\n\n // Do not run code between createServerClient and\n // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n // issues with users being randomly logged out.\n\n // IMPORTANT: If you remove getClaims() and you use server-side rendering\n // with the Supabase client, your users may be randomly logged out.\n const { data } = await supabase.auth.getClaims()\n const user = data?.claims\n\n if (\n !user &&\n !request.nextUrl.pathname.startsWith('/login') &&\n !request.nextUrl.pathname.startsWith('/auth') &&\n // the OAuth consent route sends unauthenticated visitors to the login page\n // itself, so that it can preserve the authorization in the `next` parameter\n request.nextUrl.pathname !== '/oauth/consent'\n ) {\n // no user, potentially respond by redirecting the user to the login page\n const url = request.nextUrl.clone()\n url.pathname = '/auth/login'\n return NextResponse.redirect(url)\n }\n\n // IMPORTANT: You *must* return the supabaseResponse object as it is.\n // If you're creating a new response object with NextResponse.next() make sure to:\n // 1. Pass the request in it, like so:\n // const myNewResponse = NextResponse.next({ request })\n // 2. Copy over the cookies, like so:\n // myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n // 3. Change the myNewResponse object to fit your needs, but avoid changing\n // the cookies!\n // 4. Finally:\n // return myNewResponse\n // If this is not done, you may be causing the browser and server to go out\n // of sync and terminate the user's session prematurely!\n\n return supabaseResponse\n}\n", "type": "registry:lib" }, { diff --git a/apps/ui-library/registry/blocks.ts b/apps/ui-library/registry/blocks.ts index 4be38f7085a58..f1c045df99922 100644 --- a/apps/ui-library/registry/blocks.ts +++ b/apps/ui-library/registry/blocks.ts @@ -4,6 +4,10 @@ import { clients } from './clients' import currentUserAvatar from './default/blocks/current-user-avatar/registry-item.json' with { type: 'json' } import dropzone from './default/blocks/dropzone/registry-item.json' with { type: 'json' } import infiniteQueryHook from './default/blocks/infinite-query-hook/registry-item.json' with { type: 'json' } +import oauthConsentNextjs from './default/blocks/oauth-consent-nextjs/registry-item.json' with { type: 'json' } +import oauthConsentReactRouter from './default/blocks/oauth-consent-react-router/registry-item.json' with { type: 'json' } +import oauthConsentReact from './default/blocks/oauth-consent-react/registry-item.json' with { type: 'json' } +import oauthConsentTanstack from './default/blocks/oauth-consent-tanstack/registry-item.json' with { type: 'json' } import passwordBasedAuthNextjs from './default/blocks/password-based-auth-nextjs/registry-item.json' with { type: 'json' } import passwordBasedAuthReactRouter from './default/blocks/password-based-auth-react-router/registry-item.json' with { type: 'json' } import passwordBasedAuthReact from './default/blocks/password-based-auth-react/registry-item.json' with { type: 'json' } @@ -13,6 +17,7 @@ import realtimeChat from './default/blocks/realtime-chat/registry-item.json' wit import realtimeCursor from './default/blocks/realtime-cursor/registry-item.json' with { type: 'json' } import realtimeFlow from './default/blocks/realtime-flow/registry-item.json' with { type: 'json' } import realtimeMonaco from './default/blocks/realtime-monaco/registry-item.json' with { type: 'json' } +import safeNextPath from './default/blocks/safe-next-path/registry-item.json' with { type: 'json' } import socialAuthNextjs from './default/blocks/social-auth-nextjs/registry-item.json' with { type: 'json' } import socialAuthReactRouter from './default/blocks/social-auth-react-router/registry-item.json' with { type: 'json' } import socialAuthReact from './default/blocks/social-auth-react/registry-item.json' with { type: 'json' } @@ -32,12 +37,19 @@ const combine = (component: RegistryItem) => { }) } +const withClientAndDocs = (component: RegistryItem, client: RegistryItem) => ({ + ...registryItemAppend(component, [client]), + docs: [component.docs, client.docs].filter(Boolean).join('\n\n'), +}) + const nextjsClient = clients.find((client) => client.name === 'supabase-client-nextjs') const reactClient = clients.find((client) => client.name === 'supabase-client-react') const tanstackClient = clients.find((client) => client.name === 'supabase-client-tanstack') const reactRouterClient = clients.find((client) => client.name === 'supabase-client-react-router') export const blocks = [ + safeNextPath as RegistryItem, + registryItemAppend(passwordBasedAuthNextjs as RegistryItem, [nextjsClient!]), registryItemAppend(passwordBasedAuthReact as RegistryItem, [reactClient!]), registryItemAppend(passwordBasedAuthReactRouter as RegistryItem, [reactRouterClient!]), @@ -58,6 +70,11 @@ export const blocks = [ // infinite query hook is intentionally not combined with the clients since it depends on clients having database types. infiniteQueryHook as RegistryItem, + withClientAndDocs(oauthConsentNextjs as RegistryItem, nextjsClient!), + withClientAndDocs(oauthConsentReact as RegistryItem, reactClient!), + withClientAndDocs(oauthConsentReactRouter as RegistryItem, reactRouterClient!), + withClientAndDocs(oauthConsentTanstack as RegistryItem, tanstackClient!), + // tanstack-db is served dynamically via API route, but we register it here for the static build registryItemAppend(tanstackDbNextjs as RegistryItem, [nextjsClient!]), ] as RegistryItem[] diff --git a/apps/ui-library/registry/default/blocks/oauth-consent-nextjs/app/oauth/consent/page.tsx b/apps/ui-library/registry/default/blocks/oauth-consent-nextjs/app/oauth/consent/page.tsx new file mode 100644 index 0000000000000..444f8b75d800f --- /dev/null +++ b/apps/ui-library/registry/default/blocks/oauth-consent-nextjs/app/oauth/consent/page.tsx @@ -0,0 +1,15 @@ +import { OAuthConsent } from '@/registry/default/blocks/oauth-consent/components/oauth-consent' + +export default async function ConsentPage({ + searchParams, +}: { + searchParams: Promise<{ authorization_id?: string }> +}) { + const { authorization_id } = await searchParams + + return ( +
+ +
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/oauth-consent-nextjs/registry-item.json b/apps/ui-library/registry/default/blocks/oauth-consent-nextjs/registry-item.json new file mode 100644 index 0000000000000..9eb440eed246c --- /dev/null +++ b/apps/ui-library/registry/default/blocks/oauth-consent-nextjs/registry-item.json @@ -0,0 +1,23 @@ +{ + "name": "oauth-consent-nextjs", + "type": "registry:block", + "title": "OAuth Consent for Next.js", + "description": "OAuth 2.1 consent screen and route for an existing authenticated application.", + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` \u2014 edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```", + "registryDependencies": ["button", "card", "https://supabase.com/library/r/safe-next-path.json"], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "type": "registry:hook" + }, + { + "path": "registry/default/blocks/oauth-consent-nextjs/app/oauth/consent/page.tsx", + "type": "registry:page", + "target": "app/oauth/consent/page.tsx" + } + ] +} diff --git a/apps/ui-library/registry/default/blocks/oauth-consent-react-router/app/routes/oauth.consent.tsx b/apps/ui-library/registry/default/blocks/oauth-consent-react-router/app/routes/oauth.consent.tsx new file mode 100644 index 0000000000000..444a8f4a177cb --- /dev/null +++ b/apps/ui-library/registry/default/blocks/oauth-consent-react-router/app/routes/oauth.consent.tsx @@ -0,0 +1,16 @@ +import { useSearchParams } from 'react-router' + +import { OAuthConsent } from '@/registry/default/blocks/oauth-consent/components/oauth-consent' + +export default function ConsentRoute() { + const [searchParams] = useSearchParams() + + return ( +
+ +
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/oauth-consent-react-router/registry-item.json b/apps/ui-library/registry/default/blocks/oauth-consent-react-router/registry-item.json new file mode 100644 index 0000000000000..aafde11087346 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/oauth-consent-react-router/registry-item.json @@ -0,0 +1,23 @@ +{ + "name": "oauth-consent-react-router", + "type": "registry:block", + "title": "OAuth Consent for React Router", + "description": "OAuth 2.1 consent screen and route for an existing authenticated application.", + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` \u2014 edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```", + "registryDependencies": ["button", "card", "https://supabase.com/library/r/safe-next-path.json"], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "type": "registry:hook" + }, + { + "path": "registry/default/blocks/oauth-consent-react-router/app/routes/oauth.consent.tsx", + "type": "registry:file", + "target": "app/routes/oauth.consent.tsx" + } + ] +} diff --git a/apps/ui-library/registry/default/blocks/oauth-consent-react/registry-item.json b/apps/ui-library/registry/default/blocks/oauth-consent-react/registry-item.json new file mode 100644 index 0000000000000..40c3f2cd74e47 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/oauth-consent-react/registry-item.json @@ -0,0 +1,18 @@ +{ + "name": "oauth-consent-react", + "type": "registry:block", + "title": "OAuth Consent for React", + "description": "OAuth 2.1 consent component for an existing authenticated application.", + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` \u2014 edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```", + "registryDependencies": ["button", "card", "https://supabase.com/library/r/safe-next-path.json"], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "type": "registry:hook" + } + ] +} diff --git a/apps/ui-library/registry/default/blocks/oauth-consent-tanstack/registry-item.json b/apps/ui-library/registry/default/blocks/oauth-consent-tanstack/registry-item.json new file mode 100644 index 0000000000000..f531763b62b30 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/oauth-consent-tanstack/registry-item.json @@ -0,0 +1,23 @@ +{ + "name": "oauth-consent-tanstack", + "type": "registry:block", + "title": "OAuth Consent for TanStack Start", + "description": "OAuth 2.1 consent screen and route for an existing authenticated application.", + "docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` \u2014 edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```", + "registryDependencies": ["button", "card", "https://supabase.com/library/r/safe-next-path.json"], + "files": [ + { + "path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx", + "type": "registry:component" + }, + { + "path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts", + "type": "registry:hook" + }, + { + "path": "registry/default/blocks/oauth-consent-tanstack/routes/oauth/consent.tsx", + "type": "registry:file", + "target": "routes/oauth/consent.tsx" + } + ] +} diff --git a/apps/ui-library/registry/default/blocks/oauth-consent-tanstack/routes/oauth/consent.tsx b/apps/ui-library/registry/default/blocks/oauth-consent-tanstack/routes/oauth/consent.tsx new file mode 100644 index 0000000000000..ba795f2395570 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/oauth-consent-tanstack/routes/oauth/consent.tsx @@ -0,0 +1,23 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { OAuthConsent } from '@/registry/default/blocks/oauth-consent/components/oauth-consent' + +// This route is installed into the consumer's routes directory. Their TanStack +// route generator adds it to FileRoutesByPath after installation. +// @ts-expect-error The local generated route tree does not include this block route. +export const Route = createFileRoute('/oauth/consent')({ + component: ConsentPage, + validateSearch: (search) => ({ + authorization_id: typeof search.authorization_id === 'string' ? search.authorization_id : null, + }), +}) + +function ConsentPage() { + const { authorization_id } = Route.useSearch() + + return ( +
+ +
+ ) +} diff --git a/apps/ui-library/registry/default/blocks/oauth-consent/components/oauth-consent.tsx b/apps/ui-library/registry/default/blocks/oauth-consent/components/oauth-consent.tsx new file mode 100644 index 0000000000000..c143f515c778c --- /dev/null +++ b/apps/ui-library/registry/default/blocks/oauth-consent/components/oauth-consent.tsx @@ -0,0 +1,173 @@ +'use client' + +import { cn } from '@/lib/utils' +import { + useOAuthConsent, + type OAuthConsentDecision, +} from '@/registry/default/blocks/oauth-consent/hooks/use-oauth-consent' +import { Button } from '@/registry/default/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/registry/default/components/ui/card' + +const getInitial = (value: string) => value.trim().charAt(0).toUpperCase() || '?' + +interface ConsentCardShellProps extends React.ComponentPropsWithoutRef<'div'> { + clientName: string + productName: string +} + +function ConsentCardShell({ + clientName, + productName, + className, + children, + ...props +}: ConsentCardShellProps) { + return ( +
+ + +
+
+ {getInitial(clientName)} +
+ +
+ Authorize {clientName} + Review what this client gets access to. +
+ + {children} + +
+ ) +} + +export interface OAuthConsentCardProps extends React.ComponentPropsWithoutRef<'div'> { + clientName: string + productName?: string + redirectUri: string + email: string + scopes?: string[] + error?: string | null + decision?: OAuthConsentDecision | null + onApprove?: () => void + onDeny?: () => void +} + +export function OAuthConsentCard({ + clientName, + productName = 'Your product', + redirectUri, + email, + scopes = [], + error = null, + decision = null, + onApprove, + onDeny, + ...props +}: OAuthConsentCardProps) { + return ( + +
+
+
Client
+
{clientName}
+
+
+
Redirects to
+
{redirectUri}
+
+
+
Signed in as
+
{email}
+
+ {scopes.length > 0 && ( +
+
Scopes
+
{scopes.join(', ')}
+
+ )} +
+

+ Allow access only if you recognize this application. It can act on your behalf only within + the requested permissions and the access you already have. +

+ {error && ( +

+ {error} +

+ )} +
+ + +
+
+ ) +} + +interface OAuthConsentProps extends React.ComponentPropsWithoutRef<'div'> { + authorizationId?: string | null + signInPath?: string + productName?: string +} + +export function OAuthConsent({ + authorizationId, + signInPath = '/auth/login', + productName = 'Your product', + ...props +}: OAuthConsentProps) { + const { details, email, error, isLoading, decision, approve, deny } = useOAuthConsent({ + authorizationId, + signInPath, + }) + + if (isLoading || !details || !email) { + return ( + + {isLoading ? ( +

+ Loading authorization request... +

+ ) : ( +

+ {error ?? + 'Unable to load the authorization request. Start again from your OAuth client.'} +

+ )} +
+ ) + } + + return ( + void approve()} + onDeny={() => void deny()} + {...props} + /> + ) +} diff --git a/apps/ui-library/registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts b/apps/ui-library/registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts new file mode 100644 index 0000000000000..337381b6c7402 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts @@ -0,0 +1,147 @@ +import { isAuthSessionMissingError } from '@supabase/supabase-js' +import type { OAuthAuthorizationDetails } from '@supabase/supabase-js' +import { useCallback, useEffect, useRef, useState } from 'react' + +import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path' +import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client' + +export type OAuthConsentDecision = 'approve' | 'deny' + +export interface UseOAuthConsentOptions { + authorizationId?: string | null + signInPath?: string +} + +const withNextParam = (path: string, next: string) => { + const url = new URL(path, window.location.origin) + const searchParams = new URLSearchParams(url.search) + searchParams.set('next', next) + url.search = searchParams.toString() + return `${url.pathname}${url.search}${url.hash}` +} + +const useOAuthConsent = ({ + authorizationId, + signInPath = '/auth/login', +}: UseOAuthConsentOptions) => { + const [details, setDetails] = useState(null) + const [error, setError] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [decision, setDecision] = useState(null) + const isDeciding = useRef(false) + + useEffect(() => { + let active = true + + const loadAuthorization = async () => { + setIsLoading(true) + setError(null) + setDetails(null) + setDecision(null) + + if (!authorizationId) { + setError('This page needs an authorization_id. Start again from your OAuth client.') + setIsLoading(false) + return + } + + const supabase = createClient() + const { + data: { user }, + error: userError, + } = await supabase.auth.getUser() + + if (userError && !isAuthSessionMissingError(userError)) { + if (active) { + setError(userError.message) + setIsLoading(false) + } + return + } + + if (!user) { + const next = `${window.location.pathname}${window.location.search}` + if (active) { + window.location.replace(withNextParam(safeNextPath(signInPath, '/auth/login'), next)) + } + return + } + + const { data, error } = await supabase.auth.oauth.getAuthorizationDetails(authorizationId) + if (error) { + if (active) { + setError(error.message) + setIsLoading(false) + } + return + } + + if (!('authorization_id' in data)) { + if (active) { + window.location.replace(data.redirect_url) + } + return + } + + if (active) { + setDetails(data) + setIsLoading(false) + } + } + + void loadAuthorization() + return () => { + active = false + } + }, [authorizationId, signInPath]) + + const decide = useCallback( + async (action: OAuthConsentDecision) => { + if (!authorizationId || isDeciding.current) return + + isDeciding.current = true + setDecision(action) + setError(null) + const supabase = createClient() + const result = + action === 'approve' + ? await supabase.auth.oauth.approveAuthorization(authorizationId, { + skipBrowserRedirect: true, + }) + : await supabase.auth.oauth.denyAuthorization(authorizationId, { + skipBrowserRedirect: true, + }) + + if (result.error) { + setError(result.error.message) + setDecision(null) + isDeciding.current = false + return + } + + if (!result.data?.redirect_url) { + setError('The server did not return a redirect. Start again from your OAuth client.') + setDecision(null) + isDeciding.current = false + return + } + + window.location.assign(result.data.redirect_url) + }, + [authorizationId] + ) + + return { + details, + email: details?.user.email ?? null, + error, + isLoading, + decision, + approve: () => decide('approve'), + deny: () => decide('deny'), + } +} + +type UseOAuthConsentReturn = ReturnType + +export { useOAuthConsent, type OAuthAuthorizationDetails, type UseOAuthConsentReturn } diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx index 30dc196709339..39bc9640d7bb2 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx +++ b/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/components/login-form.tsx @@ -4,6 +4,7 @@ import { useRouter } from 'next/navigation' import { useState } from 'react' import { cn } from '@/lib/utils' +import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path' import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client' import { Button } from '@/registry/default/components/ui/button' import { @@ -37,7 +38,8 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe }) if (error) throw error // Update this route to redirect to an authenticated route. The user already has an active session. - router.push('/example/password-based-auth/protected') + const next = new URLSearchParams(window.location.search).get('next') + router.push(safeNextPath(next, '/example/password-based-auth/protected')) } catch (error: unknown) { setError(error instanceof Error ? error.message : 'An error occurred') } finally { diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/registry-item.json b/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/registry-item.json index 2203774c8dc53..f0a593ff84fec 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/registry-item.json +++ b/apps/ui-library/registry/default/blocks/password-based-auth-nextjs/registry-item.json @@ -3,7 +3,13 @@ "type": "registry:block", "title": "Password Based Auth flow for Nextjs and Supabase", "description": "Password Based Auth flow for Nextjs and Supabase", - "registryDependencies": ["button", "card", "input", "label"], + "registryDependencies": [ + "button", + "card", + "input", + "label", + "https://supabase.com/library/r/safe-next-path.json" + ], "dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"], "files": [ { diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/login.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/login.tsx index 04ff0dc42861b..15d9defc3429f 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/login.tsx +++ b/apps/ui-library/registry/default/blocks/password-based-auth-react-router/app/routes/login.tsx @@ -1,5 +1,6 @@ -import { Link, redirect, useFetcher, type ActionFunctionArgs } from 'react-router' +import { Link, redirect, useFetcher, useSearchParams, type ActionFunctionArgs } from 'react-router' +import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path' import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server' import { Button } from '@/registry/default/components/ui/button' import { @@ -14,6 +15,7 @@ import { Label } from '@/registry/default/components/ui/label' export const action = async ({ request }: ActionFunctionArgs) => { const { supabase, headers } = createClient(request) + const origin = new URL(request.url).origin const formData = await request.formData() @@ -32,11 +34,12 @@ export const action = async ({ request }: ActionFunctionArgs) => { } // Update this route to redirect to an authenticated route. The user already has an active session. - return redirect('/protected', { headers }) + return redirect(safeNextPath(formData.get('next'), '/protected', origin), { headers }) } export default function Login() { const fetcher = useFetcher() + const [searchParams] = useSearchParams() const error = fetcher.data?.error const loading = fetcher.state === 'submitting' @@ -52,6 +55,7 @@ export default function Login() {
+
diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-react-router/registry-item.json b/apps/ui-library/registry/default/blocks/password-based-auth-react-router/registry-item.json index be88ea4890485..b0ac01f4c0d71 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-react-router/registry-item.json +++ b/apps/ui-library/registry/default/blocks/password-based-auth-react-router/registry-item.json @@ -3,7 +3,13 @@ "type": "registry:block", "title": "Password Based Auth flow for React Router and Supabase", "description": "Password Based Auth flow for React Router and Supabase", - "registryDependencies": ["button", "card", "input", "label"], + "registryDependencies": [ + "button", + "card", + "input", + "label", + "https://supabase.com/library/r/safe-next-path.json" + ], "dependencies": [ "@supabase/ssr@latest", "@react-router/dev@latest", diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-react/components/login-form.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-react/components/login-form.tsx index 89575682af283..9b0c71b95cf18 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-react/components/login-form.tsx +++ b/apps/ui-library/registry/default/blocks/password-based-auth-react/components/login-form.tsx @@ -1,6 +1,7 @@ import { useState } from 'react' import { cn } from '@/lib/utils' +import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path' import { createClient } from '@/registry/default/clients/react/lib/supabase/client' import { Button } from '@/registry/default/components/ui/button' import { @@ -32,7 +33,8 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe }) if (error) throw error // Update this route to redirect to an authenticated route. The user already has an active session. - location.href = '/protected' + const next = new URLSearchParams(window.location.search).get('next') + location.href = safeNextPath(next, '/protected') } catch (error: unknown) { setError(error instanceof Error ? error.message : 'An error occurred') } finally { diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-react/registry-item.json b/apps/ui-library/registry/default/blocks/password-based-auth-react/registry-item.json index 459510f698ed6..73b2bb342a86f 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-react/registry-item.json +++ b/apps/ui-library/registry/default/blocks/password-based-auth-react/registry-item.json @@ -3,7 +3,13 @@ "type": "registry:block", "title": "Password Based Auth flow for React and Supabase", "description": "Password Based Auth flow for React and Supabase", - "registryDependencies": ["button", "card", "input", "label"], + "registryDependencies": [ + "button", + "card", + "input", + "label", + "https://supabase.com/library/r/safe-next-path.json" + ], "files": [ { "path": "registry/default/blocks/password-based-auth-react/components/login-form.tsx", diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx index cfb46c7b53eb5..00824c9fad808 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx @@ -1,7 +1,8 @@ -import { Link, useNavigate } from '@tanstack/react-router' +import { Link } from '@tanstack/react-router' import { useState } from 'react' import { cn } from '@/lib/utils' +import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path' import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client' import { Button } from '@/registry/default/components/ui/button' import { @@ -19,7 +20,6 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe const [password, setPassword] = useState('') const [error, setError] = useState(null) const [isLoading, setIsLoading] = useState(false) - const navigate = useNavigate() const handleLogin = async (e: React.FormEvent) => { e.preventDefault() @@ -33,8 +33,11 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe password, }) if (error) throw error - // Update this route to redirect to an authenticated route. The user already has an active session. - await navigate({ to: '/protected' }) + // Follow the `next` query parameter if it is a same-origin relative path, e.g. when + // the OAuth consent screen sent the user here to sign in first. It may point outside + // the typed route tree, so it needs a full navigation. + const next = new URLSearchParams(window.location.search).get('next') + window.location.assign(safeNextPath(next, '/protected')) } catch (error: unknown) { setError(error instanceof Error ? error.message : 'An error occurred') } finally { diff --git a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/registry-item.json b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/registry-item.json index eb78910ed8a1f..43f4673a848f9 100644 --- a/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/registry-item.json +++ b/apps/ui-library/registry/default/blocks/password-based-auth-tanstack/registry-item.json @@ -3,7 +3,13 @@ "type": "registry:block", "title": "Password Based Auth flow for tanstack and Supabase", "description": "Password Based Auth flow for tanstack and Supabase", - "registryDependencies": ["button", "card", "input", "label"], + "registryDependencies": [ + "button", + "card", + "input", + "label", + "https://supabase.com/library/r/safe-next-path.json" + ], "dependencies": ["@supabase/ssr@latest"], "files": [ { diff --git a/apps/ui-library/registry/default/blocks/safe-next-path/lib/safe-next-path.ts b/apps/ui-library/registry/default/blocks/safe-next-path/lib/safe-next-path.ts new file mode 100644 index 0000000000000..f727da65643b9 --- /dev/null +++ b/apps/ui-library/registry/default/blocks/safe-next-path/lib/safe-next-path.ts @@ -0,0 +1,12 @@ +export const safeNextPath = (path: unknown, fallback = '/', origin?: string) => { + if (typeof path !== 'string' || !path.startsWith('/')) return fallback + + const currentOrigin = origin ?? window.location.origin + + try { + const url = new URL(path, currentOrigin) + return url.origin === currentOrigin ? `${url.pathname}${url.search}${url.hash}` : fallback + } catch { + return fallback + } +} diff --git a/apps/ui-library/registry/default/blocks/safe-next-path/registry-item.json b/apps/ui-library/registry/default/blocks/safe-next-path/registry-item.json new file mode 100644 index 0000000000000..130e136101e7e --- /dev/null +++ b/apps/ui-library/registry/default/blocks/safe-next-path/registry-item.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "safe-next-path", + "type": "registry:lib", + "title": "Safe next path", + "description": "Validates same-origin paths before redirecting.", + "registryDependencies": [], + "dependencies": [], + "files": [ + { + "path": "registry/default/blocks/safe-next-path/lib/safe-next-path.ts", + "type": "registry:lib", + "target": "lib/safe-next-path.ts" + } + ] +} diff --git a/apps/ui-library/registry/default/blocks/social-auth-nextjs/components/login-form.tsx b/apps/ui-library/registry/default/blocks/social-auth-nextjs/components/login-form.tsx index 9b12133009ff5..fb8c80893c26b 100644 --- a/apps/ui-library/registry/default/blocks/social-auth-nextjs/components/login-form.tsx +++ b/apps/ui-library/registry/default/blocks/social-auth-nextjs/components/login-form.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { cn } from '@/lib/utils' +import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path' import { createClient } from '@/registry/default/clients/nextjs/lib/supabase/client' import { Button } from '@/registry/default/components/ui/button' import { @@ -24,10 +25,11 @@ export function LoginForm({ className, ...props }: React.ComponentPropsWithoutRe setError(null) try { + const next = new URLSearchParams(window.location.search).get('next') const { error } = await supabase.auth.signInWithOAuth({ provider: 'github', options: { - redirectTo: `${window.location.origin}/auth/oauth?next=/protected`, + redirectTo: `${window.location.origin}/auth/oauth?next=${encodeURIComponent(safeNextPath(next, '/protected'))}`, }, }) diff --git a/apps/ui-library/registry/default/blocks/social-auth-nextjs/registry-item.json b/apps/ui-library/registry/default/blocks/social-auth-nextjs/registry-item.json index c37a59023ea5d..d3b16a1fdc2b5 100644 --- a/apps/ui-library/registry/default/blocks/social-auth-nextjs/registry-item.json +++ b/apps/ui-library/registry/default/blocks/social-auth-nextjs/registry-item.json @@ -3,7 +3,7 @@ "type": "registry:block", "title": "Social Auth flow for Nextjs and Supabase", "description": "Social Auth flow for Nextjs and Supabase", - "registryDependencies": ["button", "card"], + "registryDependencies": ["button", "card", "https://supabase.com/library/r/safe-next-path.json"], "dependencies": ["@supabase/ssr@latest", "@supabase/supabase-js@latest"], "files": [ { diff --git a/apps/ui-library/registry/default/blocks/social-auth-react-router/app/routes/login.tsx b/apps/ui-library/registry/default/blocks/social-auth-react-router/app/routes/login.tsx index 9ad26a0bb4ab6..70ff49bc99c98 100644 --- a/apps/ui-library/registry/default/blocks/social-auth-react-router/app/routes/login.tsx +++ b/apps/ui-library/registry/default/blocks/social-auth-react-router/app/routes/login.tsx @@ -1,5 +1,6 @@ -import { redirect, useFetcher, type ActionFunctionArgs } from 'react-router' +import { redirect, useFetcher, useSearchParams, type ActionFunctionArgs } from 'react-router' +import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path' import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server' import { Button } from '@/registry/default/components/ui/button' import { @@ -11,18 +12,21 @@ import { } from '@/registry/default/components/ui/card' export const action = async ({ request }: ActionFunctionArgs) => { - const { supabase } = createClient(request) + const { supabase, headers } = createClient(request) const origin = new URL(request.url).origin + const formData = await request.formData() + const next = safeNextPath(formData.get('next'), '/protected', origin) + const { data, error } = await supabase.auth.signInWithOAuth({ provider: 'github', options: { - redirectTo: `${origin}/auth/oauth?next=/protected`, + redirectTo: `${origin}/auth/oauth?next=${encodeURIComponent(next)}`, }, }) if (data.url) { - return redirect(data.url) + return redirect(data.url, { headers }) } if (error) { @@ -34,6 +38,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { export default function Login() { const fetcher = useFetcher() + const [searchParams] = useSearchParams() const error = fetcher.data?.error const loading = fetcher.state === 'submitting' @@ -49,6 +54,7 @@ export default function Login() { +
{error &&

{error}

}