Merge branch 'master' into feat/deep-dark

This commit is contained in:
Jonathan Summers-Muir committed 2024-07-30 00:40:31 +08:00
commit 2aeb6790d2
1414 files changed
+81912 -29508

No files matched your search

+2 -1
View File
@@ -3,5 +3,6 @@
/apps/studio/ @supabase/Dashboard
/apps/www/ @supabase/Website
/apps/www/_blog
/apps/www/_blog @supabase/marketing
/apps/www/public/images/blog @supabase/marketing
/docker/ @supabase/cli
+23 -43
View File
@@ -5,61 +5,41 @@ on:
jobs:
check-external:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.is-member.outputs.result }}
permissions:
issues: write
steps:
- name: Get deps
run: |
sudo apt-get update && sudo apt-get install -y jq
- name: Get org members
id: is-member
env:
GH_ORG: supabase
GH_USER: ${{ github.event.issue.user.login }}
GH_TOKEN: ${{ secrets.SB_ORG_MEMBERSHIP_PAT }}
run: |
gh api /orgs/supabase/members/$GH_USER
- if: failure()
name: is-member
run: |
echo "User is not a member of Supabase org"
echo "result=false" >> "$GITHUB_OUTPUT"
- if: success()
run: |
echo "User is a member of Supabase org"
echo "result=true" >> "$GITHUB_OUTPUT"
- name: Label contributor
id: label-contributor
if: ${{ steps.is-member.outputs.result == 'false' }}
- name: Check if organization member
id: is-org-member
uses: JamesSingleton/is-organization-member@1.0.0
with:
organization: ${{ github.repository_owner }}
username: ${{ github.event.issue.user.login }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: label-member
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ github.event.issue.user.login }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
LABELS: external-issue
run: |
echo "applying external label"
gh issue edit $NUMBER --add-label $LABELS
- name: Label internal
id: label-internal
if: ${{ steps.is-member.outputs.result == 'true' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ github.event.issue.user.login }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
LABELS: internal-issue
run: |
echo "applying internal label"
gh issue edit $NUMBER --add-label $LABELS
if [ ${{ steps.is-org-member.outputs.result }} != "true" ]; then
echo "User is outside of organization, labeling external"
gh issue edit "$NUMBER" --add-label "external-issue"
else
echo "User is within the organization, labeling internal"
gh issue edit "$NUMBER" --add-label "internal-issue"
fi
triage-new:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Label triage
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
LABELS: to-triage
run: |
echo "applying triage label"
gh issue edit $NUMBER --add-label $LABELS
echo "Applying triage label for new issue"
gh issue edit "$NUMBER" --add-label "$LABELS"
+4 -1
View File
@@ -1,4 +1,4 @@
name: Check code with TypeScript
name: Check code with TypeScript & Lint
on:
pull_request:
@@ -30,3 +30,6 @@ jobs:
- name: Run TypeScript type check
run: npx turbo run typecheck
- name: Run Lint
run: npx turbo run lint
+2 -1
View File
@@ -1,2 +1,3 @@
^./i18n
^./packages/api-types
^./packages/api-types
^./apps/www/lib/redirects.js
+9 -9
View File
@@ -120,6 +120,15 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Storage" target="_blank" rel="noopener noreferrer">storage-swift</a></td>
<td><a href="https://github.com/supabase/supabase-swift/tree/main/Sources/Functions" target="_blank" rel="noopener noreferrer">functions-swift</a></td>
</tr>
<tr>
<td>Python</td>
<td><a href="https://github.com/supabase/supabase-py" target="_blank" rel="noopener noreferrer">supabase-py</a></td>
<td><a href="https://github.com/supabase/postgrest-py" target="_blank" rel="noopener noreferrer">postgrest-py</a></td>
<td><a href="https://github.com/supabase/gotrue-py" target="_blank" rel="noopener noreferrer">gotrue-py</a></td>
<td><a href="https://github.com/supabase/realtime-py" target="_blank" rel="noopener noreferrer">realtime-py</a></td>
<td><a href="https://github.com/supabase/storage-py" target="_blank" rel="noopener noreferrer">storage-py</a></td>
<td><a href="https://github.com/supabase/functions-py" target="_blank" rel="noopener noreferrer">functions-py</a></td>
</tr>
<!-- /notranslate -->
<th colspan="7">💚 Community 💚</th>
<!-- notranslate -->
@@ -159,15 +168,6 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Storage" target="_blank" rel="noopener noreferrer">storage-kt</a></td>
<td><a href="https://github.com/supabase-community/supabase-kt/tree/master/Functions" target="_blank" rel="noopener noreferrer">functions-kt</a></td>
</tr>
<tr>
<td>Python</td>
<td><a href="https://github.com/supabase-community/supabase-py" target="_blank" rel="noopener noreferrer">supabase-py</a></td>
<td><a href="https://github.com/supabase-community/postgrest-py" target="_blank" rel="noopener noreferrer">postgrest-py</a></td>
<td><a href="https://github.com/supabase-community/gotrue-py" target="_blank" rel="noopener noreferrer">gotrue-py</a></td>
<td><a href="https://github.com/supabase-community/realtime-py" target="_blank" rel="noopener noreferrer">realtime-py</a></td>
<td><a href="https://github.com/supabase-community/storage-py" target="_blank" rel="noopener noreferrer">storage-py</a></td>
<td><a href="https://github.com/supabase-community/functions-py" target="_blank" rel="noopener noreferrer">functions-py</a></td>
</tr>
<tr>
<td>Ruby</td>
<td><a href="https://github.com/supabase-community/supabase-rb" target="_blank" rel="noopener noreferrer">supabase-rb</a></td>
+1 -1
View File
@@ -40,6 +40,6 @@
"autoprefixer": "^10.4.14",
"nextjs-node-loader": "^1.1.5",
"postcss": "^8.4.31",
"typescript": "^5.4.3"
"typescript": "^5.5.0"
}
}
+154
View File
@@ -489,6 +489,94 @@ export const Index: Record<string, any> = {
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",
@@ -1072,6 +1160,39 @@ export const Index: Record<string, any> = {
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: []
},
"switch-demo": {
name: "switch-demo",
type: "components:example",
@@ -1831,6 +1952,28 @@ export const Index: Record<string, any> = {
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",
@@ -1864,5 +2007,16 @@ export const Index: Record<string, any> = {
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: []
},
},
}
+9
View File
@@ -0,0 +1,9 @@
'use client'
import { useConfig } from '@/hooks/use-config'
import { SonnerToaster as Toaster } from 'ui'
export function SonnerToaster() {
const [config] = useConfig()
return <Toaster position={config.sonnerPosition} expand={config.sonnerExpand} />
}
+6 -10
View File
@@ -2,11 +2,13 @@ import '@/styles/globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import { ThemeProvider } from './Providers'
import { Toaster } from 'ui'
import { SonnerToaster } from './SonnerToast'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
title: 'Supabase Design System',
description: 'Generated by create next app',
}
@@ -15,22 +17,16 @@ interface RootLayoutProps {
}
export default async function Layout({ children }: RootLayoutProps) {
// console.log('Root Layout mounted or re-rendered')
return (
<html
// className="dark"
lang="en"
// data-theme="dark"
// style={{ colorScheme: 'dark' }}
suppressHydrationWarning
>
<html lang="en" suppressHydrationWarning>
<head />
<body className={inter.className}>
<ThemeProvider themes={['dark', 'light', 'deep-dark']} defaultTheme="system" enableSystem>
<div vaul-drawer-wrapper="">
<div className="relative flex min-h-screen flex-col bg-background">{children}</div>
</div>
<Toaster />
<SonnerToaster />
</ThemeProvider>
</body>
</html>
@@ -1,19 +0,0 @@
'use client'
import SVG from 'react-inlinesvg'
import { useTheme } from 'next-themes'
function DesignSystemMarks() {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme?.includes('dark')
return (
<SVG
className="h-4 w-auto"
src={`${process.env.NEXT_PUBLIC_BASE_PATH}/img/design-system-marks/design-system-marks--${isDark ? 'dark' : 'light'}.svg`}
/>
)
}
export { DesignSystemMarks }
@@ -2,14 +2,15 @@
import SVG from 'react-inlinesvg'
import { useTheme } from 'next-themes'
import { cn } from 'ui'
const HomepageSvgHandler = ({ name }: { name: string }) => {
const HomepageSvgHandler = ({ name, className }: { name: string; className?: string }) => {
const { resolvedTheme } = useTheme()
return (
<div>
<SVG
className="h-32 w-auto"
className={cn('h-32 w-auto', className)}
src={`${process.env.NEXT_PUBLIC_BASE_PATH}/img/design-system-marks/${name}--${resolvedTheme}.svg`}
/>
</div>
@@ -44,6 +44,8 @@ import { Icons } from '@/components/icons'
import { ThemeSettings } from '@/components/theme-settings'
import { CodeFragment } from '@/components/code-fragment'
import { Admonition } from 'ui-patterns/admonition'
import { SonnerExpandConfig } from './sonner-expand-config'
import { SonnerPositionConfig } from './sonner-expand-position'
const components = {
Accordion,
@@ -273,6 +275,8 @@ const components = {
ThemeSettings,
CodeFragment,
Admonition,
SonnerExpandConfig,
SonnerPositionConfig,
}
interface MdxProps {
@@ -0,0 +1,37 @@
import { useConfig } from '@/hooks/use-config'
import { Switch } from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
function SonnerExpandConfig() {
const [config, setConfig] = useConfig()
return (
<div className="px-5 py-5 border rounded-lg my-2 bg-surface-75">
<form>
<FormItemLayout
name="sonnerExpand"
id="sonnerExpand"
isReactForm={false}
label="Use expand prop"
description="You will need to fire a few Sonner toasts first"
layout="flex"
>
<Switch
name="sonnerExpand"
id="sonnerExpand"
size="large"
checked={config.sonnerExpand}
onCheckedChange={(e) =>
setConfig({
...config,
sonnerExpand: e,
})
}
/>
</FormItemLayout>
</form>
</div>
)
}
export { SonnerExpandConfig }
@@ -0,0 +1,70 @@
import { useConfig } from '@/hooks/use-config'
import { ComponentProps } from 'react'
import {
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectLabel_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Select_Shadcn_,
SonnerToaster,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
function SonnerPositionConfig() {
const [config, setConfig] = useConfig()
type Position = NonNullable<ComponentProps<typeof SonnerToaster>['position']>
const positions: Position[] = [
'top-left',
'top-right',
'bottom-left',
'bottom-right',
'top-center',
'bottom-center',
]
return (
<div className="px-5 py-5 border rounded-lg my-2 bg-surface-75">
<form>
<FormItemLayout
name="sonnerExpand"
id="sonnerExpand"
isReactForm={false}
label="Use position prop"
description="You will need to fire a few Sonner toasts first"
layout="flex"
>
<Select_Shadcn_
value={config.sonnerPosition}
defaultValue={config.sonnerPosition}
onValueChange={(e: Position) => {
setConfig({
...config,
sonnerPosition: e,
})
}}
>
<SelectTrigger_Shadcn_ className="w-[180px]">
<SelectValue_Shadcn_ placeholder="Select a position" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
<SelectLabel_Shadcn_>Positions available</SelectLabel_Shadcn_>
{positions.map((position) => (
<SelectItem_Shadcn_ key={position} value={position}>
{position}
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
</form>
</div>
)
}
export { SonnerPositionConfig }
@@ -1,9 +1,8 @@
// import { docsConfig } from '@/config/docs'
import Link from 'next/link'
import { DesignSystemMarks } from './design-system-marks'
import { ThemeSwitcherDropdown } from './theme-switcher-dropdown'
import { TopNavigationSearch } from './top-navigation-search'
import { CommandMenu } from './command-menu'
import { HomepageSvgHandler } from './homepage-svg-handler'
import { ThemeSwitcherDropdown } from './theme-switcher-dropdown'
function TopNavigation() {
return (
@@ -15,7 +14,7 @@ function TopNavigation() {
<Link href="/">
<h1>Supabase Design System</h1>
</Link>
<DesignSystemMarks />
<HomepageSvgHandler name="design-system-marks" className="h-4 w-auto" />
</div>
{/* {docsConfig.mainNav.map((section) => (
<>
+15
View File
@@ -82,6 +82,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/fragments/admonition',
items: [],
},
{
title: 'Assistant Chat',
href: '/docs/fragments/assistant-chat',
items: [],
},
{
title: 'Modal',
href: '/docs/fragments/modal',
@@ -193,6 +198,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/components/command',
items: [],
},
{
title: 'Command Menu (cmdk)',
href: '/docs/components/commandmenu',
items: [],
},
{
title: 'Context Menu',
href: '/docs/components/context-menu',
@@ -374,6 +384,11 @@ export const docsConfig: DocsConfig = {
href: '/docs/components/tree-view',
items: [],
},
{
title: 'Expanding Textarea',
href: '/docs/components/expanding-textarea',
items: [],
},
],
},
],
@@ -4,12 +4,47 @@ description: Latest updates and announcements.
toc: false
---
## 12th June 2024 - Button styles updated
- [`<Button />`](/design-system/docs/components/button) styles updated
[PR](https://github.com/supabase/supabase/pull/27055)
## 5th June 2024 - More Modal and Dialog examples
[PR](https://github.com/supabase/supabase/pull/26844)
## 3rd June 2024 - Add scroll example to NavigationMenu
- [Added scroll example](https://supabase-design-system.vercel.app/design-system/docs/components/navigation-menu#with-horizontal-scroll) to [`<NavigationMenu />`](/design-system/docs/components/navigation-menu)
## 25th May 2024 - TreeView component added
- [`<TreeView />`](/design-system/docs/components/tree-view) component added.
[PR](https://github.com/supabase/supabase/pull/26821)
## 24th May 2024 - FormItemLayout updated, and InfoTooltip added
- [`<FormItemLayout />`](/design-system/docs/fragments/form-item-layout) now supports new props, `BeforeLabel` and `AfterLabel`
- [`<InfoTooltip />`](/design-system/docs/fragments/info-tooltip) added to easily add information tooltips
[PR](https://github.com/supabase/supabase/pull/26712)
## 24th May 2024 - Added Admonition component
- [Admonition](/design-system/docs/fragments/admonition)
[PR](https://github.com/supabase/supabase/pull/26710)
## 24th May 2024 - New components and docs
### New components added
- [Multi Select](/design-system/components/multi-select)
[PR](https://github.com/supabase/supabase/pull/26719)
### New components documented
- [Radio Group Card](/design-system/docs/components/radio-group-card)
@@ -0,0 +1,111 @@
---
title: Command Menu (cmdk)
description: A central command menu that acts as a control center with searchable actions.
component: true
---
The example below uses `cmd-j` as the shortcut instead of `cmd-k` to avoid clashing with this site's native command menu.
<ComponentPreview name="commandmenu-demo" peekCode wide />
## Usage
```tsx
import {
CommandInput,
CommandList,
CommandMenu,
CommandMenuTrigger as CommandMenuTriggerPrimitive,
CommandProvider,
useRegisterCommands,
} from 'ui-patterns/CommandMenu'
```
```tsx
function Commands() {
useRegisterCommands('Action commands', [
{
id: 'alert',
name: 'Alert',
action: () => alert('You triggered a command'),
},
])
useRegisterCommands('Route commands', [
{
id: 'supabase-website',
name: 'Go to Supabase website',
route: 'https://supabase.com',
},
])
return null
}
function CommandMenuTrigger() {
return (
<CommandMenuTriggerPrimitive>
<Button>Open command menu</Button>
</CommandMenuTriggerPrimitive>
)
}
export default function CommandMenuDemo() {
return (
<CommandProvider openKey="j">
<Commands />
<CommandMenu trigger={<CommandMenuTrigger />}>
<CommandInput />
<CommandList />
</CommandMenu>
</CommandProvider>
)
}
```
## Examples
To avoid keyboard shortcuts clashing wildly, the open shortcut is disabled for the following examples. Use the button to open the example previews.
### With experimental badge
You can add an experimental badge to any command item:
<ComponentPreview name="commandmenu-badge" />
### With icon
You can add an icon to any command item:
<ComponentPreview name="commandmenu-icon" />
### With default hidden
You can hide any command menu item unless it matches an active search query. Try searching for `open sesame` here:
<ComponentPreview name="commandmenu-hidden" />
### With force-mount
You can force-mount any command item (force it to always show, regardless of search query). You can also force-mount an entire section.
<ComponentPreview name="commandmenu-force" />
### With conditional commands
You can use the dependencies (`deps`) array on the register command options to change the registered commands in response to state changes.
Dependency equality is calculated using the `lodash` [isEqual](https://lodash.com/docs/4.17.15#isEqual) function, so you may need suitable use of `useCallback` or `useMemo` to maintain or break equality.
<ComponentPreview name="commandmenu-conditional" />
### With subpage
You can define a subpage of commands:
<ComponentPreview name="commandmenu-subpage" />
### With arbitrary subpage
You can define an arbitrary subpage that loads a custom component:
<ComponentPreview name="commandmenu-subpage-custom" />
@@ -0,0 +1,25 @@
---
title: Expanding Textarea
description: Displays a textarea which autoexpands (or shrinks) based on its content.
component: true
source:
shadcn: true
---
<ComponentPreview name="expanding-textarea-demo" peekCode wide />
## Usage
```tsx
import { ExpandingTextArea } from 'ui'
```
```tsx
<ExpandingTextArea />
```
## Examples
### Default
<ComponentPreview name="expanding-textarea-demo" peekCode wide />
@@ -16,46 +16,6 @@ Sonner is built and maintained by [emilkowalski\_](https://twitter.com/emilkowal
## Installation
<Tabs defaultValue="cli">
<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">
<Steps>
<Step>Run the following command:</Step>
```bash
npx shadcn-ui@latest add sonner
```
<Step>Add the Toaster component</Step>
```tsx title="app/layout.tsx" {1,9}
import { Toaster } from '@/components/ui/sonner'
export default function RootLayout({ children }) {
return (
<html lang="en">
<head />
<body>
<main>{children}</main>
<Toaster />
</body>
</html>
)
}
```
</Steps>
</TabsContent>
<TabsContent value="manual">
<Steps>
<Step>Install the following dependencies:</Step>
@@ -88,10 +48,6 @@ export default function RootLayout({ children }) {
</Steps>
</TabsContent>
</Tabs>
## Usage
```tsx
@@ -101,3 +57,41 @@ import { toast } from 'sonner'
```tsx
toast('Event has been created.')
```
## Expand
You can change the amount of toasts visible through the visibleToasts prop.
```tsx
<Toaster expand={true | false} />
```
You can use the Input below to try it out.
<SonnerExpandConfig />
## Position
You can change the amount of toasts visible through the visibleToasts prop.
```tsx
<Toaster
position={
'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center'
}
/>
```
You can use the Input below to try it out.
<SonnerPositionConfig />
## Types
You can customize the type of toast you want to render, and pass an options object as the second argument.
<ComponentPreview name="sonner-types" />
## Upload example
<ComponentPreview name="sonner-upload" />
@@ -0,0 +1,52 @@
---
title: Assistant Chat
description: Bespoke Assistant chat text area, with support for a commands Popover.
component: true
---
<ComponentPreview name="assistant-chat-demo" peekCode wide />
## Usage
```tsx
import { AssistantChatForm } from 'ui-patterns/AssistantChat/AssistantChatForm'
```
```tsx
<AssistantChatForm
icon={<Box strokeWidth={1.5} size={24} className="text-foreground-muted" />}
value={value}
loading={loading}
disabled={loading}
onValueChange={(e) => setValueState(e.target.value)}
onSubmit={async (event) => {
event.preventDefault()
handleSubmit(event)
}}
/>
```
## Commands
The Assistant Chat support a commands Popover that can be used to display a list of available commands.
It requires a useState and a ref though for it to work correctly.
This is so that the input is still in focus, and the user can still interact with the Popover with the keyboard.
```tsx
const [commandsOpen, setCommandsOpen] = useState<boolean>(false)
const textAreaRef = createRef<HTMLTextAreaElement>()
<AssistantCommandPopover
open={commandsOpen}
setOpen={setCommandsOpen}
textAreaRef={textAreaRef}
>
<AssistantChatForm
commandsOpen={commandsOpen}
setCommandsOpen={setCommandsOpen}
/>
</AssistantCommandPopover>
```
<ComponentPreview name="assistant-chat-commands" />
+10 -5
View File
@@ -1,22 +1,27 @@
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
// import { BackendProvider } from '@/registry/backend-provider'
import { Style } from '@/registry/styles'
import { Theme } from '@/registry/themes'
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { ComponentProps } from 'react'
import { SonnerToaster } from 'ui'
// import { BackendProvider } from '@/registry/backend-provider'
type Config = {
style: Style['name']
theme: Theme['name']
radius: number
sonnerPosition: ComponentProps<typeof SonnerToaster>['position']
sonnerExpand: boolean
// backendProvider: BackendProvider['name'] | undefined
}
const configAtom = atomWithStorage<Config>('config', {
style: 'default',
// backendProvider: 'nextjs-auth',
theme: 'zinc',
radius: 0.5,
sonnerPosition: 'bottom-right',
sonnerExpand: false,
// backendProvider: 'nextjs-auth',
})
export function useConfig() {
+4 -2
View File
@@ -1,5 +1,7 @@
import { withContentlayer } from 'next-contentlayer2'
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/design-system'
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['ui', 'common', 'shared-data', 'icons', 'tsconfig'],
@@ -22,11 +24,11 @@ const nextConfig = {
},
async redirects() {
return [
...(process.env.NEXT_PUBLIC_BASE_PATH?.length
...(BASE_PATH.length
? [
{
source: '/',
destination: process.env.NEXT_PUBLIC_BASE_PATH,
destination: BASE_PATH,
basePath: false,
permanent: false,
},
+3 -3
View File
@@ -14,7 +14,7 @@
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.0.5",
"contentlayer2": "0.4.5",
"contentlayer2": "0.4.6",
"eslint-config-supabase": "*",
"fs": "^0.0.1-security",
"jotai": "^2.8.0",
@@ -22,7 +22,7 @@
"lucide-react": "^0.338.0",
"markdown-wasm": "^1.2.0",
"next": "^14.2.3",
"next-contentlayer2": "0.4.5",
"next-contentlayer2": "0.4.6",
"react": "^18.2.0",
"react-docgen": "^7.0.3",
"react-dom": "^18.2.0",
@@ -52,7 +52,7 @@
"shiki": "^1.1.7",
"tailwindcss": "^3.3.0",
"tsconfig": "*",
"typescript": "^5",
"typescript": "^5.5.0",
"unist-builder": "3.0.0"
}
}
@@ -0,0 +1,100 @@
<svg width="570" height="527" viewBox="0 0 570 527" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.0027 21.0027L30.7733 24.7733L44.2293 38.2293L48 42L51.7707 38.2293L65.2267 24.7733L68.9973 21.0027L65.2267 17.2267L51.7707 3.77067L48 0L44.2293 3.77067L30.7733 17.2267L27.0027 21.0027ZM61.456 21.0027L48 34.4533L34.544 20.9973L48 7.54133L61.456 21.0027ZM27.0027 74.9973L30.7733 78.7733L44.2293 92.2293L48 96L51.7707 92.2293L65.2267 78.7733L68.9973 75.0027L65.2267 71.2267L51.7707 57.7707L48 54L44.2293 57.7707L30.7733 71.2267L27.0027 74.9973ZM61.456 74.9973L48 88.464L34.544 75.008L48 61.536L61.456 74.9973ZM3.77067 51.7707L0 48L3.77067 44.2293L17.2267 30.7733L21.0027 27.0027L24.7733 30.7733L38.2293 44.2293L42 48L38.2293 51.7707L24.7733 65.2267L21.0027 68.9973L17.2267 65.2267L3.77067 51.7707ZM20.9973 61.456L34.464 48L20.9973 34.544L7.54133 48L20.9973 61.456ZM54 48L57.7707 51.7707L71.2267 65.2267L74.9973 68.9973L78.7733 65.2267L92.2293 51.7707L96 48L92.2293 44.2293L78.7733 30.7733L75.0027 27.0027L71.2267 30.7733L57.7707 44.2293L54 48ZM88.4533 48L74.9973 61.456L61.5467 48L75.008 34.544L88.4533 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip1_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 48L206 0L158 48L206 96L254 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip2_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 48L364 0L316 48L364 96L412 48ZM323.541 48L364 7.54133L404.459 48L364 88.4587L323.541 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip3_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 21.0027L504.773 24.7733L518.229 38.2293L522 42L525.771 38.2293L539.227 24.7733L542.997 21.0027L539.227 17.2267L525.771 3.77067L522 0L518.229 3.77067L504.773 17.2267L501.003 21.0027ZM501.003 74.9973L504.773 78.7733L518.229 92.2293L522 96L525.771 92.2293L539.227 78.7733L542.997 75.0027L539.227 71.2267L525.771 57.7707L522 54L518.229 57.7707L504.773 71.2267L501.003 74.9973ZM477.771 51.7707L474 48L477.771 44.2293L491.227 30.7733L495.003 27.0027L498.773 30.7733L512.229 44.2293L516 48L512.229 51.7707L498.773 65.2267L495.003 68.9973L491.227 65.2267L477.771 51.7707ZM528 48L531.771 51.7707L545.227 65.2267L548.997 68.9973L552.773 65.2267L566.229 51.7707L570 48L566.229 44.2293L552.773 30.7733L549.003 27.0027L545.227 30.7733L531.771 44.2293L528 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip4_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 195L48 147L0 195L48 243L96 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip5_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 195L206 147L158 195L206 243L254 195ZM165.541 195L206 154.541L246.459 195L206 235.459L165.541 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip6_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M343.003 168.003L346.773 171.773L360.229 185.229L364 189L367.771 185.229L381.227 171.773L384.997 168.003L381.227 164.227L367.771 150.771L364 147L360.229 150.771L346.773 164.227L343.003 168.003ZM377.456 168.003L364 181.453L350.544 167.997L364 154.541L377.456 168.003ZM343.003 221.997L346.773 225.773L360.229 239.229L364 243L367.771 239.229L381.227 225.773L384.997 222.003L381.227 218.227L367.771 204.771L364 201L360.229 204.771L346.773 218.227L343.003 221.997ZM377.456 221.997L364 235.464L350.544 222.008L364 208.536L377.456 221.997ZM319.771 198.771L316 195L319.771 191.229L333.227 177.773L337.003 174.003L340.773 177.773L354.229 191.229L358 195L354.229 198.771L340.773 212.227L337.003 215.997L333.227 212.227L319.771 198.771ZM336.997 208.456L350.464 195L336.997 181.544L323.541 195L336.997 208.456ZM370 195L373.771 198.771L387.227 212.227L390.997 215.997L394.773 212.227L408.229 198.771L412 195L408.229 191.229L394.773 177.773L391.003 174.003L387.227 177.773L373.771 191.229L370 195ZM404.453 195L390.997 208.456L377.547 195L391.008 181.544L404.453 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip7_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 168.003L504.773 171.773L518.229 185.229L522 189L525.771 185.229L539.227 171.773L542.997 168.003L539.227 164.227L525.771 150.771L522 147L518.229 150.771L504.773 164.227L501.003 168.003ZM501.003 221.997L504.773 225.773L518.229 239.229L522 243L525.771 239.229L539.227 225.773L542.997 222.003L539.227 218.227L525.771 204.771L522 201L518.229 204.771L504.773 218.227L501.003 221.997ZM477.771 198.771L474 195L477.771 191.229L491.227 177.773L495.003 174.003L498.773 177.773L512.229 191.229L516 195L512.229 198.771L498.773 212.227L495.003 215.997L491.227 212.227L477.771 198.771ZM528 195L531.771 198.771L545.227 212.227L548.997 215.997L552.773 212.227L566.229 198.771L570 195L566.229 191.229L552.773 177.773L549.003 174.003L545.227 177.773L531.771 191.229L528 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip8_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 342L48 294L0 342L48 390L96 342ZM7.54133 342L48 301.541L88.4587 342L48 382.459L7.54133 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip9_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M185.003 315.003L188.773 318.773L202.229 332.229L206 336L209.771 332.229L223.227 318.773L226.997 315.003L223.227 311.227L209.771 297.771L206 294L202.229 297.771L188.773 311.227L185.003 315.003ZM185.003 368.997L188.773 372.773L202.229 386.229L206 390L209.771 386.229L223.227 372.773L226.997 369.003L223.227 365.227L209.771 351.771L206 348L202.229 351.771L188.773 365.227L185.003 368.997ZM161.771 345.771L158 342L161.771 338.229L175.227 324.773L179.003 321.003L182.773 324.773L196.229 338.229L200 342L196.229 345.771L182.773 359.227L179.003 362.997L175.227 359.227L161.771 345.771ZM212 342L215.771 345.771L229.227 359.227L232.997 362.997L236.773 359.227L250.229 345.771L254 342L250.229 338.229L236.773 324.773L233.003 321.003L229.227 324.773L215.771 338.229L212 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip10_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 342L364 294L316 342L364 390L412 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip11_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 315.003L504.773 318.773L518.229 332.229L522 336L525.771 332.229L539.227 318.773L542.997 315.003L539.227 311.227L525.771 297.771L522 294L518.229 297.771L504.773 311.227L501.003 315.003ZM535.456 315.003L522 328.453L508.544 314.997L522 301.541L535.456 315.003ZM501.003 368.997L504.773 372.773L518.229 386.229L522 390L525.771 386.229L539.227 372.773L542.997 369.003L539.227 365.227L525.771 351.771L522 348L518.229 351.771L504.773 365.227L501.003 368.997ZM535.456 368.997L522 382.464L508.544 369.008L522 355.536L535.456 368.997ZM477.771 345.771L474 342L477.771 338.229L491.227 324.773L495.003 321.003L498.773 324.773L512.229 338.229L516 342L512.229 345.771L498.773 359.227L495.003 362.997L491.227 359.227L477.771 345.771ZM494.997 355.456L508.464 342L494.997 328.544L481.541 342L494.997 355.456ZM528 342L531.771 345.771L545.227 359.227L548.997 362.997L552.773 359.227L566.229 345.771L570 342L566.229 338.229L552.773 324.773L549.003 321.003L545.227 324.773L531.771 338.229L528 342ZM562.453 342L548.997 355.456L535.547 342L549.008 328.544L562.453 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip12_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 479L48 431L0 479L48 527L96 479ZM7.54133 479L48 438.541L88.4587 479L48 519.459L7.54133 479Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip13_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M185.003 452.003L188.773 455.773L202.229 469.229L206 473L209.771 469.229L223.227 455.773L226.997 452.003L223.227 448.227L209.771 434.771L206 431L202.229 434.771L188.773 448.227L185.003 452.003ZM219.456 452.003L206 465.453L192.544 451.997L206 438.541L219.456 452.003ZM185.003 505.997L188.773 509.773L202.229 523.229L206 527L209.771 523.229L223.227 509.773L226.997 506.003L223.227 502.227L209.771 488.771L206 485L202.229 488.771L188.773 502.227L185.003 505.997ZM219.456 505.997L206 519.464L192.544 506.008L206 492.536L219.456 505.997ZM161.771 482.771L158 479L161.771 475.229L175.227 461.773L179.003 458.003L182.773 461.773L196.229 475.229L200 479L196.229 482.771L182.773 496.227L179.003 499.997L175.227 496.227L161.771 482.771ZM178.997 492.456L192.464 479L178.997 465.544L165.541 479L178.997 492.456ZM212 479L215.771 482.771L229.227 496.227L232.997 499.997L236.773 496.227L250.229 482.771L254 479L250.229 475.229L236.773 461.773L233.003 458.003L229.227 461.773L215.771 475.229L212 479ZM246.453 479L232.997 492.456L219.547 479L233.008 465.544L246.453 479Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip14_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 479L364 431L316 479L364 527L412 479Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip15_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 452.003L504.773 455.773L518.229 469.229L522 473L525.771 469.229L539.227 455.773L542.997 452.003L539.227 448.227L525.771 434.771L522 431L518.229 434.771L504.773 448.227L501.003 452.003ZM501.003 505.997L504.773 509.773L518.229 523.229L522 527L525.771 523.229L539.227 509.773L542.997 506.003L539.227 502.227L525.771 488.771L522 485L518.229 488.771L504.773 502.227L501.003 505.997ZM477.771 482.771L474 479L477.771 475.229L491.227 461.773L495.003 458.003L498.773 461.773L512.229 475.229L516 479L512.229 482.771L498.773 496.227L495.003 499.997L491.227 496.227L477.771 482.771ZM528 479L531.771 482.771L545.227 496.227L548.997 499.997L552.773 496.227L566.229 482.771L570 479L566.229 475.229L552.773 461.773L549.003 458.003L545.227 461.773L531.771 475.229L528 479Z" fill="#B4B4B4"/>
</g>
<defs>
<clipPath id="clip0_1709_1536">
<rect width="96" height="96" fill="white"/>
</clipPath>
<clipPath id="clip1_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158)"/>
</clipPath>
<clipPath id="clip2_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316)"/>
</clipPath>
<clipPath id="clip3_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474)"/>
</clipPath>
<clipPath id="clip4_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 147)"/>
</clipPath>
<clipPath id="clip5_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 147)"/>
</clipPath>
<clipPath id="clip6_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 147)"/>
</clipPath>
<clipPath id="clip7_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 147)"/>
</clipPath>
<clipPath id="clip8_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 294)"/>
</clipPath>
<clipPath id="clip9_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 294)"/>
</clipPath>
<clipPath id="clip10_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 294)"/>
</clipPath>
<clipPath id="clip11_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 294)"/>
</clipPath>
<clipPath id="clip12_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 431)"/>
</clipPath>
<clipPath id="clip13_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 431)"/>
</clipPath>
<clipPath id="clip14_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 431)"/>
</clipPath>
<clipPath id="clip15_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 431)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,61 @@
<svg width="1360" height="96" viewBox="0 0 1360 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.0027 21.0027L30.7733 24.7733L44.2293 38.2293L48 42L51.7707 38.2293L65.2267 24.7733L68.9973 21.0027L65.2267 17.2267L51.7707 3.77067L48 0L44.2293 3.77067L30.7733 17.2267L27.0027 21.0027ZM61.456 21.0027L48 34.4533L34.544 20.9973L48 7.54133L61.456 21.0027ZM27.0027 74.9973L30.7733 78.7733L44.2293 92.2293L48 96L51.7707 92.2293L65.2267 78.7733L68.9973 75.0027L65.2267 71.2267L51.7707 57.7707L48 54L44.2293 57.7707L30.7733 71.2267L27.0027 74.9973ZM61.456 74.9973L48 88.464L34.544 75.008L48 61.536L61.456 74.9973ZM3.77067 51.7707L0 48L3.77067 44.2293L17.2267 30.7733L21.0027 27.0027L24.7733 30.7733L38.2293 44.2293L42 48L38.2293 51.7707L24.7733 65.2267L21.0027 68.9973L17.2267 65.2267L3.77067 51.7707ZM20.9973 61.456L34.464 48L20.9973 34.544L7.54133 48L20.9973 61.456ZM54 48L57.7707 51.7707L71.2267 65.2267L74.9973 68.9973L78.7733 65.2267L92.2293 51.7707L96 48L92.2293 44.2293L78.7733 30.7733L75.0027 27.0027L71.2267 30.7733L57.7707 44.2293L54 48ZM88.4533 48L74.9973 61.456L61.5467 48L75.008 34.544L88.4533 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip1_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 48L206 0L158 48L206 96L254 48ZM165.541 48L206 7.54133L246.459 48L206 88.4587L165.541 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip2_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 48L364 0L316 48L364 96L412 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip3_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 21.0027L504.773 24.7733L518.229 38.2293L522 42L525.771 38.2293L539.227 24.7733L542.997 21.0027L539.227 17.2267L525.771 3.77067L522 0L518.229 3.77067L504.773 17.2267L501.003 21.0027ZM501.003 74.9973L504.773 78.7733L518.229 92.2293L522 96L525.771 92.2293L539.227 78.7733L542.997 75.0027L539.227 71.2267L525.771 57.7707L522 54L518.229 57.7707L504.773 71.2267L501.003 74.9973ZM477.771 51.7707L474 48L477.771 44.2293L491.227 30.7733L495.003 27.0027L498.773 30.7733L512.229 44.2293L516 48L512.229 51.7707L498.773 65.2267L495.003 68.9973L491.227 65.2267L477.771 51.7707ZM528 48L531.771 51.7707L545.227 65.2267L548.997 68.9973L552.773 65.2267L566.229 51.7707L570 48L566.229 44.2293L552.773 30.7733L549.003 27.0027L545.227 30.7733L531.771 44.2293L528 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip4_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M728 48L680 0L632 48L680 96L728 48ZM639.541 48L680 7.54133L720.459 48L680 88.4587L639.541 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip5_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M886 48L838 0L790 48L838 96L886 48Z" fill="#15593B"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1017 21L996 0L975 21L996 42L1017 21ZM978.299 21L996 3.29933L1013.7 21L996 38.7007L978.299 21Z" fill="white"/>
<path d="M978.299 21L996 3.29933L1013.7 21L996 38.7007L978.299 21Z" fill="#3ECF8E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1017 75L996 54L975 75L996 96L1017 75ZM978.299 75L996 57.2993L1013.7 75L996 92.7007L978.299 75Z" fill="white"/>
<path d="M978.299 75L996 57.2993L1013.7 75L996 92.7007L978.299 75Z" fill="#3ECF8E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M990 48L969 27L948 48L969 69L990 48ZM951.299 48L969 30.2993L986.701 48L969 65.7007L951.299 48Z" fill="white"/>
<path d="M951.299 48L969 30.2993L986.701 48L969 65.7007L951.299 48Z" fill="#3ECF8E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1044 48L1023 27L1002 48L1023 69L1044 48ZM1005.3 48L1023 30.2993L1040.7 48L1023 65.7007L1005.3 48Z" fill="white"/>
<path d="M1005.3 48L1023 30.2993L1040.7 48L1023 65.7007L1005.3 48Z" fill="#3ECF8E"/>
<g clip-path="url(#clip6_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1202 48L1154 0L1106 48L1154 96L1202 48ZM1113.54 48L1154 7.54133L1194.46 48L1154 88.4587L1113.54 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip7_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1360 48L1312 0L1264 48L1312 96L1360 48ZM1271.54 48L1312 7.54133L1352.46 48L1312 88.4587L1271.54 48Z" fill="#15593B"/>
<path d="M1271.54 48L1312 7.54133L1352.46 48L1312 88.4587L1271.54 48Z" fill="#3ECF8E"/>
</g>
<defs>
<clipPath id="clip0_1709_1278">
<rect width="96" height="96" fill="white"/>
</clipPath>
<clipPath id="clip1_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(158)"/>
</clipPath>
<clipPath id="clip2_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(316)"/>
</clipPath>
<clipPath id="clip3_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(474)"/>
</clipPath>
<clipPath id="clip4_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(632)"/>
</clipPath>
<clipPath id="clip5_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(790)"/>
</clipPath>
<clipPath id="clip6_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(1106)"/>
</clipPath>
<clipPath id="clip7_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(1264)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +1,104 @@
<svg width="626" height="587" viewBox="0 0 626 587" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M55.0027 51.0027L58.7733 54.7733L72.2293 68.2293L76 72L79.7707 68.2293L93.2267 54.7733L96.9973 51.0027L93.2267 47.2267L79.7707 33.7707L76 30L72.2293 33.7707L58.7733 47.2267L55.0027 51.0027ZM89.456 51.0027L76 64.4533L62.544 50.9973L76 37.5413L89.456 51.0027ZM55.0027 104.997L58.7733 108.773L72.2293 122.229L76 126L79.7707 122.229L93.2267 108.773L96.9973 105.003L93.2267 101.227L79.7707 87.7707L76 84L72.2293 87.7707L58.7733 101.227L55.0027 104.997ZM89.456 104.997L76 118.464L62.544 105.008L76 91.536L89.456 104.997ZM31.7707 81.7707L28 78L31.7707 74.2293L45.2267 60.7733L49.0027 57.0027L52.7733 60.7733L66.2293 74.2293L70 78L66.2293 81.7707L52.7733 95.2267L49.0027 98.9973L45.2267 95.2267L31.7707 81.7707ZM48.9973 91.456L62.464 78L48.9973 64.544L35.5413 78L48.9973 91.456ZM82 78L85.7707 81.7707L99.2267 95.2267L102.997 98.9973L106.773 95.2267L120.229 81.7707L124 78L120.229 74.2293L106.773 60.7733L103.003 57.0027L99.2267 60.7733L85.7707 74.2293L82 78ZM116.453 78L102.997 91.456L89.5467 78L103.008 64.544L116.453 78Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip1_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M282 78L234 30L186 78L234 126L282 78Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip2_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M440 78L392 30L344 78L392 126L440 78ZM351.541 78L392 37.5413L432.459 78L392 118.459L351.541 78Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip3_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M529.003 51.0027L532.773 54.7733L546.229 68.2293L550 72L553.771 68.2293L567.227 54.7733L570.997 51.0027L567.227 47.2267L553.771 33.7707L550 30L546.229 33.7707L532.773 47.2267L529.003 51.0027ZM529.003 104.997L532.773 108.773L546.229 122.229L550 126L553.771 122.229L567.227 108.773L570.997 105.003L567.227 101.227L553.771 87.7707L550 84L546.229 87.7707L532.773 101.227L529.003 104.997ZM505.771 81.7707L502 78L505.771 74.2293L519.227 60.7733L523.003 57.0027L526.773 60.7733L540.229 74.2293L544 78L540.229 81.7707L526.773 95.2267L523.003 98.9973L519.227 95.2267L505.771 81.7707ZM556 78L559.771 81.7707L573.227 95.2267L576.997 98.9973L580.773 95.2267L594.229 81.7707L598 78L594.229 74.2293L580.773 60.7733L577.003 57.0027L573.227 60.7733L559.771 74.2293L556 78Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip4_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M124 225L76 177L28 225L76 273L124 225Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip5_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M282 225L234 177L186 225L234 273L282 225ZM193.541 225L234 184.541L274.459 225L234 265.459L193.541 225Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip6_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M371.003 198.003L374.773 201.773L388.229 215.229L392 219L395.771 215.229L409.227 201.773L412.997 198.003L409.227 194.227L395.771 180.771L392 177L388.229 180.771L374.773 194.227L371.003 198.003ZM405.456 198.003L392 211.453L378.544 197.997L392 184.541L405.456 198.003ZM371.003 251.997L374.773 255.773L388.229 269.229L392 273L395.771 269.229L409.227 255.773L412.997 252.003L409.227 248.227L395.771 234.771L392 231L388.229 234.771L374.773 248.227L371.003 251.997ZM405.456 251.997L392 265.464L378.544 252.008L392 238.536L405.456 251.997ZM347.771 228.771L344 225L347.771 221.229L361.227 207.773L365.003 204.003L368.773 207.773L382.229 221.229L386 225L382.229 228.771L368.773 242.227L365.003 245.997L361.227 242.227L347.771 228.771ZM364.997 238.456L378.464 225L364.997 211.544L351.541 225L364.997 238.456ZM398 225L401.771 228.771L415.227 242.227L418.997 245.997L422.773 242.227L436.229 228.771L440 225L436.229 221.229L422.773 207.773L419.003 204.003L415.227 207.773L401.771 221.229L398 225ZM432.453 225L418.997 238.456L405.547 225L419.008 211.544L432.453 225Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip7_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M529.003 198.003L532.773 201.773L546.229 215.229L550 219L553.771 215.229L567.227 201.773L570.997 198.003L567.227 194.227L553.771 180.771L550 177L546.229 180.771L532.773 194.227L529.003 198.003ZM529.003 251.997L532.773 255.773L546.229 269.229L550 273L553.771 269.229L567.227 255.773L570.997 252.003L567.227 248.227L553.771 234.771L550 231L546.229 234.771L532.773 248.227L529.003 251.997ZM505.771 228.771L502 225L505.771 221.229L519.227 207.773L523.003 204.003L526.773 207.773L540.229 221.229L544 225L540.229 228.771L526.773 242.227L523.003 245.997L519.227 242.227L505.771 228.771ZM556 225L559.771 228.771L573.227 242.227L576.997 245.997L580.773 242.227L594.229 228.771L598 225L594.229 221.229L580.773 207.773L577.003 204.003L573.227 207.773L559.771 221.229L556 225Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip8_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M124 372L76 324L28 372L76 420L124 372ZM35.5413 372L76 331.541L116.459 372L76 412.459L35.5413 372Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip9_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M213.003 345.003L216.773 348.773L230.229 362.229L234 366L237.771 362.229L251.227 348.773L254.997 345.003L251.227 341.227L237.771 327.771L234 324L230.229 327.771L216.773 341.227L213.003 345.003ZM213.003 398.997L216.773 402.773L230.229 416.229L234 420L237.771 416.229L251.227 402.773L254.997 399.003L251.227 395.227L237.771 381.771L234 378L230.229 381.771L216.773 395.227L213.003 398.997ZM189.771 375.771L186 372L189.771 368.229L203.227 354.773L207.003 351.003L210.773 354.773L224.229 368.229L228 372L224.229 375.771L210.773 389.227L207.003 392.997L203.227 389.227L189.771 375.771ZM240 372L243.771 375.771L257.227 389.227L260.997 392.997L264.773 389.227L278.229 375.771L282 372L278.229 368.229L264.773 354.773L261.003 351.003L257.227 354.773L243.771 368.229L240 372Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip10_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M440 372L392 324L344 372L392 420L440 372Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip11_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M529.003 345.003L532.773 348.773L546.229 362.229L550 366L553.771 362.229L567.227 348.773L570.997 345.003L567.227 341.227L553.771 327.771L550 324L546.229 327.771L532.773 341.227L529.003 345.003ZM563.456 345.003L550 358.453L536.544 344.997L550 331.541L563.456 345.003ZM529.003 398.997L532.773 402.773L546.229 416.229L550 420L553.771 416.229L567.227 402.773L570.997 399.003L567.227 395.227L553.771 381.771L550 378L546.229 381.771L532.773 395.227L529.003 398.997ZM563.456 398.997L550 412.464L536.544 399.008L550 385.536L563.456 398.997ZM505.771 375.771L502 372L505.771 368.229L519.227 354.773L523.003 351.003L526.773 354.773L540.229 368.229L544 372L540.229 375.771L526.773 389.227L523.003 392.997L519.227 389.227L505.771 375.771ZM522.997 385.456L536.464 372L522.997 358.544L509.541 372L522.997 385.456ZM556 372L559.771 375.771L573.227 389.227L576.997 392.997L580.773 389.227L594.229 375.771L598 372L594.229 368.229L580.773 354.773L577.003 351.003L573.227 354.773L559.771 368.229L556 372ZM590.453 372L576.997 385.456L563.547 372L577.008 358.544L590.453 372Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip12_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M124 509L76 461L28 509L76 557L124 509ZM35.5413 509L76 468.541L116.459 509L76 549.459L35.5413 509Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip13_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M213.003 482.003L216.773 485.773L230.229 499.229L234 503L237.771 499.229L251.227 485.773L254.997 482.003L251.227 478.227L237.771 464.771L234 461L230.229 464.771L216.773 478.227L213.003 482.003ZM247.456 482.003L234 495.453L220.544 481.997L234 468.541L247.456 482.003ZM213.003 535.997L216.773 539.773L230.229 553.229L234 557L237.771 553.229L251.227 539.773L254.997 536.003L251.227 532.227L237.771 518.771L234 515L230.229 518.771L216.773 532.227L213.003 535.997ZM247.456 535.997L234 549.464L220.544 536.008L234 522.536L247.456 535.997ZM189.771 512.771L186 509L189.771 505.229L203.227 491.773L207.003 488.003L210.773 491.773L224.229 505.229L228 509L224.229 512.771L210.773 526.227L207.003 529.997L203.227 526.227L189.771 512.771ZM206.997 522.456L220.464 509L206.997 495.544L193.541 509L206.997 522.456ZM240 509L243.771 512.771L257.227 526.227L260.997 529.997L264.773 526.227L278.229 512.771L282 509L278.229 505.229L264.773 491.773L261.003 488.003L257.227 491.773L243.771 505.229L240 509ZM274.453 509L260.997 522.456L247.547 509L261.008 495.544L274.453 509Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip14_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M440 509L392 461L344 509L392 557L440 509Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip15_1709_1537)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M529.003 482.003L532.773 485.773L546.229 499.229L550 503L553.771 499.229L567.227 485.773L570.997 482.003L567.227 478.227L553.771 464.771L550 461L546.229 464.771L532.773 478.227L529.003 482.003ZM529.003 535.997L532.773 539.773L546.229 553.229L550 557L553.771 553.229L567.227 539.773L570.997 536.003L567.227 532.227L553.771 518.771L550 515L546.229 518.771L532.773 532.227L529.003 535.997ZM505.771 512.771L502 509L505.771 505.229L519.227 491.773L523.003 488.003L526.773 491.773L540.229 505.229L544 509L540.229 512.771L526.773 526.227L523.003 529.997L519.227 526.227L505.771 512.771ZM556 509L559.771 512.771L573.227 526.227L576.997 529.997L580.773 526.227L594.229 512.771L598 509L594.229 505.229L580.773 491.773L577.003 488.003L573.227 491.773L559.771 505.229L556 509Z" fill="#B4B4B4"/>
</g>
<rect x="4.98267" y="4" width="616.035" height="579" stroke="#4D4D4D" stroke-width="8"/>
<rect x="160.991" y="302" width="304.017" height="140" stroke="#4D4D4D" stroke-width="8"/>
<rect x="317" y="4" width="304.017" height="297" stroke="#4D4D4D" stroke-width="8"/>
<rect x="4.98267" y="153" width="156.017" height="289" stroke="#4D4D4D" stroke-width="8"/>
<defs>
<clipPath id="clip0_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(28 30)"/>
</clipPath>
<clipPath id="clip1_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(186 30)"/>
</clipPath>
<clipPath id="clip2_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(344 30)"/>
</clipPath>
<clipPath id="clip3_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(502 30)"/>
</clipPath>
<clipPath id="clip4_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(28 177)"/>
</clipPath>
<clipPath id="clip5_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(186 177)"/>
</clipPath>
<clipPath id="clip6_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(344 177)"/>
</clipPath>
<clipPath id="clip7_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(502 177)"/>
</clipPath>
<clipPath id="clip8_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(28 324)"/>
</clipPath>
<clipPath id="clip9_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(186 324)"/>
</clipPath>
<clipPath id="clip10_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(344 324)"/>
</clipPath>
<clipPath id="clip11_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(502 324)"/>
</clipPath>
<clipPath id="clip12_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(28 461)"/>
</clipPath>
<clipPath id="clip13_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(186 461)"/>
</clipPath>
<clipPath id="clip14_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(344 461)"/>
</clipPath>
<clipPath id="clip15_1709_1537">
<rect width="96" height="96" fill="white" transform="translate(502 461)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,55 @@
import React, { useRef, useState } from 'react'
import { AssistantChatForm } from 'ui-patterns/AssistantChat/AssistantChatForm'
import { AssistantCommandsPopover } from 'ui-patterns/AssistantChat/AssistantCommandsPopover'
export default function AssistantChatCommands() {
const [commandsOpen, setCommandsOpen] = useState<boolean>(false)
const textAreaRef = useRef<HTMLTextAreaElement>(null)
const [value, setValueState] = useState('')
const [loading, setLoading] = useState(false)
function handleSubmit(event: React.FormEvent) {
// set loading for 3 seconds and then reset
setLoading(true)
setTimeout(() => {
setLoading(false)
}, 1500)
}
const suggestions = [
'Add policy for org Inserted User Access',
'Add policy for User-Specific Todo Access',
'Add policy for Org Update Restriction',
]
return (
<div className="flex flex-col gap-3">
<AssistantCommandsPopover
open={commandsOpen}
setOpen={setCommandsOpen}
textAreaRef={textAreaRef}
value={value}
setValue={(e) => setValueState(e)}
suggestions={suggestions}
>
<AssistantChatForm
textAreaRef={textAreaRef}
value={value}
loading={loading}
disabled={loading}
onValueChange={(e) => setValueState(e.target.value)}
commandsOpen={commandsOpen}
setCommandsOpen={setCommandsOpen}
onSubmit={async (event) => {
event.preventDefault()
handleSubmit(event)
}}
/>
</AssistantCommandsPopover>
<p className="text-xs mt-3 text-foreground-lighter">
Press <span className="bg-surface-300 px-[3px] py-[2px] border rounded">/</span> to open
commands
</p>
</div>
)
}
@@ -0,0 +1,30 @@
import { Box } from 'lucide-react'
import { useState } from 'react'
import { AssistantChatForm } from 'ui-patterns/AssistantChat/AssistantChatForm'
export default function AssistantChatDemo() {
const [value, setValueState] = useState('')
const [loading, setLoading] = useState(false)
function handleSubmit(event: React.FormEvent) {
// set loading for 3 seconds and then reset
setLoading(true)
setTimeout(() => {
setLoading(false)
}, 1500)
}
return (
<AssistantChatForm
icon={<Box strokeWidth={1.5} size={24} className="text-foreground-muted" />}
value={value}
loading={loading}
disabled={loading}
onValueChange={(e) => setValueState(e.target.value)}
onSubmit={async (event) => {
event.preventDefault()
handleSubmit(event)
}}
/>
)
}
@@ -0,0 +1,42 @@
import { Button } from '@ui/components/shadcn/ui/button'
import {
BadgeExperimental,
CommandInput,
CommandList,
CommandMenu,
CommandProvider,
useRegisterCommands,
useSetCommandMenuOpen,
} from 'ui-patterns/CommandMenu'
function Commands() {
useRegisterCommands('Commands', [
{
id: 'experimental',
name: 'Experimental',
action: () => alert('You triggered an experimental command'),
badge: () => <BadgeExperimental />,
},
])
return null
}
function CommandMenuTrigger() {
const setOpen = useSetCommandMenuOpen()
return <Button onClick={() => setOpen(true)}>Open command menu</Button>
}
export default function CommandMenuDemo() {
return (
<CommandProvider openKey="">
<CommandMenuTrigger />
<Commands />
<CommandMenu>
<CommandInput />
<CommandList />
</CommandMenu>
</CommandProvider>
)
}
@@ -0,0 +1,70 @@
import { Button } from '@ui/components/shadcn/ui/button'
import { useMemo, useState } from 'react'
import {
CommandInput,
CommandList,
CommandMenu,
CommandMenuTrigger as CommandMenuTriggerPrimitive,
CommandProvider,
useRegisterCommands,
} from 'ui-patterns/CommandMenu'
function Commands() {
const [activeCommand, setActiveCommand] = useState(1)
const toggleCommands = () => setActiveCommand((state) => (state === 1 ? 2 : 1))
const commandOne = useMemo(
() => [
{
id: 'one',
name: 'One',
action: () => alert('One'),
},
],
[]
)
const commandTwo = useMemo(
() => [
{
id: 'two',
name: 'Two',
action: () => alert('Two'),
},
],
[]
)
const command = activeCommand === 1 ? commandOne : commandTwo
useRegisterCommands('Commands', command, { deps: [command] })
return (
<Button onClick={toggleCommands}>
{activeCommand === 1 ? 'Change command to command two' : 'Change command to command one'}
</Button>
)
}
function CommandMenuTrigger() {
return (
<CommandMenuTriggerPrimitive>
<Button>Open command menu</Button>
</CommandMenuTriggerPrimitive>
)
}
export default function CommandMenuDemo() {
return (
<CommandProvider openKey="">
<div className="flex flex-col gap-2">
<CommandMenu trigger={<CommandMenuTrigger />}>
<CommandInput />
<CommandList />
</CommandMenu>
<Commands />
</div>
</CommandProvider>
)
}
@@ -0,0 +1,48 @@
import { Button } from '@ui/components/shadcn/ui/button'
import {
CommandInput,
CommandList,
CommandMenu,
CommandMenuTrigger as CommandMenuTriggerPrimitive,
CommandProvider,
useRegisterCommands,
} from 'ui-patterns/CommandMenu'
function Commands() {
useRegisterCommands('Action commands', [
{
id: 'alert',
name: 'Alert',
action: () => alert('You triggered a command'),
},
])
useRegisterCommands('Route commands', [
{
id: 'supabase-website',
name: 'Go to Supabase website',
route: 'https://supabase.com',
},
])
return null
}
function CommandMenuTrigger() {
return (
<CommandMenuTriggerPrimitive>
<Button>Open command menu</Button>
</CommandMenuTriggerPrimitive>
)
}
export default function CommandMenuDemo() {
return (
<CommandProvider openKey="j">
<Commands />
<CommandMenu trigger={<CommandMenuTrigger />}>
<CommandInput />
<CommandList />
</CommandMenu>
</CommandProvider>
)
}
@@ -0,0 +1,60 @@
import { Button } from '@ui/components/shadcn/ui/button'
import {
CommandInput,
CommandList,
CommandMenu,
CommandMenuTrigger as CommandMenuTriggerPrimitive,
CommandProvider,
useRegisterCommands,
} from 'ui-patterns/CommandMenu'
function Commands() {
useRegisterCommands('Commands', [
{
id: 'forced',
name: 'Force-mounted',
action: () => alert("This command always shows even if it doesn't match the search query"),
forceMount: true,
},
{
id: 'not-forced',
name: 'Not force-mounted',
action: () => alert("This command will disappear if it doesn't match the search query"),
},
])
useRegisterCommands(
'Force-mounted section',
[
{
id: 'example',
name: 'An example',
action: () => alert('This entire section is force-mounted'),
},
],
{
forceMountSection: true,
}
)
return null
}
function CommandMenuTrigger() {
return (
<CommandMenuTriggerPrimitive>
<Button>Open command menu</Button>
</CommandMenuTriggerPrimitive>
)
}
export default function CommandMenuDemo() {
return (
<CommandProvider openKey="">
<Commands />
<CommandMenu trigger={<CommandMenuTrigger />}>
<CommandInput />
<CommandList />
</CommandMenu>
</CommandProvider>
)
}
@@ -0,0 +1,47 @@
import { Button } from '@ui/components/shadcn/ui/button'
import {
CommandInput,
CommandList,
CommandMenu,
CommandMenuTrigger as CommandMenuTriggerPrimitive,
CommandProvider,
useRegisterCommands,
} from 'ui-patterns/CommandMenu'
function Commands() {
useRegisterCommands('Commands', [
{
id: 'shown',
name: 'shown',
action: () => alert('This command is shown by default'),
},
{
id: 'hidden',
name: 'open sesame',
action: () => alert('This command is only shown when searched'),
defaultHidden: true,
},
])
return null
}
function CommandMenuTrigger() {
return (
<CommandMenuTriggerPrimitive>
<Button>Open command menu</Button>
</CommandMenuTriggerPrimitive>
)
}
export default function CommandMenuDemo() {
return (
<CommandProvider openKey="">
<Commands />
<CommandMenu trigger={<CommandMenuTrigger />}>
<CommandInput />
<CommandList />
</CommandMenu>
</CommandProvider>
)
}
@@ -0,0 +1,43 @@
import { Button } from '@ui/components/shadcn/ui/button'
import { Github } from 'lucide-react'
import {
CommandInput,
CommandList,
CommandMenu,
CommandMenuTrigger as CommandMenuTriggerPrimitive,
CommandProvider,
useRegisterCommands,
} from 'ui-patterns/CommandMenu'
function Commands() {
useRegisterCommands('Commands', [
{
id: 'github',
name: 'Go to repo',
route: 'https://github.com/supabase/supabase',
icon: () => <Github />,
},
])
return null
}
function CommandMenuTrigger() {
return (
<CommandMenuTriggerPrimitive>
<Button>Open command menu</Button>
</CommandMenuTriggerPrimitive>
)
}
export default function CommandMenuDemo() {
return (
<CommandProvider openKey="">
<Commands />
<CommandMenu trigger={<CommandMenuTrigger />}>
<CommandInput />
<CommandList />
</CommandMenu>
</CommandProvider>
)
}
@@ -0,0 +1,65 @@
import { Button } from '@ui/components/shadcn/ui/button'
import { useMemo } from 'react'
import {
Breadcrumb,
CommandInput,
CommandList,
CommandMenu,
CommandMenuTrigger as CommandMenuTriggerPrimitive,
CommandProvider,
PageType,
useRegisterCommands,
useRegisterPage,
useSetPage,
} from 'ui-patterns/CommandMenu'
function CustomSubpage() {
return (
<div className="p-5">
<Breadcrumb className="mb-4" />
<h1 className="font-heading text-xl mb-4 text-foreground">Custom subpage</h1>
<p className="text-foreground-light text-sm">
Here is a custom subpage built from a component that you provide.
</p>
</div>
)
}
function Commands() {
const setPage = useSetPage()
useRegisterPage('Subpage', {
type: PageType.Component,
component: CustomSubpage,
})
useRegisterCommands('Commands', [
{
id: 'subpage',
name: 'Go to subpage',
action: () => setPage('Subpage'),
},
])
return null
}
function CommandMenuTrigger() {
return (
<CommandMenuTriggerPrimitive>
<Button>Open command menu</Button>
</CommandMenuTriggerPrimitive>
)
}
export default function CommandMenuDemo() {
return (
<CommandProvider openKey="">
<Commands />
<CommandMenu trigger={<CommandMenuTrigger />}>
<CommandInput />
<CommandList />
</CommandMenu>
</CommandProvider>
)
}
@@ -0,0 +1,73 @@
import { Button } from '@ui/components/shadcn/ui/button'
import { useMemo } from 'react'
import {
CommandInput,
CommandList,
CommandMenu,
CommandMenuTrigger as CommandMenuTriggerPrimitive,
CommandProvider,
PageType,
useRegisterCommands,
useRegisterPage,
useSetPage,
} from 'ui-patterns/CommandMenu'
function Commands() {
const setPage = useSetPage()
const commands = useMemo(
() => [
{
id: 'subcommand',
name: 'Subcommand',
action: () => alert('Triggered from subpage'),
},
],
[]
)
useRegisterPage(
'Subpage',
{
type: PageType.Commands,
sections: [
{
id: 'subpage',
name: 'Subpage',
commands,
},
],
},
{ deps: [commands] }
)
useRegisterCommands('Commands', [
{
id: 'subpage',
name: 'Go to subpage',
action: () => setPage('Subpage'),
},
])
return null
}
function CommandMenuTrigger() {
return (
<CommandMenuTriggerPrimitive>
<Button>Open command menu</Button>
</CommandMenuTriggerPrimitive>
)
}
export default function CommandMenuDemo() {
return (
<CommandProvider openKey="">
<Commands />
<CommandMenu trigger={<CommandMenuTrigger />}>
<CommandInput />
<CommandList />
</CommandMenu>
</CommandProvider>
)
}
@@ -0,0 +1,16 @@
import { useState } from 'react'
import { ExpandingTextArea } from 'ui'
export default function ExpandingTextareaDemo() {
const [value, setValue] = useState('')
return (
<ExpandingTextArea
placeholder="Type your message in multiple lines here."
value={value}
onChange={(event) => {
setValue(event.target.value)
}}
/>
)
}
@@ -0,0 +1,21 @@
import { toast } from 'sonner'
import { Button } from 'ui'
export default function SonnerDemo() {
return (
<Button
type="default"
onClick={() =>
toast('Event has been created', {
description: 'Sunday, December 03, 2023 at 9:00 AM',
cancel: {
label: 'Undo',
onClick: () => console.log('Undo'),
},
})
}
>
Show Toast
</Button>
)
}
@@ -0,0 +1,97 @@
import { toast } from 'sonner'
import { Button } from 'ui'
export default function SonnerDemo() {
const promise = () =>
new Promise((resolve) => setTimeout(() => resolve({ name: 'Sonner' }), 2000))
return (
<div className="flex gap-1">
<Button type="default" onClick={() => toast('Event has been created')}>
Default
</Button>
<Button
type="default"
onClick={() =>
toast.message('Event has been created', {
description: 'Monday, January 3rd at 6:00pm',
})
}
>
description
</Button>
<Button
type="default"
onClick={() =>
toast.success('Event has been created', {
description: 'Sunday, December 03, 2023 at 9:00 AM',
})
}
>
Success
</Button>
<Button type="default" onClick={() => toast.success('Event has been created')}>
Show Toast
</Button>
<Button
type="default"
onClick={() => toast.info('Be at the area 10 minutes before the event time')}
>
Info
</Button>
<Button
type="warning"
onClick={() => toast.warning('Event start time cannot be earlier than 8am')}
>
Warning
</Button>
<Button type="danger" onClick={() => toast.error('Event has not been created')}>
Error
</Button>
<Button
type="primary"
onClick={() =>
toast('Event has been created', {
action: {
label: 'Action!',
onClick: () => console.log('Undo'),
},
})
}
>
Action
</Button>
<Button type="default" onClick={() => toast.loading('Event has been created')}>
Loading
</Button>
<Button
type="default"
onClick={() =>
toast.promise(promise, {
loading: 'Loading...',
success: (data) => {
// @ts-expect-error
return `${data.name} toast has been added`
},
error: 'Error',
})
}
>
Promise
</Button>
<Button
type="default"
onClick={() =>
toast(
<>
<div>A custom toast with default styling</div>
<Button type="default">Hello world</Button>
</>
)
}
>
Custom
</Button>
</div>
)
}
@@ -0,0 +1,88 @@
import { Check, Loader2 } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { Button, Progress } from 'ui'
export default function SonnerUpload() {
const [isUploading, setIsUploadingState] = useState(false)
const uploadPromise = (files: number, toastId: number) => {
toast.loading(<UploadContent progress={0} files={files} />, {
id: toastId,
})
return new Promise((resolve) => {
let currentProgress = 0
toast.loading(<UploadContent progress={currentProgress} files={files} />, {
id: toastId,
})
const intervalId = setInterval(() => {
currentProgress += Math.floor(Math.random() * 10) + 1 // Increment progress by a random value
if (currentProgress > 100) currentProgress = 100 // Ensure progress does not exceed 100%
if (currentProgress >= 100) {
clearInterval(intervalId) // Clear the interval once upload is complete
resolve({ progress: currentProgress, message: 'Upload complete', files: 3, toastId }) // Resolve the promise when progress reaches 100
}
toast.loading(<UploadContent progress={currentProgress} files={files} />, {
id: toastId,
})
}, 200) // Adjust the interval time as needed
})
}
const UploadContent = ({ progress, files }: { progress: number; files: number }) => (
<div className="flex gap-3 w-full">
{progress >= 100 ? (
<Check className="text-foreground mt-0.5" size={16} strokeWidth={4} />
) : (
<Loader2 className="animate-spin text-foreground-muted mt-0.5" size={16} />
)}
<div className="flex flex-col gap-2 w-full">
<div className="flex w-full justify-between">
<p className="text-foreground text-sm">
{progress >= 100
? `Upload complete`
: files
? `Uploading ${files} files...`
: `Uploading...`}
</p>
<p className="text-foreground-light text-sm font-mono">{`${progress}%`}</p>
</div>
<Progress value={progress} className="w-full" />
<small className="text-foreground-muted text-xs">
Please do not close the browser until completed
</small>
</div>
</div>
)
return (
<div className="flex flex-col gap-3">
<Button
type="default"
onClick={async () => {
// random id
const toastId = Math.random()
const max = 20 // specify the maximum value
const files = Math.floor(Math.random() * max)
try {
setIsUploadingState(true)
const promise = await uploadPromise(files, toastId)
// @ts-expect-error
toast.success(<UploadContent progress={promise.progress} files={promise.files} />, {
duration: 2000,
id: toastId,
})
setIsUploadingState(false)
} catch (error) {}
}}
>
Start upload
</Button>
</div>
)
}
+81 -6
View File
@@ -331,6 +331,54 @@ export const examples: Registry = [
registryDependencies: ['command', 'dialog'],
files: ['example/command-dialog.tsx'],
},
{
name: 'commandmenu-demo',
type: 'components:example',
registryDependencies: ['commandmenu'],
files: ['example/commandmenu-demo.tsx'],
},
{
name: 'commandmenu-badge',
type: 'components:example',
registryDependencies: ['commandmenu'],
files: ['example/commandmenu-badge.tsx'],
},
{
name: 'commandmenu-icon',
type: 'components:example',
registryDependencies: ['commandmenu'],
files: ['example/commandmenu-icon.tsx'],
},
{
name: 'commandmenu-hidden',
type: 'components:example',
registryDependencies: ['commandmenu'],
files: ['example/commandmenu-hidden.tsx'],
},
{
name: 'commandmenu-force',
type: 'components:example',
registryDependencies: ['commandmenu'],
files: ['example/commandmenu-force.tsx'],
},
{
name: 'commandmenu-conditional',
type: 'components:example',
registryDependencies: ['commandmenu'],
files: ['example/commandmenu-conditional.tsx'],
},
{
name: 'commandmenu-subpage',
type: 'components:example',
registryDependencies: ['commandmenu'],
files: ['example/commandmenu-subpage.tsx'],
},
{
name: 'commandmenu-subpage-custom',
type: 'components:example',
registryDependencies: ['commandmenu'],
files: ['example/commandmenu-subpage-custom.tsx'],
},
{
name: 'context-menu-demo',
type: 'components:example',
@@ -665,12 +713,24 @@ export const examples: Registry = [
registryDependencies: ['slider'],
files: ['example/slider-demo.tsx'],
},
// {
// name: 'sonner-demo',
// type: 'components:example',
// registryDependencies: ['sonner'],
// files: ['example/sonner-demo.tsx'],
// },
{
name: 'sonner-demo',
type: 'components:example',
registryDependencies: ['sonner'],
files: ['example/sonner-demo.tsx'],
},
{
name: 'sonner-types',
type: 'components:example',
registryDependencies: ['sonner'],
files: ['example/sonner-types.tsx'],
},
{
name: 'sonner-upload',
type: 'components:example',
registryDependencies: ['sonner'],
files: ['example/sonner-upload.tsx'],
},
{
name: 'switch-demo',
type: 'components:example',
@@ -1050,6 +1110,16 @@ export const examples: Registry = [
type: 'components:example',
files: ['example/modal-hide-footer.tsx'],
},
{
name: 'assistant-chat-demo',
type: 'components:example',
files: ['example/assistant-chat-demo.tsx'],
},
{
name: 'assistant-chat-commands',
type: 'components:example',
files: ['example/assistant-chat-commands.tsx'],
},
{
name: 'color-usage-surface-www-and-docs',
type: 'docs:example',
@@ -1065,4 +1135,9 @@ export const examples: Registry = [
type: 'docs:example',
files: ['example/color-usage-surface-studio-frame.tsx'],
},
{
name: 'expanding-textarea-demo',
type: 'docs:example',
files: ['example/expanding-textarea-demo.tsx'],
},
]
@@ -0,0 +1,82 @@
'use client'
import { Menu } from 'lucide-react'
import type { HTMLAttributes } from 'react'
import { useEffect, useState } from 'react'
import { useBreakpoint } from 'common'
import { cn, Popover_Shadcn_, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_ } from 'ui'
interface TocItem extends HTMLAttributes<HTMLElement> {
label: string
anchor: string
}
export function ContributingToc({ className }: { className?: string }) {
const mobileToc = useBreakpoint('xl')
const [tocItems, setTocItems] = useState<Array<TocItem>>([])
useEffect(() => {
const headings = [
...document.querySelectorAll('article.prose > h2,h3'),
] as Array<HTMLHeadingElement>
const tocItems = headings
.filter((heading) => !!heading.id)
.map((heading) => ({
label: heading.textContent.substring(0, heading.textContent.length - 1), // Remove ending `#`
anchor: heading.id,
}))
setTocItems(tocItems)
}, [])
return mobileToc ? (
<MobileToc
items={tocItems}
className={cn(
'[--local-top-spacing:2rem]',
'fixed top-[calc(var(--header-height)+var(--local-top-spacing))] right-8'
)}
/>
) : (
<TocBase
items={tocItems}
className={cn(
'[--local-top-spacing:5rem]',
'fixed top-[calc(var(--header-height)+var(--local-top-spacing))] right-8 w-36',
'xl:border-l border-foreground-muted'
)}
/>
)
}
function MobileToc({ items, className }: { items: Array<TocItem>; className?: string }) {
const [open, setOpen] = useState(false)
return (
<Popover_Shadcn_ open={open} onOpenChange={setOpen}>
<PopoverTrigger_Shadcn_ className={cn('border rounded p-2', className)}>
<Menu />
<span className="sr-only">
{open ? 'Close table of contents' : 'Open table of contents'}
</span>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ align="end" className="w-48">
<TocBase items={items} />
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
}
function TocBase({ items, className }: { items: Array<TocItem>; className?: string }) {
return (
<nav aria-label="Table of contents" className={cn('px-4 text-foreground-light', className)}>
<ul className="flex flex-col gap-1">
{items.map((item) => (
<li key={item.anchor} className="overflow-hidden truncate">
<a href={`#${item.anchor}`}>{item.label}</a>
</li>
))}
</ul>
</nav>
)
}
+399
View File
@@ -0,0 +1,399 @@
# Contributing to Supabase Docs
Thanks for contributing to Supabase Docs! Here are a few resources to help you get started.
The code and content for our docs site are located in the main [Supabase GitHub repo](https://github.com/supabase/supabase), under the `apps/docs` directory.
In the repo, you'll also find:
- The [developers guide](https://github.com/supabase/supabase/blob/master/apps/docs/DEVELOPERS.md), which will help you set up your local machine to develop the docs site
- The [contributing guide](https://github.com/supabase/supabase/blob/master/apps/docs/CONTRIBUTING.md), which goes over the content organization and some general guidelines for writing docs content
## Components
Our docs content is mainly written in MDX. Aside from standard GitHub-flavored Markdown, you can use the following helper components to help you organize and display your content:
### Accordion
For content that requires progressive disclosure:
```mdx
<Accordion
type="default"
openBehaviour="multiple"
chevronAlign="right"
justified
size="medium"
className="text-foreground-light mt-8 mb-6"
>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="Accordion item 1"
id="item-1"
>
Your content here.
</AccordionItem>
</div>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="Accordion item 2"
id="item-2"
>
More content here.
</AccordionItem>
</div>
</Accordion>
```
<Accordion
type="default"
openBehaviour="multiple"
chevronAlign="right"
justified
size="medium"
className="text-foreground-light mt-8 mb-6"
>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="Accordion item 1"
id="item-1"
>
Your content here.
</AccordionItem>
</div>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="Accordion item 2"
id="item-2"
>
More content here.
</AccordionItem>
</div>
</Accordion>
### Admonition
For extra information that doesn't fit into the main flow. There are 5 supported types of admonitions:
- `danger` to warn the user about any missteps that could cause data loss or data leaks
- `deprecation` to notify the user about features that are (or will soon be) deprecated
- `caution` to warn about anything that could cause a bug or serious user inconvenience
- `tip` to point out helpful but optional actions
- `note` for anything else
Leave a blank line between the admonition tag and the contained content. This will prevent Prettier from trying to break the lines within the content.
```mdx
<Admonition type="danger">
This could lead to data loss!
</Admonition>
<Admonition type="deprecation">
This feature is deprecated.
</Admonition>
<Admonition type="caution">
You should make sure you don't set this up wrong.
</Admonition>
<Admonition type="tip">
In certain cases, you may want to do this.
</Admonition>
<Admonition type="note">
Additional helpful information.
</Admonition>
```
<Admonition type="danger">
This could lead to data loss!
</Admonition>
<Admonition type="deprecation">
This feature is deprecated.
</Admonition>
<Admonition type="caution">
You should make sure you don't set this up wrong.
</Admonition>
<Admonition type="tip">
In certain cases, you may want to do this.
</Admonition>
<Admonition type="note">
Additional helpful information.
</Admonition>
### Icons
The following icons are available. They can be styled with [Tailwind](https://tailwindcss.com/) classes:
```mdx
<IconArrowDown />
<IconCheck />
<IconX />
```
<div class="flex items-center gap-2">
<IconArrowDown />
<IconCheck />
<IconX />
</div>
### Image
You can include images with regular Markdown syntax:
```mdx
![Supabase architectural diagram](/docs/img/supabase-architecture.svg)
```
![Supabase architectural diagram](/docs/img/supabase-architecture.svg)
If your image has alternate light and dark versions, or you want to make it zoomable, you can also use the image component:
```mdx
<Image
alt="Supabase architectural diagram"
src={{
dark: '/docs/img/supabase-architecture.svg',
light: '/docs/img/supabase-architecture--light.svg',
}}
zoomable
/>
```
<Image
alt="Supabase architectural diagram"
src={{
dark: '/docs/img/supabase-architecture.svg',
light: '/docs/img/supabase-architecture--light.svg',
}}
zoomable
/>
### Project Variables
Some guides and tutorials will require that users copy their Supabase project URL and anon key. You can provide those inline if the user is signed in:
```mdx
<ProjectConfigVariables variable="url" />
<ProjectConfigVariables variable="anonKey" />
```
<ProjectConfigVariables variable="url" />
<ProjectConfigVariables variable="anonKey" />
### Step Hike
For tutorials, which feature step-by-step instructions, often with accompanying code, we use the `StepHike` pattern:
````mdx
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="The first step">
Explanation of what to do first.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sql
select ...
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="No code in this step" fullWidth>
Explanation of what to do next. This stretches the full width of the section: Sweet tiramisu apple biscuit candy cake. Orange ipsum muffin cookie cake biscuit. Orange muffin vanilla sweet sugar candy. Sprinkles jelly sweet orange candy cream.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
````
<StepHikeCompact>
<StepHikeCompact.Step step={1}>
<StepHikeCompact.Details title="The first step">
Explanation of what to do first.
</StepHikeCompact.Details>
<StepHikeCompact.Code>
```sql
select ...
```
</StepHikeCompact.Code>
</StepHikeCompact.Step>
<StepHikeCompact.Step step={2}>
<StepHikeCompact.Details title="No code in this step" fullWidth>
Explanation of what to do next. This stretches the full width of the section: Sweet tiramisu apple biscuit candy cake. Orange ipsum muffin cookie cake biscuit. Orange muffin vanilla sweet sugar candy. Sprinkles jelly sweet orange candy cream.
</StepHikeCompact.Details>
</StepHikeCompact.Step>
</StepHikeCompact>
### Tabs
Use tabs when users can select between multiple versions of the content. For example, the content might differ based on language or package manager.
If you include the `queryGroup` prop, the user's selection will sync with other tab groups. Leave out this prop to omit this behavior.
````mdx
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
const supabase = createSupabaseClient()
```
</TabPanel>
<TabPanel id="dart" label="Dart">
```dart
void main() async {
Supabase.initialize();
}
```
</TabPanel>
</Tabs>
````
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
const supabase = createSupabaseClient()
```
</TabPanel>
<TabPanel id="dart" label="Dart">
```dart
void main() async {
Supabase.initialize();
}
```
</TabPanel>
</Tabs>
## Partials
We incorporate content reuse in the docs to avoid duplication. If you find yourself writing the same content over and over, you can put it in a partial instead. Here are some examples of commonly used partials:
<Accordion
type="default"
openBehaviour="multiple"
chevronAlign="right"
justified
size="medium"
className="text-foreground-light mt-8 mb-6"
>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="Database setup"
id="database-setup"
>
```mdx
<DatabaseSetup />
```
<DatabaseSetup />
</AccordionItem>
</div>
<div className="border-b mt-3 pb-3">
<AccordionItem
header="Create client for Auth"
id="create-client-auth"
>
```mdx
<CreateClientSnippet />
```
<CreateClientSnippet />
</AccordionItem>
</div>
</Accordion>
To make a new partial:
1. Make a new MDX file in `apps/docs/components/MDX`.
1. Write your reusable content.
1. Inside `apps/docs/components/MDX/partials.tsx`, import and re-export your partial.
1. Inside `apps/docs/features/docs/mdx.shared.tsx`, import your partial and include it in the `components` object.
1. You can now use your partial inside any other MDX file by using: `<YourPartialName />`.
+26
View File
@@ -0,0 +1,26 @@
import { readFile } from 'node:fs/promises'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { ContributingToc } from '~/app/contributing/ContributingToC'
import { MDXProviderGuides } from '~/features/docs/GuidesMdx.client'
import { MDXRemoteBase } from '~/features/docs/MdxBase'
import { SidebarSkeleton } from '~/layouts/MainSkeleton'
export default async function ContributingPage() {
const contentFile = join(dirname(fileURLToPath(import.meta.url)), 'content.mdx')
const content = await readFile(contentFile, 'utf-8')
return (
<SidebarSkeleton>
<div className="px-8 py-16">
<article className="prose mx-auto">
<MDXProviderGuides>
<MDXRemoteBase source={content} />
</MDXProviderGuides>
</article>
<ContributingToc />
</div>
</SidebarSkeleton>
)
}
+22
View File
@@ -0,0 +1,22 @@
'use client'
import Link from 'next/link'
import { Button } from 'ui'
const ErrorPage = () => (
<div className="h-[calc(100vh-var(--header-height))] w-full flex flex-col gap-8 p-8 items-center justify-center">
<span className="text-center text-5xl text-foreground-lighter">
Sorry, something went wrong
</span>
<div className="flex flex-row items-center gap-4">
<Button asChild type="secondary" className="w-fit p-4 text-lg">
<Link href="/">Return to homepage</Link>
</Button>
<Button type="secondary" className="w-fit p-4 text-lg" onClick={() => location.reload()}>
Refresh page
</Button>
</div>
</div>
)
export default ErrorPage
+19
View File
@@ -0,0 +1,19 @@
import {
getGuidesMarkdown,
genGuideMeta,
genGuidesStaticParams,
} from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate } from '~/features/docs/GuidesMdx.template'
type Params = { slug: string[] }
const GuidePage = async ({ params }: { params: Params }) => {
const data = await getGuidesMarkdown(params)
return <GuideTemplate {...data!} />
}
const generateStaticParams = genGuidesStaticParams()
const generateMetadata = genGuideMeta(getGuidesMarkdown)
export default GuidePage
export { generateStaticParams, generateMetadata }
@@ -0,0 +1,134 @@
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
import { redirect } from 'next/navigation'
import { relative } from 'path'
import rehypeSlug from 'rehype-slug'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { notFoundLink } from '~/features/recommendations/NotFound.utils'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'vecs'
const branch = 'main'
const docsDir = 'docs'
const externalSite = 'https://supabase.github.io/vecs'
// Each external docs page is mapped to a local page
const pageMap = [
{
slug: 'api',
meta: {
title: 'API',
},
remoteFile: 'api.md',
},
{
slug: 'collections',
meta: {
title: 'Collections',
},
remoteFile: 'concepts_collections.md',
},
{
slug: 'indexes',
meta: {
title: 'Indexes',
},
remoteFile: 'concepts_indexes.md',
},
{
slug: 'metadata',
meta: {
title: 'Metadata',
},
remoteFile: 'concepts_metadata.md',
},
]
interface Params {
slug: string
}
const PythonClientDocs = async ({ params }: { params: Params }) => {
const { meta, ...data } = await getContent(params)
const options = {
mdxOptions: {
remarkPlugins: [remarkMkDocsAdmonition, [removeTitle, meta.title]],
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
},
} as SerializeOptions
return <GuideTemplate mdxOptions={options} meta={meta} {...data} />
}
/**
* Fetch markdown from external repo
*/
const getContent = async ({ slug }: Params) => {
const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug)
if (!page) {
redirect(notFoundLink(`api/python/${slug}`))
}
const { remoteFile, meta } = page
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
const response = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
)
const content = await response.text()
return {
pathname: `/guides/ai/python/${slug}` satisfies `/${string}`,
meta,
content,
editLink,
}
}
const urlTransform: UrlTransformFunction = (url) => {
try {
const externalSiteUrl = new URL(externalSite)
const placeholderHostname = 'placeholder'
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
// Don't modify a url with a FQDN or a url that's only a hash
if (hostname !== placeholderHostname || pathname === '/') {
return url
}
const relativePage = (
pathname.endsWith('.md')
? pathname.replace(/\.md$/, '')
: relative(externalSiteUrl.pathname, pathname)
).replace(/^\//, '')
const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile)
// If we have a mapping for this page, use the mapped path
if (page) {
return page.slug + hash
}
// If we don't have this page in our docs, link to original docs
return `${externalSite}/${relativePage}${hash}`
} catch (err) {
console.error('Error transforming markdown URL', err)
return url
}
}
const generateStaticParams = () => pageMap.map(({ slug }) => ({ slug }))
const generateMetadata = genGuideMeta(getContent)
export default PythonClientDocs
export { generateStaticParams, generateMetadata }
@@ -1,60 +1,45 @@
import Head from 'next/head'
import ReactMarkdown from 'react-markdown'
import { CodeBlock, cn } from 'ui'
import { CodeBlock } from 'ui'
import { Heading } from 'ui/src/components/CustomHTMLElements'
import GuidesTableOfContents from '~/components/GuidesTableOfContents'
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import { MainSkeleton } from '~/layouts/MainSkeleton'
import { Parameter } from '~/lib/refGenerator/refTypes'
import { type TOCHeader } from '~/components/GuidesTableOfContents'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import type { Parameter } from '~/lib/refGenerator/refTypes'
import specFile from '~/spec/cli_v1_config.yaml' assert { type: 'yml' }
// Parameters are grouped on the page by tag
const tocList = []
const content = specFile.info.tags.map((tag) => {
tocList.push({ text: tag.title, link: `${tag.id}-config`, level: 2 })
const meta = {
title: 'Supabase CLI config',
}
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/cli/config',
meta,
}))
const tocList: TOCHeader[] = []
const content = specFile.info.tags.map((tag: { id: string; title: string }, id: number) => {
tocList.push({ id: `${id}`, text: tag.title, link: `${tag.id}-config`, level: 2 })
return (
<div>
<Heading tag="h2">{tag.title} Config</Heading>
{specFile.parameters
.filter((param: Parameter) => param.tags[0] === tag.id)
.map((parameter: Parameter) => {
tocList.push({ text: parameter.id, link: `#${parameter.id}`, level: 3 })
.filter((param: Parameter) => param.tags && param.tags[0] === tag.id)
.map((parameter: Parameter, id: number) => {
tocList.push({ id: `${id}`, text: parameter.id, link: `#${parameter.id}`, level: 3 })
return <Info parameter={parameter} />
})}
</div>
)
})
export default function Config() {
const Config = () => {
const editLink = newEditLink('supabase/supabase/blob/master/apps/docs/spec/cli_v1_config.yaml')
return (
<>
<Head>
<title>Supabase CLI config</title>
</Head>
<MainSkeleton menuId={MenuId.Cli}>
<div className="grid grid-cols-12 relative gap-4 px-5 max-w-7xl mx-auto py-16">
<div className="relative col-span-12 md:col-span-9 transition-all ease-out duration-100">
<div className="w-full prose">
<h1>CLI configuration</h1>
<div className="max-w-xs w-32 h-[1px] bg-gradient-to-r from-brand-300 to-brand my-8"></div>
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
<div>{content}</div>
</div>
</div>
<GuidesTableOfContents
overrideToc={tocList}
className={cn(
'col-span-3 self-start',
'hidden md:block md:col-span-3',
'sticky top-[calc(var(--header-height,40px)+2rem)]',
'max-h-[calc(100vh-60px-5rem)]'
)}
/>
</div>
</MainSkeleton>
</>
<GuideTemplate meta={meta} editLink={editLink}>
<ReactMarkdown>{specFile.info.description}</ReactMarkdown>
<div>{content}</div>
</GuideTemplate>
)
}
@@ -81,7 +66,7 @@ function Info({ parameter }: { parameter: Parameter }) {
<tr>
<td>{parameter.id}</td>
<td>{parameter.default ? parameter.default.toString() : 'None'}</td>
<td>{parameter.required.toString()}</td>
<td>{parameter.required !== undefined && parameter.required.toString()}</td>
</tr>
</tbody>
</table>
@@ -116,3 +101,6 @@ function Info({ parameter }: { parameter: Parameter }) {
</div>
)
}
export default Config
export { generateMetadata }
@@ -0,0 +1,134 @@
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
import { redirect } from 'next/navigation'
import { relative } from 'node:path'
import rehypeSlug from 'rehype-slug'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { notFoundLink } from '~/features/recommendations/NotFound.utils'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'setup-cli'
const branch = 'gh-pages'
const docsDir = 'docs'
const externalSite = 'https://supabase.github.io/setup-cli'
// Each external docs page is mapped to a local page
const pageMap = [
{
slug: 'generating-types',
meta: {
title: 'Generate types using GitHub Actions',
description: 'End-to-end type safety across client, server, and database.',
subtitle: 'End-to-end type safety across client, server, and database.',
tocVideo: 'VSNgAIObBdw',
},
remoteFile: 'generating-types.md',
},
{
slug: 'testing',
meta: {
title: 'Automated testing using GitHub Actions',
description: 'Run your tests when you or your team make changes.',
subtitle: 'Run your tests when you or your team make changes.',
},
remoteFile: 'testing.md',
},
{
slug: 'backups',
meta: {
title: 'Automated backups using GitHub Actions',
description: 'Backup your database on a regular basis.',
subtitle: 'Backup your database on a regular basis.',
},
remoteFile: 'backups.md',
},
]
type Params = { slug: string }
const ActionDocs = async ({ params }: { params: Params }) => {
const { meta, ...data } = await getContent(params)
const options = {
mdxOptions: {
remarkPlugins: [remarkMkDocsAdmonition, remarkPyMdownTabs, [removeTitle, meta.title]],
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
},
} as SerializeOptions
return <GuideTemplate mdxOptions={options} meta={meta} {...data} />
}
/**
* Fetch markdown from external repo
*/
const getContent = async ({ slug }: Params) => {
const page = pageMap.find(({ slug: validSlug }) => validSlug && validSlug === slug)
if (!page) {
redirect(notFoundLink(`cli/github-action/${slug}`))
}
const { remoteFile, meta } = page
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
const response = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
)
const content = await response.text()
return {
pathname: `/guides/cli/github-action/${slug}` satisfies `/${string}`,
meta,
content,
editLink,
}
}
const urlTransform: UrlTransformFunction = (url) => {
try {
const externalSiteUrl = new URL(externalSite)
const placeholderHostname = 'placeholder'
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
// Don't modify a url with a FQDN or a url that's only a hash
if (hostname !== placeholderHostname || pathname === '/') {
return url
}
const relativePage = (
pathname.endsWith('.md')
? pathname.replace(/\.md$/, '')
: relative(externalSiteUrl.pathname, pathname)
).replace(/^\//, '')
const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile)
// If we have a mapping for this page, use the mapped path
if (page) {
return page.slug + hash
}
// If we don't have this page in our docs, link to original docs
return `${externalSite}/${relativePage}${hash}`
} catch (err) {
console.error('Error transforming markdown URL', err)
return url
}
}
const generateStaticParams = () => pageMap.map(({ slug }) => ({ slug }))
const generateMetadata = genGuideMeta(getContent)
export default ActionDocs
export { generateStaticParams, generateMetadata }
@@ -1,36 +1,37 @@
import { CodeHikeConfig, remarkCodeHike } from '@code-hike/mdx'
import { Octokit } from '@octokit/core'
import { capitalize } from 'lodash'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { MDXRemote } from 'next-mdx-remote'
import { serialize } from 'next-mdx-remote/serialize'
import remarkGfm from 'remark-gfm'
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
import rehypeSlug from 'rehype-slug'
import codeHikeTheme from 'config/code-hike.theme.json' assert { type: 'json' }
import components from '~/components'
import { Heading } from 'ui'
import { TabPanel, Tabs } from '~/components/Tabs'
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import Layout from '~/layouts/DefaultGuideLayout'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { MDXRemoteBase } from '~/features/docs/MdxBase'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { Tabs, TabPanel } from '~/features/ui/Tabs'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
// We fetch these docs at build time from an external repo
export const org = 'supabase'
export const repo = 'splinter'
export const branch = 'main'
export const docsDir = 'docs'
const org = 'supabase'
const repo = 'splinter'
const branch = 'main'
const docsDir = 'docs'
const meta = {
title: 'Performance and Security Advisors',
subtitle: 'Check your database for performance and security issues',
}
const editLink = 'https://github.com/supabase/splinter/tree/main/docs'
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/database/database-linter',
meta,
}))
const editLink = newEditLink('supabase/splinter/tree/main/docs')
const markdownIntro = `
You can use the Database Performance and Security Advisors to check your database for issues such as missing indexes and improperly set-up RLS policies.
@@ -40,15 +41,21 @@ You can use the Database Performance and Security Advisors to check your databas
In the dashboard, navigate to [Security Advisor](https://supabase.com/dashboard/project/_/database/security-advisor) and [Performance Advisor](https://supabase.com/dashboard/project/_/database/performance-advisor) under Database. The advisors run automatically. You can also manually rerun them after you've resolved issues.
`.trim()
const getBasename = (path: string) => path.split('/').at(-1).replace(/\.md$/, '')
const getBasename = (path: string) => path.split('/').at(-1)!.replace(/\.md$/, '')
const DatabaseAdvisorDocs = async () => {
const { lints, lintsList } = await getLints()
const options = {
mdxOptions: {
remarkPlugins: [remarkMkDocsAdmonition, remarkPyMdownTabs, [removeTitle, meta.title]],
rehypePlugins: [[linkTransform, urlTransform(lintsList)], rehypeSlug],
},
} as SerializeOptions
export default function DatabaseAdvisorDocs({
intro,
lints,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<Layout menuId={MenuId.Database} meta={meta} editLink={editLink}>
<MDXRemote {...intro} components={components} />
<GuideTemplate meta={meta} editLink={editLink}>
<MDXRemoteBase source={markdownIntro} />
<Heading tag="h2">Available checks</Heading>
<Tabs listClassNames="flex flex-wrap gap-2 [&>button]:!m-0" queryGroup="lint">
{lints.map((lint) => (
@@ -58,12 +65,12 @@ export default function DatabaseAdvisorDocs({
label={capitalize(getBasename(lint.path).replace(/_/g, ' '))}
>
<section id={getBasename(lint.path)}>
<MDXRemote {...lint.content} components={components} />
<MDXRemoteBase source={lint.content} options={options} />
</section>
</TabPanel>
))}
</Tabs>
</Layout>
</GuideTemplate>
)
}
@@ -101,42 +108,11 @@ const urlTransform: (lints: Array<{ path: string }>) => UrlTransformFunction = (
}
}
const transformMarkdown = async (
rawContent: string,
{ replacementLinks = [] }: { replacementLinks?: Array<{ path: string }> } = {}
) => {
const codeHikeOptions: CodeHikeConfig = {
theme: codeHikeTheme,
lineNumbers: true,
showCopyButton: true,
skipLanguages: [],
autoImport: false,
}
const content = await serialize(rawContent, {
scope: {
chCodeConfig: codeHikeOptions,
},
mdxOptions: {
remarkPlugins: [
remarkGfm,
remarkMkDocsAdmonition,
remarkPyMdownTabs,
[removeTitle, meta.title],
[remarkCodeHike, codeHikeOptions],
],
rehypePlugins: [[linkTransform, urlTransform(replacementLinks)], rehypeSlug],
},
})
return content
}
/**
* Fetch markdown from external repo and transform links
* Fetch lint remediation Markdown from external repo
*/
export const getStaticProps = (async () => {
const octokit = new Octokit()
const getLints = async () => {
const octokit = new Octokit({ request: { fetch: fetchRevalidatePerDay } })
const response = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', {
owner: org,
@@ -154,37 +130,33 @@ export const getStaticProps = (async () => {
if (!Array.isArray(response.data)) {
throw Error(
`Reading a directory, not a file. Should not reach this, solely to appease Typescript.`
'Reading a directory, not a file. Should not reach this, solely to appease Typescript.'
)
}
const [intro, ...lints] = await Promise.all([
await transformMarkdown(markdownIntro),
...response.data
.filter(({ path }) => /docs\/\d+.+\.md$/.test(path))
.map(async ({ path }, _, data) => {
const fileResponse = await fetch(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${path}`
)
const lintsList = response.data.filter(({ path }) => /docs\/\d+.+\.md$/.test(path))
if (fileResponse.status >= 400) {
throw Error(`Could not get contents of file ${org}/${repo}/${path}`)
}
const lints = await Promise.all(
lintsList.map(async ({ path }) => {
const fileResponse = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${path}`
)
const rawContent = await fileResponse.text()
const content = await transformMarkdown(rawContent, { replacementLinks: data })
if (fileResponse.status >= 400) {
throw Error(`Could not get contents of file ${org}/${repo}/${path}`)
}
return {
path: getBasename(path),
content,
}
}),
])
const content = await fileResponse.text()
return {
props: {
intro,
lints,
},
}
}) satisfies GetStaticProps
return {
path: getBasename(path),
content,
}
})
)
return { lints, lintsList }
}
export default DatabaseAdvisorDocs
export { generateMetadata }
@@ -0,0 +1,222 @@
import matter from 'gray-matter'
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
import { readFile } from 'node:fs/promises'
import { join, relative } from 'node:path'
import rehypeSlug from 'rehype-slug'
import emoji from 'remark-emoji'
import { genGuideMeta, genGuidesStaticParams } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { GUIDES_DIRECTORY, isValidGuideFrontmatter } from '~/lib/docs'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'wrappers'
const branch = 'main'
const docsDir = 'docs'
const externalSite = 'https://supabase.github.io/wrappers'
// Each external docs page is mapped to a local page
const pageMap = [
{
slug: 'airtable',
meta: {
title: 'Airtable',
},
remoteFile: 'airtable.md',
},
{
slug: 'auth0',
meta: {
title: 'Auth0',
},
remoteFile: 'auth0.md',
},
{
slug: 'bigquery',
meta: {
title: 'BigQuery',
},
remoteFile: 'bigquery.md',
},
{
slug: 'clickhouse',
meta: {
title: 'ClickHouse',
},
remoteFile: 'clickhouse.md',
},
{
slug: 'cognito',
meta: {
title: 'AWS Cognito',
},
remoteFile: 'cognito.md',
},
{
slug: 'firebase',
meta: {
title: 'Firebase',
},
remoteFile: 'firebase.md',
},
{
slug: 'logflare',
meta: {
title: 'Logflare',
},
remoteFile: 'logflare.md',
},
{
slug: 'mssql',
meta: {
title: 'MSSQL',
},
remoteFile: 'mssql.md',
},
{
slug: 'redis',
meta: {
title: 'Redis',
},
remoteFile: 'redis.md',
},
{
slug: 's3',
meta: {
title: 'AWS S3',
},
remoteFile: 's3.md',
},
{
slug: 'stripe',
meta: {
title: 'Stripe',
},
remoteFile: 'stripe.md',
},
]
interface Params {
slug?: string[]
}
const WrappersDocs = async ({ params }: { params: Params }) => {
const { isExternal, meta, ...data } = await getContent(params)
const options = isExternal
? ({
mdxOptions: {
remarkPlugins: [
remarkMkDocsAdmonition,
emoji,
remarkPyMdownTabs,
[removeTitle, meta.title],
],
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
},
} as SerializeOptions)
: undefined
return <GuideTemplate meta={meta} mdxOptions={options} {...data} />
}
/**
* Fetch markdown from external repo
*/
const getContent = async (params: Params) => {
const federatedPage = pageMap.find(
({ slug }) => params && slug && params.slug && slug === params.slug.at(0)
)
let isExternal: boolean
let meta: any
let content: string
let editLink: string
if (!federatedPage) {
isExternal = false
editLink = `supabase/supabase/apps/docs/content/guides/database/extensions/wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}.mdx`
const rawContent = await readFile(
join(
GUIDES_DIRECTORY,
'database',
'extensions',
`wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}.mdx`
),
'utf-8'
)
;({ data: meta, content } = matter(rawContent))
if (!isValidGuideFrontmatter(meta)) {
throw Error(`Expected valid frontmatter, got ${JSON.stringify(meta, null, 2)}`)
}
} else {
isExternal = true
let remoteFile: string
;({ remoteFile, meta } = federatedPage)
const repoPath = `${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
editLink = `${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`
const response = await fetchRevalidatePerDay(`https://raw.githubusercontent.com/${repoPath}`)
content = await response.text()
}
return {
pathname:
`/guides/database/extensions/wrappers${params.slug?.length ? `/${params.slug.join('/')}` : ''}` satisfies `/${string}`,
isExternal,
editLink: newEditLink(editLink),
meta,
content,
}
}
const urlTransform: UrlTransformFunction = (url) => {
try {
const externalSiteUrl = new URL(externalSite)
const placeholderHostname = 'placeholder'
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
// Don't modify a url with a FQDN or a url that's only a hash
if (hostname !== placeholderHostname || pathname === '/') {
return url
}
const relativePage = (
pathname.endsWith('.md')
? pathname.replace(/\.md$/, '')
: relative(externalSiteUrl.pathname, pathname)
).replace(/^\//, '')
const page = pageMap.find(({ remoteFile }) => `${relativePage}.md` === remoteFile)
// If we have a mapping for this page, use the mapped path
if (page) {
return page.slug + hash
}
// If we don't have this page in our docs, link to original docs
return `${externalSite}/${relativePage}${hash}`
} catch (err) {
console.error('Error transforming markdown URL', err)
return url
}
}
const generateStaticParams = async () => {
const mdxPaths = await genGuidesStaticParams('database/extensions/wrappers')()
const federatedPaths = pageMap.map(({ slug }) => ({
slug: [slug],
}))
return [...mdxPaths, ...federatedPaths]
}
const generateMetadata = genGuideMeta(getContent)
export default WrappersDocs
export { generateStaticParams, generateMetadata }
@@ -0,0 +1,194 @@
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
import { redirect } from 'next/navigation'
import { isAbsolute, relative } from 'path'
import rehypeSlug from 'rehype-slug'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { notFoundLink } from '~/features/recommendations/NotFound.utils'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
// We fetch these docs at build time from an external repo
const org = 'supabase'
const repo = 'pg_graphql'
const branch = 'master'
const docsDir = 'docs'
const externalSite = 'https://supabase.github.io/pg_graphql'
// Each external docs page is mapped to a local page
const pageMap = [
{
meta: {
id: 'graphql-overview',
title: 'GraphQL',
subtitle: 'Autogenerated GraphQL APIs with Postgres.',
},
remoteFile: 'supabase.md',
},
{
slug: 'api',
meta: {
id: 'graphql-api',
title: 'GraphQL API',
subtitle: 'Understanding the core concepts of the GraphQL API.',
},
remoteFile: 'api.md',
},
{
slug: 'views',
meta: {
id: 'graphql-views',
title: 'Views',
subtitle: 'Using Postgres Views with GraphQL.',
},
remoteFile: 'views.md',
},
{
slug: 'functions',
meta: {
id: 'graphql-functions',
title: 'Functions',
subtitle: 'Using Postgres Functions with GraphQL.',
},
remoteFile: 'functions.md',
},
{
slug: 'computed-fields',
meta: {
id: 'graphql-computed-fields',
title: 'Computed Fields',
subtitle: 'Using Postgres Computed Fields with GraphQL.',
},
remoteFile: 'computed-fields.md',
},
{
slug: 'configuration',
meta: {
id: 'graphql-configuration',
title: 'Configuration & Customization',
subtitle: 'Extra configuration options can be set on SQL entities using comment directives.',
},
remoteFile: 'configuration.md',
},
{
slug: 'security',
meta: {
id: 'graphql-security',
title: 'Security',
subtitle: 'Securing your GraphQL API.',
},
remoteFile: 'security.md',
},
{
slug: 'with-apollo',
meta: {
id: 'graphql-with-apollo',
title: 'With Apollo',
subtitle: 'Using pg_grapqhl with Apollo.',
},
remoteFile: 'usage_with_apollo.md',
},
{
slug: 'with-relay',
meta: {
id: 'graphql-with-relay',
title: 'With Relay',
subtitle: 'Using pg_grapqhl with Relay.',
},
remoteFile: 'usage_with_relay.md',
},
]
interface Params {
slug?: string[]
}
const PGGraphQLDocs = async ({ params }: { params: Params }) => {
const { meta, ...data } = await getContent(params)
const options = {
mdxOptions: {
remarkPlugins: [remarkMkDocsAdmonition, remarkPyMdownTabs, [removeTitle, meta.title]],
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
},
} as SerializeOptions
return <GuideTemplate mdxOptions={options} meta={meta} {...data} />
}
/**
* Fetch markdown from external repo and transform links
*/
const getContent = async ({ slug }: Params) => {
const page = pageMap.find((page) => page.slug === slug?.at(0))
if (!page) {
redirect(notFoundLink(`graphql/${slug ? slug.join('/') : ''}`))
}
const { remoteFile, meta } = page
const editLink = newEditLink(`${org}/${repo}/blob/${branch}/${docsDir}/${remoteFile}`)
const response = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/${remoteFile}`
)
const content = await response.text()
return {
pathname: `/guides/graphql${slug?.length ? `/${slug.join('/')}` : ''}` satisfies `/${string}`,
meta,
content,
editLink,
}
}
const urlTransform: UrlTransformFunction = (url) => {
try {
const externalSiteUrl = new URL(externalSite)
const placeholderHostname = 'placeholder'
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
// Don't modify a url with a FQDN or a url that's only a hash
if (hostname !== placeholderHostname || pathname === '/') {
return url
}
const getRelativePath = () => {
if (pathname.endsWith('.md')) {
return pathname.replace(/\.md$/, '')
}
if (isAbsolute(url)) {
return relative(externalSiteUrl.pathname, pathname)
}
return pathname
}
const relativePath = getRelativePath().replace(/^\//, '')
const page = pageMap.find(({ remoteFile }) => `${relativePath}.md` === remoteFile)
// If we have a mapping for this page, use the mapped path
if (page) {
return 'graphql/' + page.slug + hash
}
// If we don't have this page in our docs, link to original docs
return `${externalSite}/${relativePath}${hash}`
} catch (err) {
console.error('Error transforming markdown URL', err)
return url
}
}
const generateStaticParams = async () => pageMap.map(({ slug }) => ({ slug: slug ? [slug] : [] }))
const generateMetadata = genGuideMeta(getContent)
export default PGGraphQLDocs
export { generateStaticParams, generateMetadata }
+16
View File
@@ -0,0 +1,16 @@
'use client'
import { usePathname } from 'next/navigation'
import { type PropsWithChildren } from 'react'
import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils'
import Layout from '~/layouts/guides'
const GuidesLayout = ({ children }: PropsWithChildren) => {
const pathname = usePathname()
const menuId = getMenuId(pathname)
return <Layout menuId={menuId}>{children}</Layout>
}
export default GuidesLayout
@@ -0,0 +1,142 @@
import matter from 'gray-matter'
import { type SerializeOptions } from 'next-mdx-remote/dist/types'
import { redirect } from 'next/navigation'
import rehypeSlug from 'rehype-slug'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { notFoundLink } from '~/features/recommendations/NotFound.utils'
import { isValidGuideFrontmatter } from '~/lib/docs'
import { UrlTransformFunction, linkTransform } from '~/lib/mdx/plugins/rehypeLinkTransform'
import remarkMkDocsAdmonition from '~/lib/mdx/plugins/remarkAdmonition'
import { removeTitle } from '~/lib/mdx/plugins/remarkRemoveTitle'
import remarkPyMdownTabs from '~/lib/mdx/plugins/remarkTabs'
import {
terraformDocsBranch,
terraformDocsDocsDir,
terraformDocsOrg,
terraformDocsRepo,
} from '../terraformConstants'
// Each external docs page is mapped to a local page
const pageMap = [
{
remoteFile: 'README.md',
meta: {
title: 'Terraform Provider',
},
useRoot: true,
},
{
slug: 'tutorial',
remoteFile: 'tutorial.md',
meta: {
title: 'Using the Supabase Terraform Provider',
},
},
]
interface Params {
slug?: string[]
}
const TerraformDocs = async ({ params }: { params: Params }) => {
const { meta, ...data } = await getContent(params)
const options = {
mdxOptions: {
remarkPlugins: [remarkMkDocsAdmonition, remarkPyMdownTabs, [removeTitle, meta.title]],
rehypePlugins: [[linkTransform, urlTransform], rehypeSlug],
},
} as SerializeOptions
return <GuideTemplate mdxOptions={options} meta={meta} {...data} />
}
/**
* The GitHub repo uses relative links, which don't lead to the right locations
* in docs.
*
* @param url The original link, as written in the Markdown file
* @returns The rewritten link
*/
const urlTransform: UrlTransformFunction = (url: string) => {
try {
const placeholderHostname = 'placeholder'
const { hostname, pathname, hash } = new URL(url, `http://${placeholderHostname}`)
// Don't modify a url with a FQDN or a url that's only a hash
if (hostname !== placeholderHostname || pathname === '/') {
return url
}
const getBasename = (pathname: string) =>
pathname.endsWith('.md') ? pathname.replace(/\.md$/, '') : pathname
const stripLeadingPrefix = (pathname: string) => pathname.replace(/^\//, '')
const stripLeadingDocs = (pathname: string) => pathname.replace(/^docs\//, '')
const relativePath = stripLeadingPrefix(getBasename(pathname))
const page = pageMap.find(
({ remoteFile, useRoot }) =>
(useRoot && `${relativePath}.md` === remoteFile) ||
(!useRoot && `${stripLeadingDocs(relativePath)}.md` === remoteFile)
)
if (page) {
return 'terraform' + `/${page.slug}` + hash
}
// If we don't have this page in our docs, link to GitHub repo
return `https://github.com/${terraformDocsOrg}/${terraformDocsRepo}/blob/${terraformDocsBranch}${pathname}${hash}`
} catch (err) {
console.error('Error transforming markdown URL', err)
return url
}
}
/**
* Fetch markdown from external repo
*/
const getContent = async ({ slug }: Params) => {
const [requestedSlug] = slug ?? []
const page = pageMap.find((page) => page.slug === requestedSlug)
if (!page) {
redirect(notFoundLink('platform/terraform' + (slug?.join('/') ?? '')))
}
const { meta, remoteFile, useRoot } = page
const editLink = newEditLink(
`${terraformDocsOrg}/${terraformDocsRepo}/blob/${terraformDocsBranch}/${useRoot ? '' : `${terraformDocsDocsDir}/`}${remoteFile}`
)
let response = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${terraformDocsOrg}/${terraformDocsRepo}/${terraformDocsBranch}/${useRoot ? '' : `${terraformDocsDocsDir}/`}${remoteFile}`
)
let rawContent = await response.text()
// Strip out HTML comments
rawContent = rawContent.replace(/<!--.*?-->/, '')
const { content, data } = matter(rawContent)
Object.assign(meta, data)
if (!isValidGuideFrontmatter(meta)) {
throw Error('Guide frontmatter is invalid.')
}
return {
pathname:
`/guides/platform/terraform${slug?.length ? `/${slug.join('/')}` : ''}` satisfies `/${string}`,
meta,
content,
editLink,
}
}
const generateStaticParams = async () => pageMap.map(({ slug }) => ({ slug: slug ? [slug] : [] }))
const generateMetadata = genGuideMeta(getContent)
export default TerraformDocs
export { generateStaticParams, generateMetadata }
@@ -1,22 +1,36 @@
import { codeBlock } from 'common-tags'
import { GetStaticProps, InferGetStaticPropsType } from 'next'
import { Check, PlusCircle } from 'lucide-react'
import Link from 'next/link'
import ReactMarkdown from 'react-markdown'
import {
CodeBlock,
Heading,
IconCheck,
IconPlusCircle,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
Tabs,
} from 'ui'
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import Layout from '~/layouts/DefaultGuideLayout'
import { org, repo, branch, docsDir } from './[[...slug]]'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { fetchRevalidatePerDay } from '~/features/helpers.fetch'
import { TabPanel, Tabs } from '~/features/ui/Tabs'
import {
terraformDocsBranch,
terraformDocsDocsDir,
terraformDocsOrg,
terraformDocsRepo,
} from '../terraformConstants'
const meta = {
title: 'Terraform Provider reference',
subtitle: 'Resources and data sources available through the Terraform Provider',
}
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/platform/terraform/reference',
meta,
}))
function ProviderSettings({ schema }: { schema: any }) {
const attributes = schema.block.attributes
@@ -63,7 +77,7 @@ function ProviderSettings({ schema }: { schema: any }) {
<td className="align-middle">
{attributes[attribute].optional && (
<>
<IconCheck className="ml-[2.5ch]" />
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
@@ -71,7 +85,7 @@ function ProviderSettings({ schema }: { schema: any }) {
<td className="align-middle">
{attributes[attribute].sensitive && (
<>
<IconCheck className="ml-[2.5ch]" />
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
@@ -92,7 +106,7 @@ function Resources({ schema }: { schema: any }) {
<p>You can configure these resources using the Supabase Terraform provider:</p>
<Tabs>
{Object.keys(schema).map((resource) => (
<Tabs.Panel id={resource} label={resource}>
<TabPanel id={resource} label={resource}>
<Heading tag="h4">Example usage</Heading>
<CodeBlock className="not-prose">{codeBlock`
resource "${resource}" "<label>" {
@@ -137,7 +151,7 @@ function Resources({ schema }: { schema: any }) {
<PopoverTrigger_Shadcn_ asChild>
<button className="flex items-center justify-between gap-2">
Nested type
<IconPlusCircle size={14} className="shrink-0" />
<PlusCircle size={14} className="shrink-0" />
</button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="max-h-[50vh] overflow-auto">
@@ -177,7 +191,7 @@ function Resources({ schema }: { schema: any }) {
<td className="align-middle">
{schema[resource].block.attributes[attribute].required && (
<>
<IconCheck className="ml-[2.5ch]" />
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
@@ -185,7 +199,7 @@ function Resources({ schema }: { schema: any }) {
<td className="align-middle">
{schema[resource].block.attributes[attribute].optional && (
<>
<IconCheck className="ml-[2.5ch]" />
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
@@ -193,7 +207,7 @@ function Resources({ schema }: { schema: any }) {
<td className="align-middle">
{schema[resource].block.attributes[attribute].computed && (
<>
<IconCheck className="ml-[2.5ch]" />
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
@@ -202,7 +216,7 @@ function Resources({ schema }: { schema: any }) {
))}
</tbody>
</table>
</Tabs.Panel>
</TabPanel>
))}
</Tabs>
</section>
@@ -216,7 +230,7 @@ function DataSources({ schema }: { schema: any }) {
<p>You can read these resources using the Supabase Terraform provider:</p>
<Tabs>
{Object.keys(schema).map((dataSource) => (
<Tabs.Panel id={dataSource} label={dataSource}>
<TabPanel id={dataSource} label={dataSource}>
<Heading tag="h4">Example usage</Heading>
<CodeBlock className="not-prose">{codeBlock`
resource "${dataSource}" "all" {
@@ -263,7 +277,7 @@ function DataSources({ schema }: { schema: any }) {
<PopoverTrigger_Shadcn_ asChild>
<button className="flex items-center justify-between gap-2">
Nested type
<IconPlusCircle size={14} />
<PlusCircle size={14} />
</button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_ className="max-h-[50vh] overflow-auto">
@@ -306,7 +320,7 @@ function DataSources({ schema }: { schema: any }) {
<td className="align-middle">
{schema[dataSource].block.attributes[attribute].required && (
<>
<IconCheck className="ml-[2.5ch]" />
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
@@ -314,7 +328,7 @@ function DataSources({ schema }: { schema: any }) {
<td className="align-middle">
{schema[dataSource].block.attributes[attribute].optional && (
<>
<IconCheck className="ml-[2.5ch]" />
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
@@ -322,7 +336,7 @@ function DataSources({ schema }: { schema: any }) {
<td className="align-middle">
{schema[dataSource].block.attributes[attribute].computed && (
<>
<IconCheck className="ml-[2.5ch]" />
<Check className="ml-[2.5ch]" />
<span className="sr-only">true</span>
</>
)}
@@ -331,19 +345,20 @@ function DataSources({ schema }: { schema: any }) {
))}
</tbody>
</table>
</Tabs.Panel>
</TabPanel>
))}
</Tabs>
</section>
)
}
export default function PGGraphQLDocs({
meta,
schema,
}: InferGetStaticPropsType<typeof getStaticProps>) {
const TerraformReferencePage = async () => {
const { schema } = await getSchema()
const editLink = newEditLink('supabase/terraform-provider-supabase')
return (
<Layout menuId={MenuId.Platform} meta={meta}>
<GuideTemplate meta={meta} editLink={editLink}>
The Terraform Provider provices access to{' '}
<Link
href="https://developer.hashicorp.com/terraform/language/resources"
@@ -371,30 +386,25 @@ export default function PGGraphQLDocs({
schema.provider_schemas['registry.terraform.io/supabase/supabase'].data_source_schemas
}
/>
</Layout>
</GuideTemplate>
)
}
/**
* Fetch JSON schema from external repo
*/
export const getStaticProps = (async () => {
const meta = {
title: 'Terraform Provider reference',
subtitle: 'Resources and data sources available through the Terraform Provider',
}
let response = await fetch(
`https://raw.githubusercontent.com/${org}/${repo}/${branch}/${docsDir}/schema.json`
const getSchema = async () => {
let response = await fetchRevalidatePerDay(
`https://raw.githubusercontent.com/${terraformDocsOrg}/${terraformDocsRepo}/${terraformDocsBranch}/${terraformDocsDocsDir}/schema.json`
)
if (!response.ok) throw Error('Failed to fetch Terraform JSON schema from GitHub')
const schema = await response.json()
return {
props: {
meta,
schema,
},
schema,
}
}) satisfies GetStaticProps
}
export default TerraformReferencePage
export { generateMetadata }
@@ -0,0 +1,9 @@
/**
* Information on where to fetch docs content
*/
const terraformDocsOrg = 'supabase'
const terraformDocsRepo = 'terraform-provider-supabase'
const terraformDocsBranch = 'v1.1.3'
const terraformDocsDocsDir = 'docs'
export { terraformDocsOrg, terraformDocsRepo, terraformDocsBranch, terraformDocsDocsDir }
@@ -0,0 +1,65 @@
import Param from '~/components/Params'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { MDXRemoteBase } from '~/features/docs/MdxBase'
import specAnalyticsV0 from '~/spec/analytics_v0_config.yaml' assert { type: 'yml' }
const meta = {
title: 'Analytics Self-hosting Config',
description: 'How to configure and deploy Supabase Analytics.',
}
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/self-hosting/analytics/config',
meta,
}))
const AnalyticsConfigPage = async () => {
const descriptionMdx = specAnalyticsV0.info.description
return (
<GuideTemplate
meta={meta}
editLink={newEditLink(
'supabase/supabase/blob/master/apps/docs/pages/guides/self-hosting/analytics/config.tsx'
)}
>
<MDXRemoteBase source={descriptionMdx} />
<div>
{specAnalyticsV0.info.tags.map(
(tag: ReturnType<typeof specAnalyticsV0>['info']['tags']) => {
return (
<>
<h2 className="text-foreground">{tag.title}</h2>
<p className="text-foreground-lighter">{tag.description}</p>
<div className="not-prose">
<h5 className="text-base text-foreground mb-3">Parameters</h5>
<ul>
{specAnalyticsV0.parameters
.filter((param: ReturnType<typeof specAnalyticsV0>['parameters']) =>
param.tags.includes(tag.id)
)
.map((param: ReturnType<typeof specAnalyticsV0>['parameters']) => {
return (
<Param
name={param.title}
type={param.type}
description={param.description}
required={param.required}
/>
)
})}
</ul>
</div>
</>
)
}
)}
</div>
</GuideTemplate>
)
}
export default AnalyticsConfigPage
export { generateMetadata }
@@ -0,0 +1,63 @@
import Param from '~/components/Params'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { MDXRemoteBase } from '~/features/docs/MdxBase'
import specAuthV1 from '~/spec/gotrue_v1_config.yaml' assert { type: 'yml' }
const meta = {
title: 'Auth Self-hosting Config',
description: 'How to configure and deploy Supabase Auth.',
}
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/self-hosting/auth/config',
meta,
}))
const AuthConfigPage = async () => {
const descriptionMdx = specAuthV1.info.description
return (
<GuideTemplate
meta={meta}
editLink={newEditLink(
'supabase/supabase/blob/master/apps/docs/pages/guides/self-hosting/auth/config.tsx'
)}
>
<MDXRemoteBase source={descriptionMdx} />
<div>
{specAuthV1.info.tags.map((tag: ReturnType<typeof specAuthV1>['info']['tags']) => {
return (
<>
<h2 className="text-foreground">{tag.title}</h2>
<p className="text-foreground-lighter">{tag.description}</p>
<div className="not-prose">
<h5 className="text-base text-foreground mb-3">Parameters</h5>
<ul>
{specAuthV1.parameters
.filter((param: ReturnType<typeof specAuthV1>['parameters']) =>
param.tags.includes(tag.id)
)
.map((param: ReturnType<typeof specAuthV1>['parameters']) => {
return (
<Param
name={param.title}
type={param.type}
description={param.description}
required={param.required}
/>
)
})}
</ul>
</div>
</>
)
})}
</div>
</GuideTemplate>
)
}
export default AuthConfigPage
export { generateMetadata }
@@ -0,0 +1,63 @@
import Param from '~/components/Params'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { MDXRemoteBase } from '~/features/docs/MdxBase'
import specRealtimeV0 from '~/spec/realtime_v0_config.yaml' assert { type: 'yml' }
const meta = {
title: 'Realtime Self-hosting Config',
description: 'How to configure and deploy Supabase Realtime.',
}
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/self-hosting/realtime/config',
meta,
}))
const RealtimeConfigPage = async () => {
const descriptionMdx = specRealtimeV0.info.description
return (
<GuideTemplate
meta={meta}
editLink={newEditLink(
'supabase/supabase/blob/master/apps/docs/pages/guides/self-hosting/realtime/config.tsx'
)}
>
<MDXRemoteBase source={descriptionMdx} />
<div>
{specRealtimeV0.info.tags.map((tag: ReturnType<typeof specRealtimeV0>['info']['tags']) => {
return (
<>
<h2 className="text-foreground">{tag.title}</h2>
<p className="text-foreground-lighter">{tag.description}</p>
<div className="not-prose">
<h5 className="text-base text-foreground mb-3">Parameters</h5>
<ul>
{specRealtimeV0.parameters
.filter((param: ReturnType<typeof specRealtimeV0>['parameters']) =>
param.tags.includes(tag.id)
)
.map((param: ReturnType<typeof specRealtimeV0>['parameters']) => {
return (
<Param
name={param.title}
type={param.type}
description={param.description}
required={param.required}
/>
)
})}
</ul>
</div>
</>
)
})}
</div>
</GuideTemplate>
)
}
export default RealtimeConfigPage
export { generateMetadata }
@@ -0,0 +1,63 @@
import Param from '~/components/Params'
import { genGuideMeta } from '~/features/docs/GuidesMdx.utils'
import { GuideTemplate, newEditLink } from '~/features/docs/GuidesMdx.template'
import { MDXRemoteBase } from '~/features/docs/MdxBase'
import specStorageV0 from '~/spec/storage_v0_config.yaml' assert { type: 'yml' }
const meta = {
title: 'Storage Self-hosting Config',
description: 'How to configure and deploy Supabase Storage.',
}
const generateMetadata = genGuideMeta(() => ({
pathname: '/guides/self-hosting/storage/config',
meta,
}))
const StorageConfigPage = async () => {
const descriptionMdx = specStorageV0.info.description
return (
<GuideTemplate
meta={meta}
editLink={newEditLink(
'supabase/supabase/blob/master/apps/docs/pages/guides/self-hosting/storage/config.tsx'
)}
>
<MDXRemoteBase source={descriptionMdx} />
<div>
{specStorageV0.info.tags.map((tag: ReturnType<typeof specStorageV0>['info']['tags']) => {
return (
<>
<h2 className="text-foreground">{tag.title}</h2>
<p className="text-foreground-lighter">{tag.description}</p>
<div className="not-prose">
<h5 className="text-base text-foreground mb-3">Parameters</h5>
<ul>
{specStorageV0.parameters
.filter((param: ReturnType<typeof specStorageV0>['parameters']) =>
param.tags.includes(tag.id)
)
.map((param: ReturnType<typeof specStorageV0>['parameters']) => {
return (
<Param
name={param.title}
type={param.type}
description={param.description}
required={param.required}
/>
)
})}
</ul>
</div>
</>
)
})}
</div>
</GuideTemplate>
)
}
export default StorageConfigPage
export { generateMetadata }
+58
View File
@@ -0,0 +1,58 @@
import '@code-hike/mdx/styles'
import 'config/code-hike.scss'
import '../styles/main.scss'
import '../styles/new-docs.scss'
import '../styles/prism-okaidia.scss'
import { genFaviconData } from 'common/MetaFavicons/app-router'
import { type Metadata, type Viewport } from 'next'
import { BASE_PATH } from '~/lib/constants'
import { GlobalProviders } from '~/features/app.providers'
import { TopNavSkeleton } from '~/layouts/MainSkeleton'
const metadata: Metadata = {
applicationName: 'Supabase Docs',
title: 'Supabase Docs',
description:
'Supabase is an open source Firebase alternative providing all the backend features you need to build a product.',
metadataBase: new URL('https://supabase.com'),
icons: genFaviconData(BASE_PATH),
robots: {
index: true,
follow: true,
},
openGraph: {
type: 'article',
authors: 'Supabase',
url: `${BASE_PATH}`,
images: `${BASE_PATH}/img/supabase-og-image.png`,
publishedTime: new Date().toISOString(),
modifiedTime: new Date().toISOString(),
},
twitter: {
card: 'summary_large_image',
site: '@supabase',
creator: '@supabase',
images: `${BASE_PATH}/img/supabase-og-image.png`,
},
}
const viewport: Viewport = {
themeColor: '#1E1E1E',
}
const RootLayout = ({ children }: { children: React.ReactNode }) => {
return (
<html lang="en">
<body>
<GlobalProviders>
<TopNavSkeleton>{children}</TopNavSkeleton>
</GlobalProviders>
</body>
</html>
)
}
export { metadata, viewport }
export default RootLayout
+20
View File
@@ -0,0 +1,20 @@
import { type Metadata } from 'next'
import { type PropsWithChildren } from 'react'
import { LayoutMainContent } from '~/layouts/DefaultLayout'
import { SidebarSkeleton } from '~/layouts/MainSkeleton'
const metadata: Metadata = {
title: 'Not found',
robots: {
index: false,
},
}
const NotFoundLayout = ({ children }: PropsWithChildren) => (
<SidebarSkeleton>
<LayoutMainContent>{children}</LayoutMainContent>
</SidebarSkeleton>
)
export default NotFoundLayout
export { metadata }
+47
View File
@@ -0,0 +1,47 @@
import { type Metadata } from 'next'
import Link from 'next/link'
import { Button } from 'ui'
import { SearchButton, Recommendations } from '~/features/recommendations/NotFound.client'
const metadata: Metadata = {
robots: {
index: false,
follow: false,
},
}
const NotFoundPage = async ({ searchParams: { page } }: { searchParams: { page?: string } }) => {
return (
<article className="prose max-w-[80ch]">
<h1>404: We couldn&apos;t find that page</h1>
<p>
Sorry, we couldn&apos;t find that page. It might be missing, or we had a temporary error
generating it.
</p>
<div className="flex flex-wrap gap-4 pt-4">
<SearchButton />
<Button type="default" size="small" className="p-4" asChild>
<Link href="/" className="no-underline">
Return to homepage
</Link>
</Button>
<Button type="text" size="small" asChild>
<Link
href="https://github.com/supabase/supabase/issues/new?assignees=&labels=documentation&projects=&template=2.Improve_docs.md"
target="_blank"
rel="noreferrer noopener"
className="no-underline"
>
Report missing page
</Link>
</Button>
</div>
<Recommendations page={page} />
</article>
)
}
export default NotFoundPage
export { metadata }
+347
View File
@@ -0,0 +1,347 @@
import { type Metadata, type ResolvingMetadata } from 'next'
import Link from 'next/link'
import { IconBackground, TextLink } from 'ui'
import { IconPanel } from 'ui-patterns/IconPanel'
import MenuIconPicker from '~/components/Navigation/NavigationMenu/MenuIconPicker'
import { GlassPanelWithIconPicker } from '~/features/ui/GlassPanelWithIconPicker'
import { IconPanelWithIconPicker } from '~/features/ui/IconPanelWithIconPicker'
import HomeLayout from '~/layouts/HomeLayout'
import { BASE_PATH } from '~/lib/constants'
const generateMetadata = async (_, parent: ResolvingMetadata): Promise<Metadata> => {
const parentAlternates = (await parent).alternates
return {
alternates: {
...parentAlternates,
canonical: `${BASE_PATH}`,
},
}
}
const products = [
{
title: 'Database',
icon: 'database',
hasLightIcon: true,
href: '/guides/database/overview',
description:
'Supabase provides a full Postgres database for every project with Realtime functionality, database backups, extensions, and more.',
},
{
title: 'Auth',
icon: 'auth',
hasLightIcon: true,
href: '/guides/auth',
description:
'Add and manage email and password, passwordless, OAuth, and mobile logins to your project through a suite of identity providers and APIs.',
},
{
title: 'Storage',
icon: 'storage',
hasLightIcon: true,
href: '/guides/storage',
description:
'Store, organize, transform, and serve large files—fully integrated with your Postgres database with Row Level Security access policies.',
},
{
title: 'AI & Vectors',
icon: 'ai',
hasLightIcon: true,
href: '/guides/ai',
description: 'Use Supabase to store and search embedding vectors.',
},
{
title: 'Realtime',
icon: 'realtime',
hasLightIcon: true,
href: '/guides/realtime',
description:
'Listen to database changes, store and sync user states across clients, broadcast data to clients subscribed to a channel, and more.',
},
{
title: 'Edge Functions',
icon: 'edge-functions',
hasLightIcon: true,
href: '/guides/functions',
description:
'Globally distributed, server-side functions to execute your code closest to your users for the lowest latency.',
},
]
const migrationGuides = [
{
title: 'Auth',
icon: '/docs/img/icons/auth0-icon',
href: '/guides/resources/migrating-to-supabase/auth0',
},
{
title: 'Firebase Auth',
icon: '/docs/img/icons/firebase-icon',
href: '/guides/resources/migrating-to-supabase/firebase-auth',
},
{
title: 'Firestore Data',
icon: '/docs/img/icons/firebase-icon',
href: '/guides/resources/migrating-to-supabase/firestore-data',
},
{
title: 'Firebase Storage',
icon: '/docs/img/icons/firebase-icon',
href: '/guides/resources/migrating-to-supabase/firebase-storage',
},
{
title: 'Heroku',
icon: '/docs/img/icons/heroku-icon',
href: '/guides/resources/migrating-to-supabase/heroku',
},
{
title: 'Render',
icon: '/docs/img/icons/render-icon',
href: '/guides/resources/migrating-to-supabase/render',
},
{
title: 'Amazon RDS',
icon: '/docs/img/icons/aws-rds-icon',
href: '/guides/resources/migrating-to-supabase/amazon-rds',
},
{
title: 'Postgres',
icon: '/docs/img/icons/postgres-icon',
href: '/guides/resources/migrating-to-supabase/postgres',
},
{
title: 'MySQL',
icon: '/docs/img/icons/mysql-icon',
href: '/guides/resources/migrating-to-supabase/mysql',
},
{
title: 'MSSQL',
icon: '/docs/img/icons/mssql-icon',
href: '/guides/resources/migrating-to-supabase/mssql',
},
]
const selfHostingOptions = [
{
title: 'Auth',
icon: 'auth',
href: '/reference/self-hosting-auth/introduction',
},
{
title: 'Realtime',
icon: 'realtime',
href: '/reference/self-hosting-realtime/introduction',
},
{
title: 'Storage',
icon: 'storage',
href: '/reference/self-hosting-storage/introduction',
},
{
title: 'Analytics',
icon: 'analytics',
href: '/reference/self-hosting-analytics/introduction',
},
]
const clientLibraries = [
{
title: 'Javascript',
icon: 'reference-javascript',
href: '/reference/javascript/introduction',
},
{
title: 'Flutter',
icon: 'reference-dart',
href: '/reference/dart/introduction',
},
{
title: 'Python',
icon: 'reference-python',
href: '/reference/python/introduction',
},
{
title: 'C#',
icon: 'reference-csharp',
href: '/reference/csharp/introduction',
},
{
title: 'Swift',
icon: 'reference-swift',
href: '/reference/swift/introduction',
},
{
title: 'Kotlin',
icon: 'reference-kotlin',
href: '/reference/kotlin/introduction',
},
]
const additionalResources = [
{
title: 'Management API',
description: 'Manage your Supabase projects and organizations.',
icon: 'reference-api',
href: '/reference/api/introduction',
},
{
title: 'Supabase CLI',
description: 'Use the CLI to develop, manage and deploy your projects.',
icon: 'reference-cli',
href: '/reference/cli/introduction',
},
{
title: 'Platform Guides',
description: 'Learn more about the tools and services powering Supabase.',
icon: 'platform',
href: '/guides/platform',
},
{
title: 'Integrations',
description: 'Explore a variety of integrations from Supabase partners.',
icon: 'integrations',
href: '/guides/integrations',
},
]
const HomePage = () => (
<HomeLayout>
<div className="flex flex-col">
<h2 id="products">Products</h2>
<ul className="grid grid-cols-12 gap-6 not-prose [&_svg]:text-brand-600">
{products.map((product) => {
return (
<li key={product.title} className="col-span-12 md:col-span-4">
<Link href={product.href} passHref>
<GlassPanelWithIconPicker {...product}>
{product.description}
</GlassPanelWithIconPicker>
</Link>
</li>
)
})}
</ul>
<div className="flex flex-col lg:grid grid-cols-12 gap-6 py-12 border-b">
<div className="col-span-4 flex flex-col gap-1 [&_h2]:m-0 [&_h3]:m-0">
<div className="md:max-w-xs 2xl:max-w-none">
<div className="flex items-center gap-3 mb-3 text-brand-600">
<h2 id="client-libraries" className="group scroll-mt-24">
Client Libraries
</h2>
</div>
</div>
</div>
<div className="grid col-span-8 grid-cols-12 gap-6 not-prose">
{clientLibraries.map((library) => {
return (
<Link
href={library.href}
key={library.title}
passHref
className="col-span-6 md:col-span-4"
>
<IconPanelWithIconPicker {...library} />
</Link>
)
})}
</div>
</div>
<div className="flex flex-col lg:grid grid-cols-12 gap-6 py-12 border-b">
<div className="col-span-4 flex flex-col gap-1 [&_h2]:m-0">
<h2 id="migrate-to-supabase" className="group scroll-mt-24">
Migrate to Supabase
</h2>
<p className="text-foreground-light text-sm p-0 m-0">
Bring your existing data, auth and storage to Supabase following our migration guides.
</p>
<TextLink
label="Explore more resources"
url="/guides/resources"
className="no-underline text-brand text-sm"
/>
</div>
<ul className="grid col-span-8 grid-cols-12 gap-6 not-prose">
{migrationGuides.map((guide) => {
return (
<li key={guide.title} className="col-span-6 md:col-span-4">
<Link href={guide.href} passHref>
<IconPanel {...guide} background={true} showLink={false} />
</Link>
</li>
)
})}
</ul>
</div>
<div className="flex flex-col gap-6 py-12 border-b">
<div className="col-span-4 flex flex-col gap-1 [&_h2]:m-0 [&_h3]:m-0">
<h3 id="additional-resources" className="group scroll-mt-24">
Additional resources
</h3>
</div>
<ul className="grid grid-cols-12 gap-6 not-prose">
{additionalResources.map((resource) => {
return (
<li key={resource.title} className="col-span-12 md:col-span-6 lg:col-span-3">
<Link
href={resource.href}
className="col-span-12 md:col-span-6 lg:col-span-3"
passHref
>
<GlassPanelWithIconPicker {...resource} background={false}>
{resource.description}
</GlassPanelWithIconPicker>
</Link>
</li>
)
})}
</ul>
</div>
<div className="flex flex-col lg:grid grid-cols-12 gap-6 py-12">
<div className="col-span-4 flex flex-col gap-1 [&_h2]:m-0 [&_h3]:m-0">
<div className="md:max-w-xs 2xl:max-w-none">
<div className="flex items-center gap-3 mb-3 text-brand-600">
<IconBackground>
<MenuIconPicker icon="self-hosting" width={18} height={18} />
</IconBackground>
<h3 id="self-hosting" className="group scroll-mt-24">
Self-Hosting
</h3>
</div>
<p className="text-foreground-light text-sm">Get started with self-hosting Supabase.</p>
<TextLink
label="More on Self-Hosting"
url="/guides/self-hosting"
className="no-underline text-brand text-sm"
/>
</div>
</div>
<div className="grid col-span-8 grid-cols-12 gap-6 not-prose">
<ul className="col-span-full lg:col-span-8 grid grid-cols-12 gap-6">
{selfHostingOptions.map((option) => {
return (
<li key={option.title} className="col-span-6">
<Link href={option.href} passHref>
<IconPanelWithIconPicker {...option} background={true} showLink={false} />
</Link>
</li>
)
})}
</ul>
</div>
</div>
</div>
</HomeLayout>
)
export default HomePage
export { generateMetadata }
+1 -1
View File
@@ -1,6 +1,6 @@
import { CodeBlock } from '@ui/components/CodeBlock'
import { sample } from '@har-sdk/openapi-sampler'
import { Tabs, TabPanel } from '~/components/Tabs'
import { Tabs, TabPanel } from '~/features/ui/Tabs'
type IParamProps = any
@@ -1,11 +1,9 @@
import { lazy, Suspense } from 'react'
'use client'
const Generator = lazy(() => import('./AppleSecretGenerator'))
import dynamic from 'next/dynamic'
const Generator = dynamic(() => import('./AppleSecretGenerator'), { ssr: false })
export function AppleSecretGenerator() {
return (
<Suspense>
<Generator />
</Suspense>
)
return <Generator />
}
@@ -1,3 +1,5 @@
'use client'
import { useEffect, useReducer, useRef } from 'react'
import { PhoneLoginsItems } from '../Navigation/NavigationMenu/NavigationMenu.constants'
import { IconPanel } from 'ui-patterns/IconPanel'
+159
View File
@@ -0,0 +1,159 @@
'use client'
import React, { Fragment } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import {
Breadcrumb_Shadcn_ as Breadcrumb,
BreadcrumbList_Shadcn_ as BreadcrumbList,
BreadcrumbItem_Shadcn_ as BreadcrumbItem,
BreadcrumbLink_Shadcn_ as BreadcrumbLink,
BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator,
BreadcrumbPage_Shadcn_ as BreadcrumbPage,
BreadcrumbEllipsis_Shadcn_ as BreadcrumbEllipsis,
Button,
cn,
Drawer,
DrawerClose,
DrawerContent,
DrawerFooter,
DrawerTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from 'ui'
import { getMenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu.utils'
import { useBreakpoint } from 'common'
import * as NavItems from './Navigation/NavigationMenu/NavigationMenu.constants'
const Breadcrumbs = ({ className }: { className?: string }) => {
const pathname = usePathname()
const menuId = getMenuId(pathname)
const menu = NavItems[menuId]
const breadcrumbs = findMenuItemByUrl(menu, pathname, [])
const [open, setOpen] = React.useState(false)
const isMobile = useBreakpoint('md')
const ITEMS_TO_DISPLAY = isMobile ? 4 : 3
if (!breadcrumbs?.length || breadcrumbs?.length === 1) return null
const appendedBreadcrumbs = breadcrumbs?.slice(-ITEMS_TO_DISPLAY + 1, isMobile ? -1 : undefined)
return (
<Breadcrumb className={cn(className)}>
<BreadcrumbList className="text-foreground-lighter p-0">
{breadcrumbs.length >= ITEMS_TO_DISPLAY && (
<>
<BreadcrumbItem>
{breadcrumbs[0].url ? (
<BreadcrumbLink href={`/docs${breadcrumbs[0].url}`}>
{breadcrumbs[0].title || breadcrumbs[0].name}
</BreadcrumbLink>
) : (
<BreadcrumbPage>{breadcrumbs[0].title || breadcrumbs[0].name}</BreadcrumbPage>
)}
</BreadcrumbItem>
<BreadcrumbSeparator />
</>
)}
{breadcrumbs.length > ITEMS_TO_DISPLAY && (
<>
<BreadcrumbItem>
{!isMobile ? (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger className="flex items-center gap-1" aria-label="Toggle menu">
<BreadcrumbEllipsis className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{breadcrumbs.slice(1, -2).map((crumb, index) => (
<DropdownMenuItem
key={index}
className={cn(!crumb.url && 'pointer-events-none')}
>
{crumb.url ? (
<Link href={`/docs${crumb.url}`}>{crumb.title || crumb.name}</Link>
) : (
crumb.title || crumb.name
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
) : (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger aria-label="Toggle Menu">
<BreadcrumbEllipsis className="h-4 w-4" />
</DrawerTrigger>
<DrawerContent showHandle={false}>
<div className="grid gap-1 px-4">
{breadcrumbs
.slice(1, -2)
.map((crumb) =>
crumb.url ? (
<Link href={`/docs${crumb.url}`}>{crumb.title || crumb.name}</Link>
) : (
crumb.title || crumb.name
)
)}
</div>
<DrawerFooter className="pt-4">
<DrawerClose asChild>
<Button type="outline">Close</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
)}
</BreadcrumbItem>
<BreadcrumbSeparator />
</>
)}
{appendedBreadcrumbs?.map((crumb, i) => (
<Fragment key={crumb.url}>
<BreadcrumbItem
className={cn(
'flex items-center overflow-hidden',
i === appendedBreadcrumbs.length - 1 && 'md:text-foreground-light'
)}
>
{crumb.url ? (
<BreadcrumbLink href={`/docs${crumb.url}`}>
{crumb.title || crumb.name}
</BreadcrumbLink>
) : (
<BreadcrumbPage>{crumb.title || crumb.name}</BreadcrumbPage>
)}
</BreadcrumbItem>
<BreadcrumbSeparator
className={cn(i === appendedBreadcrumbs.length - 1 && 'md:hidden')}
/>
</Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
)
}
function findMenuItemByUrl(menu, targetUrl, parents = []) {
// If the menu has items, recursively search through them
if (menu.items) {
for (let item of menu.items) {
const result = findMenuItemByUrl(item, targetUrl, [...parents, menu])
if (result) {
return result
}
}
}
// Check if the current menu object itself has the target URL
if (menu.url === targetUrl) {
return [...parents, menu]
}
// If the URL is not found, return null
return null
}
export default Breadcrumbs
+6 -2
View File
@@ -1,6 +1,7 @@
import React, { FC } from 'react'
import Image from 'next/legacy/image'
import Link from 'next/link'
import { cn } from 'ui'
interface Props {
title: string
@@ -9,6 +10,7 @@ interface Props {
icon?: string | any
children?: any
layout?: 'vertical' | 'horizontal'
className?: string
}
const ButtonCard: FC<Props> = ({
@@ -18,14 +20,16 @@ const ButtonCard: FC<Props> = ({
description = '',
to,
layout = 'vertical',
className,
}) => {
return (
<Link
href={to}
className={[
className={cn(
'h-full block shadow-none bg-surface-100 rounded transition',
'border border-transparent hover:border-overlay',
].join(' ')}
className
)}
>
{children ? (
children
+2
View File
@@ -1,3 +1,5 @@
'use client'
import React, { type PropsWithChildren } from 'react'
import { LazyMotion, domAnimation, m } from 'framer-motion'
@@ -58,7 +58,7 @@ export default function Extensions() {
</div>
<div className="lg:grid lg:grid-cols-12">
<div className="col-span-3 not-prose">
<div className="lg:sticky top-24">
<div className="lg:sticky top-32">
<h3 className="text-sm text-foreground-light">Filter</h3>
<ul className="mt-3 flex flex-wrap lg:grid gap-2 grow">
{tags.sort().map((tag) => (
@@ -1,3 +1,5 @@
'use client'
import { lazy, Suspense } from 'react'
const ExtensionsInternal = lazy(() => import('./Extensions'))
+1 -1
View File
@@ -74,7 +74,7 @@ function Feedback() {
const [modalOpen, setModalOpen] = useState(false)
const feedbackButtonRef = useRef<HTMLButtonElement>(null)
const pathname = usePathname()
const pathname = usePathname() ?? ''
const sendTelemetryEvent = useSendTelemetryEvent()
const { mutate: sendFeedbackComment } = useSendFeedbackMutation()
const supabase = useSupabaseClient<Database>()
+17 -5
View File
@@ -1,3 +1,5 @@
'use client'
import { usePathname } from 'next/navigation'
import { useEffect, useState } from 'react'
import { cn } from 'ui'
@@ -20,7 +22,7 @@ const formatSlug = (slug: string) => {
const formatTOCHeader = (content: string) => {
let begin = false
const res = []
const res: Array<string> = []
for (const x of content) {
if (x === '`') {
if (!begin) {
@@ -52,17 +54,24 @@ const useTocRerenderTrigger = () => {
return toggleRenderFlag
}
interface TOCHeader {
id?: string
text: string
link: string
level: number
}
const GuidesTableOfContents = ({
className,
overrideToc,
video,
}: {
className?: string
overrideToc?: Array<{ text: string; link: string; level: number }>
overrideToc?: Array<TOCHeader>
video?: string
}) => {
useSubscribeTocRerender()
const [tocList, setTocList] = useState([])
const [tocList, setTocList] = useState<TOCHeader[]>([])
const pathname = usePathname()
const [hash] = useHash()
@@ -79,6 +88,7 @@ const GuidesTableOfContents = ({
const headings = Array.from(
document.querySelector('#sb-docs-guide-main-article')?.querySelectorAll('h2, h3') ?? []
)
const newHeadings = headings
.filter((heading) => heading.id)
.map((heading) => {
@@ -87,9 +97,10 @@ const GuidesTableOfContents = ({
if (!link) return null
const level = heading.tagName === 'H2' ? 2 : 3
return { text, link, level }
return { text, link, level } as Partial<TOCHeader>
})
.filter(Boolean)
.filter((x): x is TOCHeader => !!x && !!x.text && !!x.link && !!x.level)
setTocList(newHeadings)
})
@@ -140,3 +151,4 @@ const GuidesTableOfContents = ({
export default GuidesTableOfContents
export { useTocRerenderTrigger }
export type { TOCHeader }
+4 -2
View File
@@ -1,3 +1,5 @@
'use client'
import Link from 'next/link'
import { IconBackground, IconPlay } from 'ui'
import { useBreakpoint } from 'common'
@@ -100,12 +102,12 @@ const HomePageCover = (props) => {
)
return (
<div className="w-full bg-alternative border-b max-w-none mb-16 md:mb-12 xl:mb-0">
<div className="relative z-10 w-full bg-alternative border-b max-w-none mb-16 md:mb-12 xl:mb-0">
<div className="max-w-7xl px-5 mx-auto py-8 sm:pb-16 sm:pt-12 xl:pt-16 flex flex-col xl:flex-row justify-between gap-12 xl:gap-12">
<div className="flex flex-col sm:flex-row gap-4 sm:gap-8 items-start sm:items-center w-full max-w-xl xl:max-w-[33rem]">
<DocsCoverLogo aria-hidden="true" />
<div className="flex flex-col">
<h1 className="m-0 mb-3 text-2xl sm:text-3xl text-foreground">{props.meta?.title}</h1>
<h1 className="m-0 mb-3 text-2xl sm:text-3xl text-foreground">{props.title}</h1>
<p className="m-0 text-foreground-light">
Learn how to get up and running with Supabase through tutorials, APIs and platform
resources.
@@ -1,3 +1,5 @@
'use client'
import dynamic from 'next/dynamic'
import { Suspense } from 'react'
@@ -0,0 +1,7 @@
<Admonition type="tip">
Make sure you're using the right `supabase` client in the following code.
If you're not using Server-Side Rendering or cookie-based Auth, you can directly use the `createClient` from `@supabase/supabase-js`. If you're using Server-Side Rendering, see the [Server-Side Auth guide](/docs/guides/auth/server-side/creating-a-client) for instructions on creating your Supabase client.
</Admonition>
+41 -38
View File
@@ -57,9 +57,9 @@ At the callback endpoint, handle the code exchange to save the user session.
Create a new file at `app/auth/callback/route.ts` and populate with the following:
```ts app/auth/callback/route.ts
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
import { type CookieOptions, createServerClient } from '@supabase/ssr'
// The client you created from the Server-Side Auth instructions
import { createClient } from '@/utils/supabase/server'
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
@@ -68,27 +68,19 @@ export async function GET(request: Request) {
const next = searchParams.get('next') ?? '/'
if (code) {
const cookieStore = cookies()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
cookieStore.delete({ name, ...options })
},
},
}
)
const supabase = createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
const forwardedHost = request.headers.get('x-forwarded-host') // original origin before load balancer
const isLocalEnv = process.env.NODE_ENV === 'development'
if (isLocalEnv) {
// we can be sure that there is no load balancer in between, so no need to watch for X-Forwarded-Host
return NextResponse.redirect(`${origin}${next}`)
} else if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}${next}`)
} else {
return NextResponse.redirect(`${origin}${next}`)
}
}
}
@@ -132,7 +124,7 @@ export const GET = async (event) => {
Create a new file at `src/pages/auth/callback.ts` and populate with the following:
```ts src/pages/auth/callback.ts
import { createServerClient } from '@supabase/ssr'
import { createServerClient, parseCookieHeader } from '@supabase/ssr'
import { type APIRoute } from 'astro'
export const GET: APIRoute = async ({ request, cookies, redirect }) => {
@@ -146,14 +138,13 @@ export const GET: APIRoute = async ({ request, cookies, redirect }) => {
import.meta.env.PUBLIC_SUPABASE_ANON_KEY,
{
cookies: {
get(key) {
return cookies.get(key)?.value
getAll() {
return parseCookieHeader(Astro.request.headers.get('Cookie') ?? '')
},
set(key, value, options) {
cookies.set(key, value, options)
},
remove(key, options) {
cookies.delete(key, options)
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
Astro.cookies.set(name, value, options)
)
},
},
}
@@ -179,7 +170,7 @@ Create a new file at `app/routes/auth.callback.tsx` and populate with the follow
```ts app/routes/auth.callback.tsx
import { redirect, type LoaderFunctionArgs } from '@remix-run/node'
import { createServerClient, parse, serialize } from '@supabase/ssr'
import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'
export async function loader({ request }: LoaderFunctionArgs) {
const requestUrl = new URL(request.url)
@@ -191,14 +182,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
const cookies = parse(request.headers.get('Cookie') ?? '')
const supabase = createServerClient(process.env.SUPABASE_URL!, process.env.SUPABASE_ANON_KEY!, {
cookies: {
get(key) {
return cookies[key]
getAll() {
return parseCookieHeader(request.headers.get('Cookie') ?? '')
},
set(key, value, options) {
headers.append('Set-Cookie', serialize(key, value, options))
},
remove(key, options) {
headers.append('Set-Cookie', serialize(key, '', options))
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
headers.append('Set-Cookie', serializeCookieHeader(name, value, options))
)
},
},
})
@@ -227,7 +217,20 @@ app.get("/auth/callback", async function (req, res) {
const next = req.query.next ?? "/"
if (code) {
const supabase = createClient({ req, res })
const supabase = createServerClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY, {
cookies: {
getAll() {
return parseCookieHeader(context.req.headers.cookie ?? '')
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
context.res.appendHeader('Set-Cookie', serializeCookieHeader(name, value, options))
)
},
},
})
await supabase.auth.exchangeCodeForSession(code)
}
+29
View File
@@ -0,0 +1,29 @@
'use client'
import AuthRateLimits from './auth_rate_limits.mdx'
import CreateClientSnippet from './create_client_snippet.mdx'
import DatabaseSetup from './database_setup.mdx'
import GetSessionWarning from './get_session_warning.mdx'
import HuggingFaceDeployment from './ai/quickstart_hf_deployment.mdx'
import KotlinProjectSetup from './kotlin_project_setup.mdx'
import MigrationWarnings from './migration_warnings.mdx'
import OAuthPkceFlow from './oauth_pkce_flow.mdx'
import ProjectSetup from './project_setup.mdx'
import QuickstartIntro from './quickstart_intro.mdx'
import SocialProviderSettingsSupabase from './social_provider_settings_supabase.mdx'
import SocialProviderSetup from './social_provider_setup.mdx'
export {
AuthRateLimits,
CreateClientSnippet,
DatabaseSetup,
GetSessionWarning,
HuggingFaceDeployment,
KotlinProjectSetup,
MigrationWarnings,
ProjectSetup,
OAuthPkceFlow,
QuickstartIntro,
SocialProviderSettingsSupabase,
SocialProviderSetup,
}
@@ -27,7 +27,9 @@ create policy "Users can update own profile." on profiles
-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
create function public.handle_new_user()
returns trigger as $$
returns trigger
set search_path = ''
as $$
begin
insert into public.profiles (id, full_name, avatar_url)
values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');
-2
View File
@@ -1,2 +0,0 @@
import { Mermaid } from 'mdx-mermaid/lib/Mermaid'
export default Mermaid
-11
View File
@@ -1,11 +0,0 @@
import { Suspense, lazy } from 'react'
const MermaidHeavy = lazy(() => import('./Mermaid'))
export function Mermaid(props) {
return (
<Suspense>
<MermaidHeavy {...props} />
</Suspense>
)
}
@@ -32,7 +32,11 @@ type MenuItem = {
community?: boolean
}
export type HomepageMenuItems = MenuItem[][]
export type DropdownMenuItem = MenuItem & {
menuItems?: MenuItem[][]
}
export type GlobalMenuItems = DropdownMenuItem[][]
export type NavMenuConstant = Readonly<{
title: string
@@ -0,0 +1,198 @@
import React, { Dispatch, Fragment, SetStateAction, useEffect } from 'react'
import { m, AnimatePresence, LazyMotion, domAnimation } from 'framer-motion'
import Image from 'next/image'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useKey } from 'react-use'
import { useIsLoggedIn, useIsUserLoading } from 'common'
import { Accordion, Button, cn, IconX } from 'ui'
import { ThemeToggle } from 'ui-patterns'
import { MenuItem, useActiveMenuLabel } from './GlobalNavigationMenu'
import { GLOBAL_MENU_ITEMS } from './NavigationMenu.constants'
import type { DropdownMenuItem } from '../Navigation.types'
const DEFAULT_EASE = [0.24, 0.25, 0.05, 1]
const container = {
hidden: { opacity: 0 },
show: { opacity: 1, transition: { duration: 0.15, staggerChildren: 0.05, ease: DEFAULT_EASE } },
exit: { opacity: 0, transition: { duration: 0.15 } },
}
const listItem = {
hidden: { opacity: 0, y: 10 },
show: { opacity: 1, y: 0, transition: { duration: 0.25, ease: DEFAULT_EASE } },
exit: { opacity: 0, transition: { duration: 0.05 } },
}
const itemClassName =
'block py-2 pl-2 pr-3.5 text-sm text-foreground-light hover:bg-surface-200 focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-foreground-lighter focus-visible:rounded'
const AccordionMenuItem = ({ section }: { section: DropdownMenuItem[] }) => {
const activeLabel = useActiveMenuLabel(GLOBAL_MENU_ITEMS)
return (
<m.div
variants={listItem}
className="border-b border-muted [&>div]:!rounded-none [&_div[data-state=open]>div]:py-1"
key={section[0].label}
>
{section[0].menuItems ? (
<Accordion.Item
header={section[0].label}
id={section[0].label}
className={cn(
'relative',
activeLabel === section[0].label && '!text-foreground',
itemClassName
)}
>
{section[0].menuItems?.map((menuItem, menuItemIndex) => (
<Fragment key={`desktop-docs-menu-section-${menuItemIndex}`}>
{menuItem.map((item) =>
!item.href ? (
<div className="font-mono tracking-wider flex items-center text-foreground-muted text-xs uppercase rounded-md p-2 leading-none">
{item.label}
</div>
) : (
<MenuItem
href={item.href}
title={item.label}
community={item.community}
icon={item.icon}
/>
)
)}
</Fragment>
))}
</Accordion.Item>
) : (
<Link
href={section[0].href}
className={cn(activeLabel === section[0].label && '!text-foreground', itemClassName)}
>
{section[0].label}
</Link>
)}
</m.div>
)
}
const Menu = () => (
<Accordion
type="default"
openBehaviour="multiple"
size="small"
className="space-y-1 mt-2.5"
justified
chevronAlign="right"
>
{GLOBAL_MENU_ITEMS.map((section) => (
<AccordionMenuItem section={section} />
))}
</Accordion>
)
interface Props {
open: boolean
setOpen: Dispatch<SetStateAction<boolean>>
}
const GlobalMobileMenu = ({ open, setOpen }: Props) => {
const isLoggedIn = useIsLoggedIn()
const isUserLoading = useIsUserLoading()
const pathname = usePathname()
// Close mobile menu on route change
useEffect(() => {
setOpen(false)
}, [pathname])
useKey('Escape', () => setOpen(false))
return (
<LazyMotion features={domAnimation}>
<AnimatePresence mode="wait">
{open && (
<m.div
variants={container}
initial="hidden"
animate="show"
exit="exit"
className="bg-overlay fixed overflow-hidden inset-0 z-50 h-screen max-h-screen w-screen supports-[height:100cqh]:h-[100cqh] supports-[height:100svh]:h-[100svh] transform"
>
<div className="absolute px-5 h-[var(--header-height)] flex items-center justify-between w-screen left-0 top-0 z-50 bg-overlay before:content[''] before:absolute before:w-full before:h-3 before:inset-0 before:top-full before:bg-gradient-to-b before:from-background-overlay before:to-transparent">
<Link href="/" className="flex items-center gap-2">
<Image
className="cursor-pointer hidden dark:block"
src="/docs/supabase-dark.svg"
priority
width={96}
height={24}
alt="Supabase Logo"
/>
<Image
className="cursor-pointer block dark:hidden"
src="/docs/supabase-light.svg"
priority
width={96}
height={24}
alt="Supabase Logo"
/>
<span className="font-mono text-sm font-medium text-brand-link mb-px">DOCS</span>
</Link>
<div className="flex gap-4 items-center">
<ThemeToggle contentClassName="bg-surface-200" />
<button
onClick={() => setOpen(false)}
type="button"
className="inline-flex items-center justify-center focus:ring-brand bg-surface-100 hover:bg-surface-200 focus:outline-none focus:ring-2 focus:ring-inset border border-default bg-surface-100/75 text-foreground-light rounded min-w-[30px] w-[30px] h-[30px]"
>
<span className="sr-only">Close menu</span>
<IconX />
</button>
</div>
</div>
<div className="max-h-screen supports-[height:100cqh]:h-[100cqh] supports-[height:100svh]:h-[100svh] overflow-y-auto pt-12 pb-32 px-3">
<Menu />
</div>
<div className="absolute bottom-0 left-0 right-0 top-auto w-full bg-alternative flex items-stretch p-4 gap-4">
{!isUserLoading && (
<>
{isLoggedIn ? (
<Button block size="medium" asChild>
<Link href="/dashboard/projects">Dashboard</Link>
</Button>
) : (
<>
<Button block size="medium" type="default" asChild>
<Link href="https://supabase.com/dashboard/sign-in">Sign in</Link>
</Button>
<Button block size="medium" asChild>
<Link href="https://supabase.com/dashboard/new">Start your project</Link>
</Button>
</>
)}
</>
)}
</div>
</m.div>
)}
</AnimatePresence>
<AnimatePresence mode="wait">
{open && (
<m.div
variants={container}
initial="hidden"
animate="show"
exit="exit"
className="bg-alternative fixed overflow-hidden inset-0 z-40 h-screen w-screen transform"
/>
)}
</AnimatePresence>
</LazyMotion>
)
}
export default GlobalMobileMenu
@@ -0,0 +1,183 @@
'use client'
import React, { FC, Fragment, useEffect, useState } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import {
Badge,
cn,
MenubarSeparator,
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from 'ui'
import MenuIconPicker from './MenuIconPicker'
import { GLOBAL_MENU_ITEMS } from './NavigationMenu.constants'
/**
* Get TopNav active label based on current pathname
*/
export const useActiveMenuLabel = (GLOBAL_MENU_ITEMS) => {
const pathname = usePathname()
const [activeLabel, setActiveLabel] = useState('')
useEffect(() => {
// check if homepage
if (pathname === '/') {
return setActiveLabel('Home')
}
for (let index = 0; index < GLOBAL_MENU_ITEMS.length; index++) {
const section = GLOBAL_MENU_ITEMS[index]
// check if first level menu items match beginning of url
if (section[0].href?.startsWith(pathname)) {
return setActiveLabel(section[0].label)
}
// check if second level menu items match beginning of url
if (section[0].menuItems) {
section[0].menuItems.map((menuItemGroup) =>
menuItemGroup.map(
(menuItem) => menuItem.href?.startsWith(pathname) && setActiveLabel(section[0].label)
)
)
}
}
}, [pathname])
return activeLabel
}
const GlobalNavigationMenu: FC = () => {
const activeLabel = useActiveMenuLabel(GLOBAL_MENU_ITEMS)
const triggerClassName =
'h-[var(--header-height)] p-2 bg-transparent border-0 border-b-2 border-transparent font-normal rounded-none text-foreground-light hover:text-foreground data-[state=open]:!text-foreground data-[radix-collection-item]:focus-visible:ring-2 data-[radix-collection-item]:focus-visible:ring-foreground-lighter data-[radix-collection-item]:focus-visible:text-foreground h-full focus-visible:rounded !shadow-none outline-none transition-all outline-0 focus-visible:outline-4 focus-visible:outline-offset-1 focus-visible:outline-brand-600'
return (
<div className="flex relative gap-2 justify-start items-end w-full h-full">
<NavigationMenu
delayDuration={0}
skipDelayDuration={0}
className="w-full flex justify-start h-full"
renderViewport={false}
viewportClassName="mt-0 max-w-screen overflow-hidden border-0 rounded-none mt-1.5 rounded-md !border-x"
>
<NavigationMenuList className="px-6 space-x-2 h-[var(--header-height)]">
{GLOBAL_MENU_ITEMS.map((section, sectionIdx) =>
section[0].menuItems ? (
<NavigationMenuItem
key={`desktop-docs-menu-section-${section[0].label}-${sectionIdx}`}
className="text-sm relative h-full"
>
<NavigationMenuTrigger
className={cn(
navigationMenuTriggerStyle(),
triggerClassName,
activeLabel === section[0].label && 'text-foreground border-foreground'
)}
>
{section[0].label === 'Home' ? (
<MenuIconPicker icon={section[0].icon} />
) : (
section[0].label
)}
</NavigationMenuTrigger>
<NavigationMenuContent className="!top-[calc(100%+4px)] min-w-[14rem] max-h-[calc(100vh-4rem)] border-y w-screen md:w-64 overflow-hidden overflow-y-auto rounded-none md:rounded-md md:border border-overlay bg-overlay text-foreground-light shadow-md !duration-0">
<div className="p-3 md:p-1">
{section[0].menuItems?.map((menuItem, menuItemIndex) => (
<Fragment key={`desktop-docs-menu-section-${menuItemIndex}-${menuItemIndex}`}>
{menuItemIndex !== 0 && <MenubarSeparator className="bg-border-muted" />}
{menuItem.map((item, itemIdx) =>
!item.href ? (
<div
key={`desktop-docs-menu-section-label-${item.label}-${itemIdx}`}
className="font-mono tracking-wider flex items-center text-foreground-muted text-xs uppercase rounded-md p-2 leading-none"
>
{item.label}
</div>
) : (
<NavigationMenuLink
key={`desktop-docs-menu-section-label-${item.label}-${itemIdx}`}
asChild
>
<MenuItem
href={item.href}
title={item.label}
community={item.community}
icon={item.icon}
/>
</NavigationMenuLink>
)
)}
</Fragment>
))}
</div>
</NavigationMenuContent>
</NavigationMenuItem>
) : (
<NavigationMenuItem
key={`desktop-docs-menu-section-${section[0].label}-${sectionIdx}`}
className="text-sm relative h-full"
>
<NavigationMenuLink asChild>
<Link
href={section[0].href}
className={cn(
navigationMenuTriggerStyle(),
triggerClassName,
activeLabel === section[0].label && 'text-foreground border-foreground'
)}
>
{section[0].label === 'Home' ? (
<MenuIconPicker icon={section[0].icon} />
) : (
section[0].label
)}
</Link>
</NavigationMenuLink>
</NavigationMenuItem>
)
)}
</NavigationMenuList>
</NavigationMenu>
</div>
)
}
export const MenuItem = React.forwardRef<
React.ElementRef<'a'>,
React.ComponentPropsWithoutRef<'a'> & {
icon?: string
community?: boolean
}
>(({ className, title, href = '', icon, community, children, ...props }, ref) => {
return (
<Link
href={href}
ref={ref}
className={cn(
'group/menu-item flex items-center gap-2',
'w-full flex h-8 items-center text-foreground-light text-sm hover:text-foreground select-none rounded-md p-2 leading-none no-underline !outline-none focus-visible:ring-2 focus-visible:ring-foreground-lighter focus-visible:text-foreground',
className
)}
{...props}
>
{children ?? (
<>
{icon && <MenuIconPicker icon={icon} className="text-foreground-lighter" />}
<span className="flex-1">{title}</span>
{community && <Badge size="small">Community</Badge>}
</>
)}
</Link>
)
})
GlobalNavigationMenu.displayName = 'GlobalNavigationMenu'
MenuItem.displayName = 'MenuItem'
export default GlobalNavigationMenu
@@ -1,59 +0,0 @@
import Link from 'next/link'
import { Fragment } from 'react'
import { Badge, cn } from 'ui'
import { HOMEPAGE_MENU_ITEMS } from './NavigationMenu.constants'
import HomeMenuIconPicker from './HomeMenuIconPicker'
const NavigationMenuHome = () => {
return (
<div className="transition-all duration-150 ease-out opacity-100 ml-0 delay-150">
<ul className="relative w-full flex flex-col gap-4 pb-5">
{HOMEPAGE_MENU_ITEMS.map((section, sectionIndex) => {
return (
<Fragment key={`section-container-${sectionIndex}-border`}>
{sectionIndex !== 0 && (
<div className="h-px w-full border-b" key={`section-${sectionIndex}-border`}></div>
)}
<div key={`section-${sectionIndex}`}>
<div className="flex flex-col gap-4">
{section.map((link, i) => {
if (!link.href) {
return (
<h2
key={link.label}
className={cn(
'font-mono uppercase text-xs text-foreground-lighter',
i !== 0 && 'mt-4'
)}
>
{link.label}
</h2>
)
} else {
return (
<Link href={link.href} passHref key={link.label}>
<li
className={[
'group flex items-center gap-2',
'text-sm transition-all duration-150 text-foreground-light hover:text-foreground hover:cursor-pointer ',
].join(' ')}
>
{link?.icon && <HomeMenuIconPicker icon={link.icon} />}
{link.label}
{link.community && <Badge size="small">Community</Badge>}
</li>
</Link>
)
}
})}
</div>
</div>
</Fragment>
)
})}
</ul>
</div>
)
}
export default NavigationMenuHome
@@ -1,3 +1,5 @@
'use client'
import React from 'react'
import {
IconMenuApi,
@@ -24,12 +26,18 @@ import {
IconMenuKotlin,
IconMenuAI,
IconMenuDevCli,
} from './HomeMenuIcons'
IconGitHub,
IconSupport,
IconTroubleshooting,
IconBranching,
} from './MenuIcons'
function getMenuIcon(menuKey: string, width: number = 16, height: number = 16, className: string) {
function getMenuIcon(menuKey: string, width: number = 16, height: number = 16, className?: string) {
switch (menuKey) {
case 'home':
return <IconMenuHome width={width} height={height} className={className} />
case 'branching':
return <IconBranching width={width} height={height} className={className} />
case 'getting-started':
return <IconMenuGettingStarted width={width} height={height} className={className} />
case 'database':
@@ -76,23 +84,29 @@ function getMenuIcon(menuKey: string, width: number = 16, height: number = 16, c
return <IconMenuCli width={width} height={height} className={className} />
case 'status':
return <IconMenuStatus width={width} height={height} className={className} />
case 'github':
return <IconGitHub width={width} height={height} className={className} />
case 'support':
return <IconSupport width={width} height={height} className={className} />
case 'contributing':
return <IconTroubleshooting width={width} height={height} className={className} />
default:
return <IconMenuPlatform width={width} height={height} className={className} />
}
}
type HomeMenuIconPickerProps = {
type MenuIconPickerProps = {
icon: string
width?: number
height?: number
className?: string
}
export default function HomeMenuIconPicker({
export default function MenuIconPicker({
icon,
width = 16,
height = 16,
className,
}: HomeMenuIconPickerProps) {
}: MenuIconPickerProps) {
return getMenuIcon(icon, width, height, className)
}
@@ -519,3 +519,119 @@ export function IconMenuStatus({ width = 16, height = 16, className }: HomeMenuI
</svg>
)
}
export function IconBranching({ width = 16, height = 16, className }: HomeMenuIcon) {
return (
<svg
className={className}
width={width}
height={height}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.00018 2.69751C7.56432 2.69751 7.13983 2.74376 6.73114 2.83144C6.46114 2.88936 6.19531 2.71744 6.13738 2.44744C6.07946 2.17744 6.25138 1.91161 6.52138 1.85368C6.99858 1.75131 7.49338 1.69751 8.00018 1.69751C8.50698 1.69751 9.00178 1.75131 9.47898 1.85368C9.74898 1.91161 9.9209 2.17744 9.86297 2.44744C9.80505 2.71744 9.53921 2.88936 9.26921 2.83144C8.86053 2.74376 8.43603 2.69751 8.00018 2.69751Z"
fill="currentColor"
/>
<path
d="M1.73138 6.85327C2.00138 6.91119 2.1733 7.17703 2.11537 7.44703C2.02769 7.85571 1.98145 8.28021 1.98145 8.71606C1.98145 9.15192 2.02769 9.57642 2.11537 9.9851C2.1733 10.2551 2.00138 10.5209 1.73138 10.5789C1.46138 10.6368 1.19555 10.4649 1.13762 10.1949C1.03524 9.71767 0.981445 9.22287 0.981445 8.71606C0.981445 8.20926 1.03524 7.71446 1.13762 7.23726C1.19555 6.96726 1.46138 6.79534 1.73138 6.85327Z"
fill="currentColor"
/>
<path
d="M14.2686 6.85327C14.5386 6.79534 14.8045 6.96726 14.8624 7.23726C14.9648 7.71446 15.0186 8.20926 15.0186 8.71607C15.0186 9.22287 14.9648 9.71766 14.8624 10.1949C14.8045 10.4649 14.5386 10.6368 14.2686 10.5789C13.9986 10.5209 13.8267 10.2551 13.8846 9.9851C13.9723 9.57641 14.0186 9.15192 14.0186 8.71607C14.0186 8.28021 13.9723 7.85571 13.8846 7.44703C13.8267 7.17703 13.9986 6.91119 14.2686 6.85327Z"
fill="currentColor"
/>
<path
d="M6.13738 14.9847C6.19531 14.7147 6.46114 14.5428 6.73114 14.6007C7.13983 14.6884 7.56432 14.7346 8.00018 14.7346C8.43603 14.7346 8.86053 14.6884 9.26922 14.6007C9.53922 14.5428 9.80505 14.7147 9.86297 14.9847C9.9209 15.2547 9.74898 15.5205 9.47898 15.5784C9.00178 15.6808 8.50698 15.7346 8.00018 15.7346C7.49337 15.7346 6.99858 15.6808 6.52137 15.5784C6.25138 15.5205 6.07946 15.2547 6.13738 14.9847Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.8048 2.46822C12.4795 1.79347 13.5735 1.79347 14.2483 2.46822C14.923 3.14298 14.923 4.23697 14.2483 4.91173C13.6764 5.48362 12.8033 5.5708 12.1396 5.17328L10.6936 6.61929C11.1446 7.19789 11.4133 7.9256 11.4133 8.71606C11.4133 9.47805 11.1636 10.1817 10.7416 10.7497L12.2103 12.2184C12.8642 11.8681 13.6961 11.9687 14.2478 12.5204C14.9225 13.1951 14.9225 14.2891 14.2478 14.9639C13.573 15.6386 12.479 15.6386 11.8043 14.9639C11.2523 14.4119 11.1519 13.5794 11.503 12.9253L10.0346 11.457C9.46646 11.8794 8.76241 12.1294 7.99997 12.1294C7.20951 12.1294 6.4818 11.8607 5.90321 11.4097L4.4574 12.8555C4.85471 13.5192 4.76747 14.3921 4.19566 14.9639C3.52091 15.6386 2.42691 15.6386 1.75216 14.9639C1.0774 14.2891 1.07741 13.1951 1.75216 12.5204C2.28359 11.9889 3.07507 11.876 3.71698 12.1817L5.21234 10.6864C4.81821 10.1298 4.5866 9.44996 4.5866 8.71605C4.5866 7.95361 4.83658 7.24956 5.25904 6.68138L3.79073 5.21307C3.13666 5.56417 2.30413 5.46373 1.75216 4.91175C1.07741 4.237 1.07741 3.143 1.75216 2.46825C2.42692 1.79349 3.52091 1.79349 4.19566 2.46825C4.74732 3.01991 4.84796 3.8518 4.49758 4.5057L5.96628 5.9744C6.53429 5.55238 7.23797 5.30269 7.99997 5.30269C8.73387 5.30269 9.41367 5.5343 9.97027 5.92842L11.466 4.43272C11.1605 3.79087 11.2734 2.99956 11.8048 2.46822ZM13.5412 3.17533C13.2569 2.8911 12.7961 2.8911 12.5119 3.17533C12.2277 3.45956 12.2277 3.92039 12.5119 4.20462C12.7961 4.48885 13.2569 4.48885 13.5412 4.20462C13.8254 3.92039 13.8254 3.45956 13.5412 3.17533ZM9.67618 10.4523C9.24205 10.8715 8.65112 11.1294 7.99997 11.1294C6.6671 11.1294 5.5866 10.0489 5.5866 8.71605C5.5866 7.38319 6.6671 6.30269 7.99997 6.30269C9.33283 6.30269 10.4133 7.38319 10.4133 8.71606C10.4133 9.36685 10.1557 9.95747 9.73695 10.3915C9.72611 10.4007 9.71556 10.4104 9.70535 10.4206C9.69509 10.4308 9.68536 10.4414 9.67618 10.4523ZM3.48856 3.17536C3.20433 2.89113 2.7435 2.89113 2.45927 3.17536C2.17504 3.45959 2.17504 3.92042 2.45927 4.20465C2.7435 4.48888 3.20433 4.48888 3.48856 4.20465C3.77279 3.92042 3.77279 3.45959 3.48856 3.17536ZM3.48856 13.2275C3.20433 12.9432 2.7435 12.9432 2.45927 13.2275C2.17504 13.5117 2.17504 13.9725 2.45927 14.2568C2.7435 14.541 3.20433 14.541 3.48856 14.2568C3.77279 13.9725 3.77279 13.5117 3.48856 13.2275ZM13.5407 13.2275C13.2564 12.9432 12.7956 12.9432 12.5114 13.2275C12.2271 13.5117 12.2271 13.9725 12.5114 14.2568C12.7956 14.541 13.2564 14.541 13.5407 14.2568C13.8249 13.9725 13.8249 13.5117 13.5407 13.2275Z"
fill="currentColor"
/>
</svg>
)
}
export function IconGitHub({ width = 16, height = 16, className }: HomeMenuIcon) {
return (
<svg
className={className}
width={width}
height={height}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.00011 1.8714C4.96998 1.8714 1.70312 5.14337 1.70312 9.18079C1.70312 12.4097 3.79371 15.1497 6.69353 16.1158C7.05838 16.183 7.19119 15.9575 7.19119 15.7634C7.19119 15.5905 7.18535 15.13 7.1817 14.5207C5.15168 14.9622 4.72335 13.5407 4.72335 13.5407C4.39206 12.6957 3.91338 12.471 3.91338 12.471C3.25082 12.0186 3.96373 12.0273 3.96373 12.0273C4.69562 12.0784 5.0809 12.7804 5.0809 12.7804C5.73179 13.8968 6.78912 13.5743 7.20432 13.3875C7.27145 12.9154 7.45972 12.5936 7.66841 12.4112C6.04848 12.2265 4.34463 11.599 4.34463 8.79843C4.34463 8.00087 4.62922 7.34779 5.09549 6.837C5.02033 6.65238 4.77005 5.90882 5.167 4.9033C5.167 4.9033 5.77995 4.70628 7.17367 5.65197C7.76894 5.48962 8.3831 5.40693 9.00011 5.40606C9.62035 5.40898 10.2442 5.48997 10.8273 5.65197C12.2203 4.70628 12.8318 4.90257 12.8318 4.90257C13.2302 5.90882 12.9792 6.65238 12.9047 6.837C13.3717 7.34779 13.6549 8.00087 13.6549 8.79843C13.6549 11.6063 11.9481 12.2244 10.3231 12.4053C10.585 12.6308 10.8178 13.0766 10.8178 13.7589C10.8178 14.7353 10.809 15.5241 10.809 15.7634C10.809 15.959 10.9404 16.1866 11.3111 16.1151C12.7641 15.6278 14.0272 14.6961 14.922 13.4517C15.8167 12.2074 16.2977 10.7134 16.2971 9.18079C16.2971 5.14337 13.0295 1.8714 9.00011 1.8714Z"
fill="currentColor"
/>
</svg>
)
}
export function IconSupport({ width = 16, height = 16, className }: HomeMenuIcon) {
return (
<svg
className={className}
width={width}
height={height}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.15298 4.15407L7.05972 7.06081M10.9401 10.9404L13.8469 13.8471M13.8469 4.15407L10.9401 7.06081L13.3601 4.64069M4.15298 13.8471L7.05972 10.9404M15.8555 8.99998C15.8555 12.7861 12.7862 15.8555 9.00004 15.8555C5.21384 15.8555 2.14453 12.7861 2.14453 8.99998C2.14453 5.21379 5.21384 2.14447 9.00004 2.14447C12.7862 2.14447 15.8555 5.21379 15.8555 8.99998ZM11.7422 8.99998C11.7422 10.5145 10.5145 11.7422 9.00004 11.7422C7.48556 11.7422 6.25783 10.5145 6.25783 8.99998C6.25783 7.4855 7.48556 6.25778 9.00004 6.25778C10.5145 6.25778 11.7422 7.4855 11.7422 8.99998Z"
stroke="currentColor"
strokeMiterlimit="10"
strokeLinejoin="bevel"
/>
</svg>
)
}
export function IconTerraform({ width = 16, height = 16, className }: HomeMenuIcon) {
return (
<svg
className={className}
width={width}
height={height}
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M10.5 6.22713V10.9596L14.6 8.59463V3.85713L10.5 6.22713Z" fill="currentColor" />
<path d="M5.95 3.85713L10.05 6.22713V10.9596L5.95 8.59213V3.85713Z" fill="currentColor" />
<path
d="M1.4 1.21463V5.94963L5.5 8.31713V3.58213L1.4 1.21463ZM5.95 13.8471L10.05 16.2146V11.4796L5.95 9.11213V13.8471Z"
fill="currentColor"
/>
</svg>
)
}
export function IconTroubleshooting({ width = 16, height = 16, className }: HomeMenuIcon) {
return (
<svg
className={className}
width={width}
height={height}
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.0326 6.65586L11.679 6.30231L12.0326 6.65586ZM13.9812 4.70721L14.4367 4.50108C14.3691 4.35154 14.2323 4.24473 14.0708 4.21531C13.9094 4.18589 13.7437 4.2376 13.6277 4.35366L13.9812 4.70721ZM8.76103 9.9274L8.96716 9.47187L8.65205 9.32928L8.40747 9.57385L8.76103 9.9274ZM4.21801 14.4704L3.86446 14.1169L4.21801 14.4704ZM2.24565 12.4981L1.89209 12.1445H1.89209L2.24565 12.4981ZM6.78866 7.95504L7.14221 8.30859L7.38679 8.06402L7.24419 7.7489L6.78866 7.95504ZM7.5932 3.53939L7.23965 3.18584L7.5932 3.53939ZM9.67545 2.4479L9.76508 2.9398L9.67545 2.4479ZM12.0089 2.73485L12.3624 3.0884C12.4785 2.97234 12.5302 2.8067 12.5008 2.64522C12.4713 2.48374 12.3645 2.34699 12.215 2.27932L12.0089 2.73485ZM10.0602 4.6835L9.70665 4.32994L10.0602 4.6835ZM10.0671 6.66267L9.71597 7.01867L10.0671 6.66267ZM12.3861 7.00941L14.3348 5.06077L13.6277 4.35366L11.679 6.30231L12.3861 7.00941ZM13.5257 4.91335C13.8144 5.55131 13.9018 6.2621 13.7763 6.95099L14.7601 7.13024C14.922 6.24136 14.8092 5.32423 14.4367 4.50108L13.5257 4.91335ZM13.7763 6.95099C13.6507 7.63988 13.3183 8.27417 12.8231 8.76931L13.5302 9.47642C14.1691 8.83754 14.5981 8.01912 14.7601 7.13024L13.7763 6.95099ZM12.8231 8.76931C12.328 9.26445 11.6937 9.59694 11.0048 9.72245L11.1841 10.7063C12.0729 10.5443 12.8913 10.1153 13.5302 9.47642L12.8231 8.76931ZM11.0048 9.72245C10.3159 9.84797 9.60512 9.76056 8.96716 9.47187L8.55489 10.3829C9.37805 10.7554 10.2952 10.8682 11.1841 10.7063L11.0048 9.72245ZM8.40747 9.57385L3.86446 14.1169L4.57157 14.824L9.11458 10.281L8.40747 9.57385ZM3.86446 14.1169C3.69668 14.2846 3.46911 14.3789 3.23183 14.3789V15.3789C3.73433 15.3789 4.21625 15.1793 4.57157 14.824L3.86446 14.1169ZM3.23183 14.3789C2.99455 14.3789 2.76699 14.2846 2.5992 14.1169L1.89209 14.824C2.24741 15.1793 2.72933 15.3789 3.23183 15.3789V14.3789ZM2.5992 14.1169C2.43142 13.9491 2.33716 13.7215 2.33716 13.4842H1.33716C1.33716 13.9867 1.53677 14.4686 1.89209 14.824L2.5992 14.1169ZM2.33716 13.4842C2.33716 13.247 2.43142 13.0194 2.5992 12.8516L1.89209 12.1445C1.53678 12.4998 1.33716 12.9817 1.33716 13.4842H2.33716ZM2.5992 12.8516L7.14221 8.30859L6.43511 7.60149L1.89209 12.1445L2.5992 12.8516ZM7.24419 7.7489C6.9555 7.11095 6.86809 6.40016 6.99361 5.71126L6.00981 5.53201C5.84785 6.42089 5.96064 7.33802 6.33313 8.16118L7.24419 7.7489ZM6.99361 5.71126C7.11913 5.02237 7.45161 4.38808 7.94676 3.89294L7.23965 3.18584C6.60077 3.82472 6.17176 4.64313 6.00981 5.53201L6.99361 5.71126ZM7.94676 3.89294C8.4419 3.3978 9.07618 3.06532 9.76508 2.9398L9.58582 1.956C8.69694 2.11795 7.87853 2.54696 7.23965 3.18584L7.94676 3.89294ZM9.76508 2.9398C10.454 2.81428 11.1648 2.90169 11.8027 3.19038L12.215 2.27932C11.3918 1.90682 10.4747 1.79404 9.58582 1.956L9.76508 2.9398ZM11.6553 2.38129L9.70665 4.32994L10.4138 5.03705L12.3624 3.0884L11.6553 2.38129ZM11.679 6.30231C11.3313 6.64999 10.7682 6.65194 10.4182 6.30668L9.71597 7.01867C10.4573 7.74984 11.6498 7.74571 12.3861 7.00941L11.679 6.30231ZM10.4182 6.30668C10.0646 5.95801 10.0627 5.38815 10.4138 5.03705L9.70665 4.32994C8.96311 5.07349 8.96729 6.28029 9.71597 7.01867L10.4182 6.30668Z"
fill="currentColor"
/>
</svg>
)
}
@@ -1,15 +1,10 @@
import type { HomepageMenuItems, NavMenuConstant, References } from '../Navigation.types'
import { IS_DEV } from '~/lib/constants'
import type { GlobalMenuItems, NavMenuConstant, References } from '../Navigation.types'
export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
export const GLOBAL_MENU_ITEMS: GlobalMenuItems = [
[
{
label: 'Home',
icon: 'home',
href: '/',
level: 'home',
},
{
label: 'Getting Started',
label: 'Start',
icon: 'getting-started',
href: '/guides/getting-started',
level: 'gettingstarted',
@@ -17,164 +12,192 @@ export const HOMEPAGE_MENU_ITEMS: HomepageMenuItems = [
],
[
{
label: 'Product',
},
{
label: 'Database',
icon: 'database',
href: '/guides/database/overview',
level: 'database',
},
{
label: 'Auth',
icon: 'auth',
href: '/guides/auth',
level: 'auth',
},
{
label: 'Storage',
icon: 'storage',
href: '/guides/storage',
level: 'storage',
},
{
label: 'Edge Functions',
icon: 'edge-functions',
href: '/guides/functions',
level: 'functions',
},
{
label: 'Realtime',
icon: 'realtime',
href: '/guides/realtime',
level: 'realtime',
},
{
label: 'AI & Vectors',
icon: 'ai',
href: '/guides/ai',
level: 'ai',
label: 'Products',
menuItems: [
[
{
label: 'Database',
icon: 'database',
href: '/guides/database/overview',
level: 'database',
},
{
label: 'Auth',
icon: 'auth',
href: '/guides/auth',
level: 'auth',
},
{
label: 'Storage',
icon: 'storage',
href: '/guides/storage',
level: 'storage',
},
{
label: 'Edge Functions',
icon: 'edge-functions',
href: '/guides/functions',
level: 'functions',
},
{
label: 'Realtime',
icon: 'realtime',
href: '/guides/realtime',
level: 'realtime',
},
{
label: 'AI & Vectors',
icon: 'ai',
href: '/guides/ai',
level: 'ai',
},
],
],
},
],
[
{
label: 'Data API',
},
{
label: 'REST',
icon: 'rest',
href: '/guides/api',
level: 'api',
},
{
label: 'GraphQL',
icon: 'graphql',
href: '/guides/graphql',
level: 'graphql',
label: 'Build',
menuItems: [
[
{
label: 'Local Dev / CLI',
icon: 'dev-cli',
href: '/guides/cli',
level: 'reference_javascript',
},
{
label: 'Platform',
icon: 'platform',
href: '/guides/platform',
level: 'platform',
},
{
label: 'Self-Hosting',
icon: 'self-hosting',
href: '/guides/self-hosting',
level: 'self_hosting',
},
{
label: 'Integrations',
icon: 'integrations',
hasLightIcon: true,
href: 'https://supabase.com/partners/integrations',
level: 'integrations',
},
],
],
},
],
[
{
label: 'Development Cycle',
},
{
label: 'Local Dev / CLI',
icon: 'dev-cli',
href: '/guides/cli',
level: 'reference_javascript',
},
{
label: 'Platform',
icon: 'platform',
href: '/guides/platform',
level: 'platform',
},
{
label: 'Self-Hosting',
icon: 'self-hosting',
href: '/guides/self-hosting',
level: 'self_hosting',
},
],
[
{
label: 'Client Library Reference',
},
{
label: 'JavaScript',
icon: 'reference-javascript',
href: '/reference/javascript/introduction',
level: 'reference_javascript',
},
{
label: 'Flutter',
icon: 'reference-dart',
href: '/reference/dart/introduction',
level: 'reference_dart',
},
{
label: 'Swift',
icon: 'reference-swift',
href: '/reference/swift/introduction',
level: 'reference_swift',
},
{
label: 'Python',
icon: 'reference-python',
href: '/reference/python/introduction',
level: 'reference_python',
community: true,
},
{
label: 'C#',
icon: 'reference-csharp',
href: '/reference/csharp/introduction',
level: 'reference_csharp',
community: true,
},
{
label: 'Kotlin',
icon: 'reference-kotlin',
href: '/reference/kotlin/introduction',
level: 'reference_kotlin',
community: true,
label: 'Reference',
menuItems: [
[
{
label: 'Client Library Reference',
},
{
label: 'JavaScript',
icon: 'reference-javascript',
href: '/reference/javascript/introduction',
level: 'reference_javascript',
},
{
label: 'Flutter',
icon: 'reference-dart',
href: '/reference/dart/introduction',
level: 'reference_dart',
},
{
label: 'Swift',
icon: 'reference-swift',
href: '/reference/swift/introduction',
level: 'reference_swift',
},
{
label: 'Python',
icon: 'reference-python',
href: '/reference/python/introduction',
level: 'reference_python',
},
{
label: 'C#',
icon: 'reference-csharp',
href: '/reference/csharp/introduction',
level: 'reference_csharp',
community: true,
},
{
label: 'Kotlin',
icon: 'reference-kotlin',
href: '/reference/kotlin/introduction',
level: 'reference_kotlin',
community: true,
},
],
[
{
label: 'CLI Commands',
icon: 'reference-cli',
href: '/reference/cli/introduction',
level: 'reference_javascript',
},
{
label: 'Management API',
icon: 'reference-api',
href: '/reference/api/introduction',
level: 'reference_javascript',
},
],
[
{ label: 'Data API' },
{
label: 'REST',
icon: 'rest',
href: '/guides/api',
level: 'api',
},
{
label: 'GraphQL',
icon: 'graphql',
href: '/guides/graphql',
level: 'graphql',
},
],
],
},
],
[
{
label: 'Resources',
},
{
label: 'CLI Commands',
icon: 'reference-cli',
href: '/reference/cli/introduction',
level: 'reference_javascript',
},
{
label: 'Management API',
icon: 'reference-api',
href: '/reference/api/introduction',
level: 'reference_javascript',
},
{
label: 'Guides and Examples',
icon: 'resources',
href: '/guides/resources',
level: 'resources',
},
{
label: 'Integrations',
icon: 'integrations',
hasLightIcon: true,
href: 'https://supabase.com/partners/integrations',
level: 'integrations',
},
],
[
{
label: 'Status',
icon: 'status',
href: 'https://status.supabase.com/',
menuItems: [
[
{
label: 'Migration guides',
icon: 'resources',
href: '/guides/resources',
level: 'resources',
},
{
label: 'Changelog',
icon: 'changelog',
hasLightIcon: true,
href: 'https://supabase.com/changelog',
level: 'changelog',
},
{
label: 'Status',
icon: 'status',
href: 'https://status.supabase.com/',
},
{
label: 'Contributing',
icon: 'contributing',
href: '/contributing' as `/${string}`,
},
],
],
},
],
]
@@ -226,7 +249,8 @@ export const REFERENCES: References = {
export const gettingstarted: NavMenuConstant = {
icon: 'getting-started',
title: 'Getting Started',
title: 'Start with Supabase',
url: '/guides/getting-started',
items: [
{ name: 'Features', url: '/guides/getting-started/features' },
{ name: 'Architecture', url: '/guides/getting-started/architecture' },
@@ -238,6 +262,7 @@ export const gettingstarted: NavMenuConstant = {
{ name: 'NuxtJS', url: '/guides/getting-started/quickstarts/nuxtjs' },
{ name: 'Vue', url: '/guides/getting-started/quickstarts/vue' },
{ name: 'Flutter', url: '/guides/getting-started/quickstarts/flutter' },
{ name: 'iOS SwiftUI', url: '/guides/getting-started/quickstarts/ios-swiftui' },
{
name: 'Android Kotlin',
url: '/guides/getting-started/quickstarts/kotlin',
@@ -733,6 +758,10 @@ export const database: NavMenuConstant = {
name: 'Column Level Security',
url: '/guides/database/postgres/column-level-security',
},
{
name: 'Hardening the Data API',
url: '/guides/database/hardening-data-api',
},
{
name: 'Custom Claims & RBAC',
url: '/guides/database/postgres/custom-claims-and-role-based-access-control-rbac',
@@ -746,10 +775,6 @@ export const database: NavMenuConstant = {
name: 'Superuser Access and Unsupported Operations',
url: '/guides/database/postgres/roles-superuser',
},
{
name: 'Column level privileges',
url: '/guides/database/postgres/column-level-security',
},
],
},
{
@@ -860,10 +885,6 @@ export const database: NavMenuConstant = {
name: 'PostGIS: Geo queries',
url: '/guides/database/extensions/postgis',
},
{
name: 'pg-safeupdate: Required Where Clauses',
url: '/guides/database/extensions/pg-safeupdate',
},
{
name: 'pgsodium (pending deprecation): Encryption Features',
url: '/guides/database/extensions/pgsodium',
@@ -1179,7 +1200,7 @@ export const functions: NavMenuConstant = {
},
{
name: 'Examples',
url: '/guides/functions/examples',
url: undefined,
items: [
{
name: 'CORS support for invoking from the browser',
@@ -1295,6 +1316,11 @@ export const realtime: NavMenuConstant = {
},
],
},
{
name: 'Security',
url: undefined,
items: [{ name: 'Authorization', url: '/guides/realtime/authorization' }],
},
{
name: 'Guides',
url: undefined,
@@ -1307,6 +1333,14 @@ export const realtime: NavMenuConstant = {
name: 'Using Realtime with Next.js',
url: '/guides/realtime/realtime-with-nextjs',
},
{
name: 'Using Realtime Presence with Flutter',
url: '/guides/realtime/realtime-user-presence',
},
{
name: 'Listening to Postgres Changes with Flutter',
url: '/guides/realtime/realtime-listening-flutter',
},
],
},
{
@@ -1691,6 +1725,7 @@ export const platform: NavMenuConstant = {
url: undefined,
items: [
{ name: 'Logging', url: '/guides/platform/logs' },
{ name: 'Log Drains', url: '/guides/platform/log-drains' },
{ name: 'Metrics', url: '/guides/platform/metrics' },
{ name: 'Monitoring with Sentry', url: '/guides/platform/sentry-monitoring' },
],
@@ -1699,7 +1734,7 @@ export const platform: NavMenuConstant = {
name: 'Platform Management',
url: undefined,
items: [
{ name: 'Access Control', url: '/guides/platform/access-control' },
{ name: 'Regions', url: '/guides/platform/regions' },
{
name: 'Custom Postgres Config',
url: '/guides/platform/custom-postgres-config',
@@ -1727,11 +1762,18 @@ export const platform: NavMenuConstant = {
url: '/guides/platform/network-restrictions',
},
{ name: 'Performance Tuning', url: '/guides/platform/performance' },
{ name: 'Permissions', url: '/guides/platform/permissions' },
{ name: 'SSL Enforcement', url: '/guides/platform/ssl-enforcement' },
{ name: 'Branching', url: '/guides/platform/branching' },
],
},
{
name: 'Security',
url: undefined,
items: [
{ name: 'Access Control', url: '/guides/platform/access-control' },
{ name: 'SSL Enforcement', url: '/guides/platform/ssl-enforcement' },
{ name: 'Platform-required Permissions', url: '/guides/platform/permissions' },
],
},
{
name: 'Billing',
url: undefined,
@@ -1864,6 +1906,10 @@ export const resources: NavMenuConstant = {
name: 'Migrate to Supabase',
url: '/guides/resources/migrating-to-supabase',
items: [
{
name: 'Auth0',
url: '/guides/resources/migrating-to-supabase/auth0',
},
{
name: 'Firebase Auth',
url: '/guides/resources/migrating-to-supabase/firebase-auth',
@@ -1,10 +1,9 @@
import { memo } from 'react'
import NavigationMenuHome from './HomeMenu'
import NavigationMenuGuideList from './NavigationMenuGuideList'
import NavigationMenuRefList from './NavigationMenuRefList'
import { useCloseMenuOnRouteChange } from './NavigationMenu.utils'
enum MenuId {
Home = 'home',
GettingStarted = 'gettingstarted',
Database = 'database',
Api = 'api',
@@ -44,10 +43,6 @@ interface BaseMenu {
type: string
}
interface HomeMenu extends BaseMenu {
type: 'home'
}
interface GuideMenu extends BaseMenu {
type: 'guide'
}
@@ -59,13 +54,9 @@ interface ReferenceMenu extends BaseMenu {
specFile?: string
}
type Menu = HomeMenu | GuideMenu | ReferenceMenu
type Menu = GuideMenu | ReferenceMenu
const menus: Menu[] = [
{
id: MenuId.Home,
type: 'home',
},
{
id: MenuId.GettingStarted,
type: 'guide',
@@ -243,15 +234,15 @@ const menus: Menu[] = [
},
]
function getMenuById(id: MenuId) {
return menus.find((menu) => menu.id === id) ?? menus.find((menu) => menu.id === MenuId.Home)
export function getMenuById(id: MenuId) {
return menus.find((menu) => menu.id === id)
}
function getMenuElement(menu: Menu) {
function getMenuElement(menu: Menu | undefined) {
if (!menu) throw Error('No menu found for this menuId')
const menuType = menu.type
switch (menuType) {
case 'home':
return <NavigationMenuHome />
case 'guide':
return <NavigationMenuGuideList id={menu.id} />
case 'reference':
@@ -264,7 +255,7 @@ function getMenuElement(menu: Menu) {
/>
)
default:
throw new Error(`Unknown menu type '${menuType}'`)
return null
}
}
@@ -272,6 +263,8 @@ const NavigationMenu = ({ menuId }: { menuId: MenuId }) => {
const level = menuId
const menu = getMenuById(level)
useCloseMenuOnRouteChange()
return getMenuElement(menu)
}
@@ -1,6 +1,11 @@
'use client'
import { useEffect, useState } from 'react'
import { ICommonItem } from '~/components/reference/Reference.types'
import { Json } from '~/types'
import { usePathname } from 'next/navigation'
import { MenuId } from '~/components/Navigation/NavigationMenu/NavigationMenu'
import type { ICommonItem } from '~/components/reference/Reference.types'
import type { Json } from '~/types'
import { menuState } from '../../../hooks/useMenuState'
export function getPathWithoutHash(relativePath: string) {
return new URL(relativePath, 'http://placeholder').pathname
@@ -93,3 +98,46 @@ export function useSpec(specFile?: string) {
return spec
}
export const useCloseMenuOnRouteChange = () => {
const pathname = usePathname()
useEffect(() => {
menuState.setMenuMobileOpen(false)
}, [pathname])
}
export const getMenuId = (pathname: string | null) => {
pathname = (pathname ??= '').replace(/^\/guides\//, '')
switch (true) {
case pathname.startsWith('ai'):
return MenuId.Ai
case pathname.startsWith('api'):
return MenuId.Api
case pathname.startsWith('auth'):
return MenuId.Auth
case pathname.startsWith('cli'):
return MenuId.Cli
case pathname.startsWith('database'):
return MenuId.Database
case pathname.startsWith('functions'):
return MenuId.Functions
case pathname.startsWith('getting-started'):
return MenuId.GettingStarted
case pathname.startsWith('graphql'):
return MenuId.Graphql
case pathname.startsWith('platform'):
return MenuId.Platform
case pathname.startsWith('realtime'):
return MenuId.Realtime
case pathname.startsWith('resources'):
return MenuId.Resources
case pathname.startsWith('self-hosting'):
return MenuId.SelfHosting
case pathname.startsWith('storage'):
return MenuId.Storage
default:
return MenuId.GettingStarted
}
}
@@ -1,13 +1,11 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { IconChevronLeft } from 'ui'
import { BASE_PATH } from '~/lib/constants'
import clientLibsCommon from '~/spec/common-cli.yml' assert { type: 'yml' }
import * as NavItems from './NavigationMenu.constants'
import clientLibsCommon from '~/spec/common-cli.yml' assert { type: 'yml' }
const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => {
const router = useRouter()
const menu = NavItems[id]
const FunctionLink = ({
@@ -26,7 +24,7 @@ const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => {
href={`#${id}`}
className="cursor-pointer transition text-foreground-lighter text-sm hover:text-brand flex gap-3"
>
{icon && <img className="w-3" src={`${router.basePath}${icon}`} />}
{icon && <img className="w-3" src={`${BASE_PATH}${icon}`} />}
{title}
</Link>
</li>
@@ -113,7 +111,7 @@ const NavigationMenuCliList = ({ currentLevel, setLevel, id }) => {
</Link>
<div className="flex items-center gap-3 my-3">
<img
src={`${router.basePath}` + menu.icon ?? `/img/icons/menu/${id}.svg`}
src={`${BASE_PATH}` + menu.icon ?? `/img/icons/menu/${id}.svg`}
className="w-5 rounded"
/>
@@ -1,14 +1,14 @@
'use client'
import * as Accordion from '@radix-ui/react-accordion'
import { useRouter } from 'next/router'
import * as NavItems from './NavigationMenu.constants'
import { getPathWithoutHash } from './NavigationMenu.utils'
import NavigationMenuGuideListItems from './NavigationMenuGuideListItems'
import { usePathname } from 'next/navigation'
const NavigationMenuGuideList = ({ id }: { id: string }) => {
const path = useRouter().asPath
const url = getPathWithoutHash(path)
const firstLevelRoute = url?.split('/')?.slice(0, 4)?.join('/')
const pathname = usePathname()
const firstLevelRoute = pathname?.split('/')?.slice(0, 4)?.join('/')
const menu = NavItems[id]
@@ -1,27 +1,25 @@
import * as Accordion from '@radix-ui/react-accordion'
import { ChevronLeft } from 'lucide-react'
import { usePathname } from 'next/navigation'
import { useTheme } from 'next-themes'
import Image from 'next/legacy/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React, { useEffect, useRef } from 'react'
import HomeMenuIconPicker from './HomeMenuIconPicker'
import { getPathWithoutHash } from './NavigationMenu.utils'
import MenuIconPicker from './MenuIconPicker'
const HeaderLink = React.memo(function HeaderLink(props: {
title: string
id: string
url: string
}) {
const router = useRouter()
const pathname = usePathname()
return (
<span
className={[
' ',
!props.title && 'capitalize',
props.url === router.asPath ? 'text-brand' : 'hover:text-brand text-foreground',
props.url === pathname ? 'text-brand' : 'hover:text-brand text-foreground',
].join(' ')}
>
{props.title ?? props.id}
@@ -30,10 +28,10 @@ const HeaderLink = React.memo(function HeaderLink(props: {
})
const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any) {
const router = useRouter()
const pathname = usePathname()
const { resolvedTheme } = useTheme()
const activeItem = props.subItem.url === getPathWithoutHash(router.asPath)
const activeItemRef = useRef(null)
const activeItem = props.subItem.url === pathname
const activeItemRef = useRef<HTMLLIElement>(null)
const LinkContainer = (props) => {
return (
@@ -77,11 +75,8 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
>
{props.subItem.icon && (
<Image
alt={props.subItem.name + router.basePath}
src={
`${router.basePath}` +
`${props.subItem.icon}${!resolvedTheme?.includes('dark') ? '-light' : ''}.svg`
}
alt={props.subItem.name}
src={`${props.subItem.icon}${!resolvedTheme?.includes('dark') ? '-light' : ''}.svg`}
width={15}
height={15}
/>
@@ -99,7 +94,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
href={`${subSubItem.url}`}
className={[
'cursor-pointer transition text-sm',
subSubItem.url === getPathWithoutHash(router.asPath)
subSubItem.url === pathname
? 'text-brand'
: 'hover:text-brand text-foreground-lighter',
].join(' ')}
@@ -117,7 +112,7 @@ const ContentAccordionLink = React.memo(function ContentAccordionLink(props: any
})
const ContentLink = React.memo(function ContentLink(props: any) {
const router = useRouter()
const pathname = usePathname()
return (
<li className="mb-1.5">
@@ -125,13 +120,13 @@ const ContentLink = React.memo(function ContentLink(props: any) {
href={props.url}
className={[
'cursor-pointer transition text-sm',
props.url === router.asPath
props.url === pathname
? 'text-brand-link'
: 'hover:text-foreground text-foreground-lighter',
].join(' ')}
>
{props.icon && (
<Image alt={props.icon} width={12} height={12} src={`${router.basePath}${props.icon}`} />
<Image alt={props.icon} width={12} height={12} src={`${pathname}${props.icon}`} />
)}
{props.name}
</Link>
@@ -144,24 +139,9 @@ const Content = (props) => {
return (
<ul className={['relative w-full flex flex-col gap-0 pb-5'].join(' ')}>
<Link
href={`${menu.parent ?? '/'}`}
className={[
'flex items-center gap-1 text-xs group mb-3',
'text-base transition-all duration-200 text-brand-link hover:text-brand-600 hover:cursor-pointer ',
].join(' ')}
>
<div className="relative w-2">
<div className="transition-all ease-out ml-0 group-hover:-ml-1">
<ChevronLeft size={10} strokeWidth={3} />
</div>
</div>
<span>Back to Home</span>
</Link>
<Link href={menu.url ?? ''}>
<div className="flex items-center gap-3 my-3 text-brand-link">
<HomeMenuIconPicker icon={menu.icon} />
<MenuIconPicker icon={menu.icon} />
<HeaderLink title={menu.title} url={menu.url} id={id} />
</div>
</Link>
@@ -1,20 +1,19 @@
'use client'
import * as Accordion from '@radix-ui/react-accordion'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { IconChevronLeft, IconChevronUp, cn } from 'ui'
import * as NavItems from './NavigationMenu.constants'
import { ChevronUp } from 'lucide-react'
import Image from 'next/legacy/image'
import { Fragment, memo } from 'react'
import { cn } from 'ui'
import RevVersionDropdown from '~/components/RefVersionDropdown'
import type { ICommonItem, ICommonSection } from '~/components/reference/Reference.types'
import { menuState, useMenuActiveRefId } from '~/hooks/useMenuState'
import React, { Fragment } from 'react'
import { ICommonItem, ICommonSection } from '~/components/reference/Reference.types'
import HomeMenuIconPicker from './HomeMenuIconPicker'
import { BASE_PATH } from '~/lib/constants'
import MenuIconPicker from './MenuIconPicker'
import * as NavItems from './NavigationMenu.constants'
import { deepFilterSections } from './NavigationMenu.utils'
const HeaderLink = React.memo(function HeaderLink(props: any) {
const HeaderLink = memo(function HeaderLink(props: any) {
return (
<span className={['text-base text-brand-600 ', !props.title && 'capitalize'].join(' ')}>
{props.title ?? props.id}
@@ -34,7 +33,7 @@ interface FunctionLinkProps {
onClick?: () => void
}
const FunctionLink = React.memo(function FunctionLink({
const FunctionLink = memo(function FunctionLink({
title,
id,
icon,
@@ -44,10 +43,9 @@ const FunctionLink = React.memo(function FunctionLink({
isSubItem = false,
onClick = () => {},
}: FunctionLinkProps) {
const router = useRouter()
const activeAccordionItem = useMenuActiveRefId()
const url = `${router.basePath}${basePath}/${slug}`
const url = `${BASE_PATH}${basePath}/${slug}`
const active = activeAccordionItem === id
return (
@@ -61,7 +59,10 @@ const FunctionLink = React.memo(function FunctionLink({
onClick={(e) => {
e.preventDefault()
history.pushState({}, '', url)
document.getElementById(slug)?.scrollIntoView()
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
document.getElementById(slug)?.scrollIntoView({
behavior: reduceMotion ? 'auto' : 'smooth',
})
onClick()
}}
className={cn(
@@ -70,7 +71,7 @@ const FunctionLink = React.memo(function FunctionLink({
active ? 'text-brand' : 'text-foreground-lighter'
)}
>
{icon && <Image width={16} height={16} alt={icon} src={`${router.basePath}${icon}`} />}
{icon && <Image width={16} height={16} alt={icon} src={`${BASE_PATH}${icon}`} />}
{title}
{active && !isSubItem && (
<div
@@ -79,7 +80,7 @@ const FunctionLink = React.memo(function FunctionLink({
></div>
)}
{isParent && (
<IconChevronUp
<ChevronUp
width={16}
className="data-open-parent:rotate-0 data-closed-parent:rotate-90 transition"
/>
@@ -94,7 +95,7 @@ export interface RenderLinkProps {
basePath: string
}
const RenderLink = React.memo(function RenderLink({ section, basePath }: RenderLinkProps) {
const RenderLink = memo(function RenderLink({ section, basePath }: RenderLinkProps) {
const activeAccordionItem = useMenuActiveRefId()
if (!('items' in section)) {
@@ -181,22 +182,8 @@ const NavigationMenuRefListItems = ({
return (
<div className={'w-full flex flex-col gap-0 sticky top-8'}>
<Link
href="/"
className={[
'flex items-center gap-1 text-xs group mb-3',
'text-base transition-all duration-200 text-foreground-light hover:text-brand-600 hover:cursor-pointer',
].join(' ')}
>
<div className="relative w-2">
<div className="transition-all ease-out ml-0 group-hover:-ml-1">
<IconChevronLeft size={10} strokeWidth={3} />
</div>
</div>
<span>Back to Main Menu</span>
</Link>
<div className="flex items-center gap-3 my-3">
<HomeMenuIconPicker icon={menu.icon} width={21} height={21} />
<MenuIconPicker icon={menu.icon} width={21} height={21} />
<HeaderLink title={menu.title} url={menu.url} id={id} />
<RevVersionDropdown />
</div>
@@ -223,4 +210,4 @@ const NavigationMenuRefListItems = ({
)
}
export default React.memo(NavigationMenuRefListItems)
export default memo(NavigationMenuRefListItems)
@@ -1,108 +1,151 @@
import { useTheme } from 'next-themes'
import Image from 'next/legacy/image'
import { FC, memo, useState } from 'react'
import Link from 'next/link'
import { FC } from 'react'
import { Button, IconCommand, IconGitHub, IconSearch } from 'ui'
import { SearchButton } from 'ui-patterns/Cmdk'
import dynamic from 'next/dynamic'
import { useIsLoggedIn, useIsUserLoading } from 'common'
import { ThemeToggle } from 'ui-patterns/ThemeToggle'
import { SearchButton } from 'ui-patterns/Cmdk'
import { Button, buttonVariants, cn } from 'ui'
import { Command, Search, Menu } from 'lucide-react'
import Image from 'next/image'
import GlobalNavigationMenu from './GlobalNavigationMenu'
const GlobalMobileMenu = dynamic(() => import('./GlobalMobileMenu'))
const TopNavDropdown = dynamic(() => import('./TopNavDropdown'))
const TopNavBar: FC = () => {
const isLoggedIn = useIsLoggedIn()
const isUserLoading = useIsUserLoading()
const { resolvedTheme } = useTheme()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return (
<nav
aria-label="top bar"
className="h-[var(--header-height,60px)] border-b backdrop-blur backdrop-filter bg bg-opacity-75"
>
<div className="px-5 max-w-7xl mx-auto flex gap-3 justify-between items-center h-full">
<div className="lg:hidden">
<Link href="/" className=" flex items-center gap-2">
<Image
className="cursor-pointer"
src={
resolvedTheme?.includes('dark')
? '/docs/supabase-dark.svg'
: '/docs/supabase-light.svg'
}
width={96}
height={24}
alt="Supabase Logo"
/>
<span className="font-mono text-sm font-medium text-brand-link">DOCS</span>
</Link>
</div>
<div className="flex items-center gap-6">
<SearchButton className="md:w-full lg:w-96 order-2 lg:order-1">
<div
className="
flex
group
items-center
justify-between
bg-surface-100
bg-opacity-75
hover:bg-surface-200
hover:bg-opacity-100
border
transition
pl-1.5 md:pl-3 pr-1.5 w-full h-[32px] rounded
text-foreground-lighter
"
>
<div className="flex items-center space-x-2">
<IconSearch size={18} strokeWidth={2} />
<p className="hidden md:flex text-sm">Search docs...</p>
</div>
<div className="hidden md:flex items-center space-x-1">
<div
aria-hidden="true"
className="md:flex items-center justify-center h-5 w-10 border rounded bg-surface-300 gap-1"
>
<IconCommand size={12} strokeWidth={1.5} />
<span className="text-[12px]">K</span>
</div>
</div>
<>
<nav
aria-label="top bar"
className="w-full z-40 flex flex-col border-b backdrop-blur backdrop-filter bg bg-opacity-75"
>
<div className="w-full px-5 lg:pl-10 flex justify-between h-[var(--header-height)] gap-3">
<div className="hidden lg:flex h-full items-center justify-center gap-2">
<HeaderLogo />
<GlobalNavigationMenu />
</div>
<div className="w-full grow lg:w-auto flex gap-3 justify-between lg:justify-end items-center h-full">
<div className="lg:hidden">
<HeaderLogo />
</div>
</SearchButton>
<div className="flex gap-2 items-center">
<SearchButton
className="flex-grow md:w-44 xl:w-56 h-[30px]
focus-visible:!outline-4
focus-visible:outline-offset-1
focus-visible:outline-brand-600"
>
<div
className="
flex
group
items-center
justify-between
bg-surface-100/75
hover:bg-opacity-100
hover:border-strong
border
transition
pl-1.5 md:pl-2 pr-1 w-full h-full rounded-md
text-foreground-lighter
"
>
<div className="flex items-center space-x-2 text-foreground-muted">
<Search size={18} strokeWidth={2} />
<p className="flex text-sm pr-2">
Search<span className="hidden xl:inline ml-1"> docs...</span>
</p>
</div>
<div className="hidden md:flex items-center space-x-1">
<div
aria-hidden="true"
className="md:flex items-center justify-center h-full px-1 border rounded bg-surface-300 gap-0.5"
>
<Command size={12} strokeWidth={1.5} />
<span className="text-[12px]">K</span>
</div>
</div>
</div>
</SearchButton>
<button
title="Menu dropdown button"
className={cn(
buttonVariants({ type: 'default' }),
'flex lg:hidden border-default bg-surface-100/75 text-foreground-light rounded-md min-w-[30px] w-[30px] h-[30px] data-[state=open]:bg-overlay-hover/30'
)}
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
<Menu size={18} strokeWidth={1} />
</button>
</div>
</div>
<div className="hidden lg:flex items-center justify-end gap-3">
{!isUserLoading && (
<Button asChild>
<a
href="https://supabase.com/dashboard"
className="h-[30px]"
target="_blank"
rel="noreferrer noopener"
>
{isLoggedIn ? 'Dashboard' : 'Sign up'}
</a>
</Button>
)}
{process.env.NEXT_PUBLIC_DEV_AUTH_PAGE === 'true' && (
<Button asChild>
<Link href="/__dev-secret-auth">Dev-only secret sign-in</Link>
</Button>
)}
<TopNavDropdown />
</div>
</div>
<div className="hidden lg:flex grow items-center justify-end gap-3">
<Button type="text" asChild>
<a href="https://supabase.com" target="_blank" rel="noreferrer noopener">
Supabase.com
</a>
</Button>
{!isUserLoading && (
<Button asChild>
<a href="https://supabase.com/dashboard" target="_blank" rel="noreferrer noopener">
{isLoggedIn ? 'Dashboard' : 'Sign up'}
</a>
</Button>
)}
{process.env.NEXT_PUBLIC_DEV_AUTH_PAGE === 'true' && (
<Button asChild>
<Link href="/__dev-secret-auth">Dev-only secret sign-in</Link>
</Button>
)}
<Link
href="https://github.com/supabase/supabase"
target="_blank"
rel="noreferrer noopener"
className="px-2.5 py-1"
>
<span className="sr-only">GitHub</span>
<IconGitHub
size={16}
className="text-foreground-light hover:text-foreground transition"
/>
</Link>
<ThemeToggle />
</div>
</div>
</nav>
</nav>
<GlobalMobileMenu open={mobileMenuOpen} setOpen={setMobileMenuOpen} />
</>
)
}
const HeaderLogo = memo(() => {
return (
<Link
href="/"
className={cn(
buttonVariants({ type: 'default' }),
'flex shrink-0 items-center w-fit !bg-transparent !border-none !shadow-none'
)}
>
<Image
className="hidden dark:block !m-0"
src="/docs/supabase-dark.svg"
priority={true}
loading="eager"
width={96}
height={18}
alt="Supabase wordmark"
/>
<Image
className="block dark:hidden !m-0"
src="/docs/supabase-light.svg"
priority={true}
loading="eager"
width={96}
height={18}
alt="Supabase wordmark"
/>
<span className="font-mono text-sm font-medium text-brand-link mb-px">DOCS</span>
</Link>
)
})
HeaderLogo.displayName = 'HeaderLogo'
TopNavBar.displayName = 'TopNavBar'
export default TopNavBar
@@ -0,0 +1,116 @@
'use client'
import React, { Fragment } from 'react'
import Link from 'next/link'
import { useTheme } from 'next-themes'
import { Menu } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Theme,
buttonVariants,
cn,
themes,
} from 'ui'
import MenuIconPicker from './MenuIconPicker'
const menu = [
[
{
label: 'Supabase.com',
icon: 'home',
href: 'https://supabase.com',
otherProps: {
target: '_blank',
rel: 'noreferrer noopener',
},
},
{
label: 'GitHub',
icon: 'github',
href: 'https://github.com/supabase/supabase',
otherProps: {
target: '_blank',
rel: 'noreferrer noopener',
},
},
{
label: 'Support',
icon: 'support',
href: 'https://supabase.com/support',
otherProps: {
target: '_blank',
rel: 'noreferrer noopener',
},
},
],
]
const TopNavDropdown = () => {
const { theme, setTheme } = useTheme()
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild className="flex">
<button
title="Menu dropdown button"
className={cn(
buttonVariants({ type: 'default' }),
'text-foreground-light border-default w-[30px] min-w-[30px] h-[30px] data-[state=open]:bg-overlay-hover/30 hover:border-strong data-[state=open]:border-stronger hover:!bg-overlay-hover/50 bg-transparent'
)}
>
<Menu size={18} strokeWidth={1} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-64">
{menu.map((menuSection, sectionIdx) => (
<Fragment key={`topnav--${sectionIdx}`}>
{sectionIdx !== 0 && <DropdownMenuSeparator key={`topnav--${sectionIdx}`} />}
{menuSection.map((sectionItem, itemIdx) => (
<Link
key={`topnav-${sectionItem.label}-${sectionIdx}-${itemIdx}`}
href={sectionItem.href}
{...sectionItem.otherProps}
>
<DropdownMenuItem className="space-x-2" onClick={() => {}}>
{sectionItem.icon && (
<MenuIconPicker icon={sectionItem.icon} className="text-foreground-lighter" />
)}
<p>{sectionItem.label}</p>
</DropdownMenuItem>
</Link>
))}
</Fragment>
))}
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuLabel>Theme</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={theme}
onValueChange={(value) => {
setTheme(value)
}}
>
{themes
.filter((x) => x.value === 'light' || x.value === 'dark' || x.value === 'system')
.map((theme: Theme) => (
<DropdownMenuRadioItem key={`topnav-theme-${theme.value}`} value={theme.value}>
{theme.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
export default TopNavDropdown
+10 -10
View File
@@ -1,6 +1,6 @@
import Link from 'next/link'
import Image from 'next/legacy/image'
import { useRouter } from 'next/router'
import { usePathname } from 'next/navigation'
import { IconChevronRight, IconArrowLeft } from '~/../../packages/ui'
import { REFERENCES } from './NavigationMenu/NavigationMenu.constants'
@@ -8,8 +8,8 @@ import { NavMenuGroup, NavMenuSection } from './Navigation.types'
import * as Accordion from '@radix-ui/react-accordion'
const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
const { asPath } = useRouter()
const pathSegments = asPath.split('/')
const pathname = usePathname()
const pathSegments = pathname.split('/')
const isInReferencePages = pathSegments.includes('reference') && pathSegments.length >= 3
const referenceMeta = pathSegments.length >= 3 ? REFERENCES[pathSegments[2]] : undefined
@@ -18,11 +18,11 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
const foundItem = group.items.find((section) => {
if (section.items.length > 0) {
const foundSubItem = section.items.find((item) => {
if (item.url === asPath) return item
if (item.url === pathname) return item
})
if (foundSubItem) return section
} else {
if (section.url === asPath) return section
if (section.url === pathname) return section
}
})
if (foundItem) return group
@@ -35,7 +35,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
return undefined
} else {
return section.items.find((item) => {
if (item.url === asPath) return item
if (item.url === pathname) return item
})
}
})
@@ -43,7 +43,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
return (
<div
className="bg-background border-muted sidebar-width sticky top-16
className="bg-background border-muted sidebar-width sticky top-44
h-screen overflow-y-scroll border-r py-8 px-6 sidebar-menu-container hidden lg:block"
>
{isInReferencePages && (
@@ -79,7 +79,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
className={[
'py-1.5 px-5 rounded text-sm transition',
`${
item.url === asPath
item.url === pathname
? 'bg-background text-brand-link'
: 'text-foreground-light hover:text-foreground'
}`,
@@ -118,7 +118,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
className={[
'py-1.5 px-5 rounded text-sm transition',
`${
section.url === asPath
section.url === pathname
? 'bg-background text-brand'
: 'text-foreground-light hover:text-foreground'
}`,
@@ -156,7 +156,7 @@ const SideBar = ({ menuItems = [] }: { menuItems: any }) => {
className={[
'py-1.5 ml-4 px-5 rounded text-sm transition',
`${
item.url === asPath
item.url === pathname
? 'bg-background text-brand'
: 'text-foreground-light hover:text-foreground'
}`,
+1 -1
View File
@@ -50,7 +50,7 @@ const Option: FC<IOption> = (props) => {
flex flex-col gap-3
"
>
<div className="flex gap-3 items-center">
<div className="flex flex-wrap gap-3 items-center">
<span className="text-sm text-foreground font-mono font-medium">
{props.name ?? 'no-name'}
</span>
Loaded 100 of 1414 files, more files were not shown because too many files have changed in this diff. Show more