diff --git a/apps/design-system/__registry__/index.tsx b/apps/design-system/__registry__/index.tsx index cead6b766f8..19308aeefbb 100644 --- a/apps/design-system/__registry__/index.tsx +++ b/apps/design-system/__registry__/index.tsx @@ -38,6 +38,39 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "PageContainer": { + name: "PageContainer", + type: "components:fragment", + registryDependencies: undefined, + component: React.lazy(() => import("@/../../packages/ui-patterns/src/PageContainer")), + source: "", + files: ["registry/default//PageContainer/index.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "PageHeader": { + name: "PageHeader", + type: "components:fragment", + registryDependencies: undefined, + component: React.lazy(() => import("@/../../packages/ui-patterns/src/PageHeader")), + source: "", + files: ["registry/default//PageHeader/index.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "PageSection": { + name: "PageSection", + type: "components:fragment", + registryDependencies: undefined, + component: React.lazy(() => import("@/../../packages/ui-patterns/src/PageSection")), + source: "", + files: ["registry/default//PageSection/index.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "accordion-demo": { name: "accordion-demo", type: "components:example", @@ -1886,6 +1919,127 @@ export const Index: Record = { subcategory: "undefined", chunks: [] }, + "page-components-integrated": { + name: "page-components-integrated", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-components-integrated")), + source: "", + files: ["registry/default/example/page-components-integrated.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-container-demo": { + name: "page-container-demo", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-container-demo")), + source: "", + files: ["registry/default/example/page-container-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-layout-detail": { + name: "page-layout-detail", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-layout-detail")), + source: "", + files: ["registry/default/example/page-layout-detail.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-layout-full-page": { + name: "page-layout-full-page", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-layout-full-page")), + source: "", + files: ["registry/default/example/page-layout-full-page.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-layout-list": { + name: "page-layout-list", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-layout-list")), + source: "", + files: ["registry/default/example/page-layout-list.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-layout-list-simple": { + name: "page-layout-list-simple", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-layout-list-simple")), + source: "", + files: ["registry/default/example/page-layout-list-simple.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-layout-settings": { + name: "page-layout-settings", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-layout-settings")), + source: "", + files: ["registry/default/example/page-layout-settings.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-header-demo": { + name: "page-header-demo", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-header-demo")), + source: "", + files: ["registry/default/example/page-header-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-section-demo": { + name: "page-section-demo", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-section-demo")), + source: "", + files: ["registry/default/example/page-section-demo.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-section-horizontal": { + name: "page-section-horizontal", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-section-horizontal")), + source: "", + files: ["registry/default/example/page-section-horizontal.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, + "page-section-with-aside": { + name: "page-section-with-aside", + type: "components:example", + registryDependencies: undefined, + component: React.lazy(() => import("@/registry/default/example/page-section-with-aside")), + source: "", + files: ["registry/default/example/page-section-with-aside.tsx"], + category: "undefined", + subcategory: "undefined", + chunks: [] + }, "inner-side-menu-demo": { name: "inner-side-menu-demo", type: "components:example", diff --git a/apps/design-system/app/layout.tsx b/apps/design-system/app/layout.tsx index 9c005db6ae4..72c8d4f5c04 100644 --- a/apps/design-system/app/layout.tsx +++ b/apps/design-system/app/layout.tsx @@ -1,4 +1,5 @@ import '@/styles/globals.css' +import '../../studio/styles/typography.scss' import type { Metadata } from 'next' import { ThemeProvider } from './Providers' import { SonnerToaster } from './SonnerToast' diff --git a/apps/design-system/components/component-preview.tsx b/apps/design-system/components/component-preview.tsx index c1304ec5986..b9af0dbeea1 100644 --- a/apps/design-system/components/component-preview.tsx +++ b/apps/design-system/components/component-preview.tsx @@ -120,11 +120,11 @@ export function ComponentPreview({ ) }, [Preview, align]) - const wideClasses = wide ? '2xl:-ml-12 2xl:-mr-12' : '' + const wideClasses = wide ? '2xl:-ml-32 2xl:-mr-32' : '' if (peekCode) { return ( -
+
+ +## Usage + +```tsx +import { PageContainer } from 'ui-patterns/PageContainer' +``` + +```tsx + + {/* Page content */} + +``` + +## Size Variants + +The `PageContainer` supports four size variants: + +- `small` - max-width: 768px +- `default` - max-width: 1200px +- `large` - max-width: 1600px +- `full` - no max-width constraint + diff --git a/apps/design-system/content/docs/fragments/page-header.mdx b/apps/design-system/content/docs/fragments/page-header.mdx new file mode 100644 index 00000000000..6908cd2e2f5 --- /dev/null +++ b/apps/design-system/content/docs/fragments/page-header.mdx @@ -0,0 +1,23 @@ +--- +title: Page Header +description: A compound component for building page headers with consistent structure. +fragment: true +--- + + + +## Sub-components + +- `PageHeader.Root` - Root container with size variants (`default`, `small`, `large`, `full`) +- `PageHeader.Breadcrumb` - Breadcrumb navigation wrapper +- `PageHeader.Icon` - Icon container positioned left of title +- `PageHeader.Summary` - Container for title and description +- `PageHeader.Title` - Primary page heading (h1) +- `PageHeader.Description` - Supporting text below title +- `PageHeader.Aside` - Container for action buttons +- `PageHeader.Footer` - Container for tab navigation (NavMenu) diff --git a/apps/design-system/content/docs/fragments/page-section.mdx b/apps/design-system/content/docs/fragments/page-section.mdx new file mode 100644 index 00000000000..36b238ee0f0 --- /dev/null +++ b/apps/design-system/content/docs/fragments/page-section.mdx @@ -0,0 +1,46 @@ +--- +title: Page Section +description: A compound component for organizing page content into distinct sections. +fragment: true +--- + + + +## Sub-components + +- `PageSection.Root` - Root container with orientation variants (`horizontal` or `vertical`) +- `PageSection.Summary` - Container for section title and description +- `PageSection.Title` - Section heading (h2) +- `PageSection.Description` - Supporting text below section title +- `PageSection.Aside` - Container for section-level actions +- `PageSection.Content` - Container for the main section content + +## Orientation Variants + +- `vertical` - Content stacks vertically (default) +- `horizontal` - Summary and content arranged horizontally on larger screens + +## Examples + +### Horizontal Orientation + + + +### With Aside Actions + + diff --git a/apps/design-system/content/docs/ui-patterns/page.mdx b/apps/design-system/content/docs/ui-patterns/page.mdx new file mode 100644 index 00000000000..abf65f30139 --- /dev/null +++ b/apps/design-system/content/docs/ui-patterns/page.mdx @@ -0,0 +1,80 @@ +--- +title: Page +description: Page components provide consistent structure and layout for page-level content. +--- + +The Page pattern consists of three main components that work together to create consistent page layouts: `PageContainer`, `PageHeader`, and `PageSection`. These components provide a structured approach to building pages with consistent spacing, max-widths, and content organization. + +## Layouts + +### Settings + +Settings pages are used for configuration and preference management. They follow a single-column layout with default widths to keep content focused and readable. + +- Use `PageHeader` with `size="default"` +- Use `PageContainer` with `size="default"` +- Use `PageSection` for organizing settings into logical groups + + + +### List + +List pages display collections of objects like tables, triggers, or functions. These pages use larger widths to accommodate wide content like data tables. + +- Use `PageHeader` with `size="large"` +- Use `PageContainer` with `size="large"` +- Use `PageSection` to wrap list content + +**Table and List Actions:** + +- **With filters or search:** If the table has filters or search, place table actions aligned with the filters on the right side. Do not use `PageSection.Aside` or `PageHeader.Aside` for table actions when filters are present. +- **Without filters:** For simple lists without filters or search, add primary list actions to `PageHeader.Aside` or `PageSection.Aside` as appropriate. + + + + + +### Data and Full Page Experiences + +Full-page experiences like editors require maximum screen real estate. These pages use full-width containers to maximize available space. + +- Use `PageHeader` with `size="full"` +- Use `PageContainer` with `size="full"` +- Content spans the full width of the viewport + +### Detail Pages + +Detail pages display dense or lengthy content split into multiple sections. The horizontal orientation allows for better information hierarchy and context. + +- Use `PageHeader` with `size="large"` +- Use `PageContainer` with `size="large"` +- Use `PageSection` with `orientation="horizontal"` to show summary alongside content +- Multiple sections can stack vertically with horizontal layouts within each + + + +## Components + +- **[PageContainer](/docs/fragments/page-container)** - Container component providing consistent max-width and padding based on size variants +- **[PageHeader](/docs/fragments/page-header)** - Compound component for building page headers with breadcrumbs, icons, titles, descriptions, actions, and navigation +- **[PageSection](/docs/fragments/page-section)** - Compound component for organizing page content into distinct sections with title, description, and action areas diff --git a/apps/design-system/package.json b/apps/design-system/package.json index 928d7cc74dc..20d600a3985 100644 --- a/apps/design-system/package.json +++ b/apps/design-system/package.json @@ -51,6 +51,7 @@ }, "devDependencies": { "@shikijs/compat": "^1.1.7", + "@tailwindcss/container-queries": "^0.1.1", "@types/lodash.template": "4.5.0", "@types/react": "catalog:", "@types/react-dom": "catalog:", diff --git a/apps/design-system/registry/default/example/page-components-integrated.tsx b/apps/design-system/registry/default/example/page-components-integrated.tsx new file mode 100644 index 00000000000..9ce9b94799c --- /dev/null +++ b/apps/design-system/registry/default/example/page-components-integrated.tsx @@ -0,0 +1,45 @@ +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageHeader } from 'ui-patterns/PageHeader' +import { PageSection } from 'ui-patterns/PageSection' +import { Button, Card, CardContent } from 'ui' + +export default function PageComponentsIntegrated() { + return ( +
+ + + Edge Functions + + Manage and deploy serverless functions that run at the edge. + + + + + + + + + + + Function Details + + Configure your function settings and view logs. + + + + + +

+ This demonstrates how PageContainer, PageHeader, and PageSection work together to + create a complete page layout. +

+
+
+
+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/page-container-demo.tsx b/apps/design-system/registry/default/example/page-container-demo.tsx new file mode 100644 index 00000000000..85b7bc11acf --- /dev/null +++ b/apps/design-system/registry/default/example/page-container-demo.tsx @@ -0,0 +1,21 @@ +import { PageContainer } from 'ui-patterns/PageContainer' +import { Button } from 'ui' + +export default function PageContainerDemo() { + return ( +
+ +

This is a page container with small size (768px max-width)

+
+ +

This is a page container with default size (1200px max-width)

+
+ +

This is a page container with large size (1600px max-width)

+
+ +

This is a page container with full size (no max-width)

+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/page-header-demo.tsx b/apps/design-system/registry/default/example/page-header-demo.tsx new file mode 100644 index 00000000000..6402405647a --- /dev/null +++ b/apps/design-system/registry/default/example/page-header-demo.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import Link from 'next/link' +import { PageHeader } from 'ui-patterns/PageHeader' +import { + Button, + NavMenu, + NavMenuItem, + BreadcrumbItem_Shadcn_ as BreadcrumbItem, + BreadcrumbLink_Shadcn_ as BreadcrumbLink, + BreadcrumbList_Shadcn_ as BreadcrumbList, + BreadcrumbSeparator_Shadcn_ as BreadcrumbSeparator, +} from 'ui' +import { Database } from 'lucide-react' + +export default function PageHeaderDemo() { + const breadcrumbItems = [ + { label: 'Project', href: '/project/demo' }, + { label: 'Edge Functions', href: '/project/demo/functions' }, + ] + + const navigationItems = [ + { label: 'Overview', href: '/project/demo/functions/demo-function' }, + { label: 'Invocations', href: '/project/demo/functions/demo-function/invocations' }, + { label: 'Logs', href: '/project/demo/functions/demo-function/logs' }, + { label: 'Details', href: '/project/demo/functions/demo-function/details' }, + ] + + return ( +
+ + + + {breadcrumbItems.map((item, index) => ( + + + + {item.label} + + + {index < breadcrumbItems.length - 1 && } + + ))} + + + + + + + Demo Function + + Serverless functions that run at the edge with low latency and automatic scaling. + + + + + + + + + + + {navigationItems.map((item) => ( + + {item.label} + + ))} + + + +
+ ) +} diff --git a/apps/design-system/registry/default/example/page-layout-detail.tsx b/apps/design-system/registry/default/example/page-layout-detail.tsx new file mode 100644 index 00000000000..6a926f0ae2a --- /dev/null +++ b/apps/design-system/registry/default/example/page-layout-detail.tsx @@ -0,0 +1,115 @@ +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageHeader } from 'ui-patterns/PageHeader' +import { PageSection } from 'ui-patterns/PageSection' +import { Button, Card, CardContent } from 'ui' + +export default function PageLayoutDetail() { + return ( +
+ + + Billing + + Manage your organization's billing and subscription settings. + + + + + + + + Subscription + + View and manage your current subscription plan and billing cycle. + + + + + +
+
+

Current Plan

+

Pro Plan

+
+
+

Billing Cycle

+

Monthly

+
+
+

Next Billing Date

+

March 15, 2024

+
+
+ +
+
+
+
+
+
+ + + + Cost Control + + Set spending limits and alerts to manage your organization's costs. + + + + + +
+
+

Monthly Spending Limit

+

$500.00

+
+
+

Current Month Spend

+

$234.50

+
+
+ +
+
+
+
+
+
+ + + + Payment Methods + + Manage payment methods and billing information for your organization. + + + + + +
+
+

Primary Payment Method

+

•••• •••• •••• 4242

+
+
+

Expires

+

12/2025

+
+
+ +
+
+
+
+
+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/page-layout-full-page.tsx b/apps/design-system/registry/default/example/page-layout-full-page.tsx new file mode 100644 index 00000000000..f9e1ddc39f2 --- /dev/null +++ b/apps/design-system/registry/default/example/page-layout-full-page.tsx @@ -0,0 +1,34 @@ +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageHeader } from 'ui-patterns/PageHeader' +import { Button } from 'ui' + +export default function PageLayoutFullPage() { + return ( +
+ + + SQL Editor + + Write and execute SQL queries with syntax highlighting and auto-complete. + + + + + + + + + +
+

+ Editor content takes full width for maximum screen real estate. +

+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/page-layout-list-simple.tsx b/apps/design-system/registry/default/example/page-layout-list-simple.tsx new file mode 100644 index 00000000000..14a1344dbce --- /dev/null +++ b/apps/design-system/registry/default/example/page-layout-list-simple.tsx @@ -0,0 +1,87 @@ +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageHeader } from 'ui-patterns/PageHeader' +import { PageSection } from 'ui-patterns/PageSection' +import { + Button, + Card, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from 'ui' + +export default function PageLayoutListSimple() { + const items = [ + { id: 1, name: 'Project Alpha', status: 'Active', members: 12 }, + { id: 2, name: 'Project Beta', status: 'Active', members: 8 }, + { id: 3, name: 'Project Gamma', status: 'Inactive', members: 5 }, + ] + + return ( +
+ + + Projects + + Manage and view all your projects in one place. + + + + + + + + + + + + + + + Name + Status + Members + Actions + + + + {items.map((item) => ( + + {item.name} + {item.status} + {item.members} + + + + + + + Edit + Duplicate + + Delete + + + + + ))} + +
+
+
+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/page-layout-list.tsx b/apps/design-system/registry/default/example/page-layout-list.tsx new file mode 100644 index 00000000000..593c24f0b13 --- /dev/null +++ b/apps/design-system/registry/default/example/page-layout-list.tsx @@ -0,0 +1,118 @@ +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageHeader } from 'ui-patterns/PageHeader' +import { PageSection } from 'ui-patterns/PageSection' +import { + Button, + Card, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, + Input, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from 'ui' +import { Search } from 'lucide-react' + +export default function PageLayoutList() { + const functions = [ + { + id: 1, + name: 'get_user_profile', + arguments: 'user_id uuid', + return_type: 'jsonb', + security: 'Definer', + }, + { + id: 2, + name: 'update_user_settings', + arguments: 'user_id uuid, settings jsonb', + return_type: 'void', + security: 'Invoker', + }, + { + id: 3, + name: 'calculate_total', + arguments: 'amount numeric, tax_rate numeric', + return_type: 'numeric', + security: 'Definer', + }, + ] + + return ( +
+ + + Database Functions + Manage your database functions + + + + + + +
+
+
+ } + className="w-full lg:w-52" + /> +
+ +
+ + + + + + Name + Arguments + Return type + Security + + + + + {functions.map((fn) => ( + + {fn.name} + {fn.arguments} + {fn.return_type} + {fn.security} + + + + + + + Edit + Duplicate + + + Delete + + + + + + ))} + +
+
+
+
+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/page-layout-settings.tsx b/apps/design-system/registry/default/example/page-layout-settings.tsx new file mode 100644 index 00000000000..4ade57ee998 --- /dev/null +++ b/apps/design-system/registry/default/example/page-layout-settings.tsx @@ -0,0 +1,234 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import * as z from 'zod' +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageHeader } from 'ui-patterns/PageHeader' +import { PageSection } from 'ui-patterns/PageSection' +import { + Button, + Card, + CardContent, + CardFooter, + FormControl_Shadcn_, + FormField_Shadcn_, + Form_Shadcn_, + Input_Shadcn_, + PrePostTab, + Switch, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +const RefreshTokenSchema = z.object({ + REFRESH_TOKEN_ROTATION_ENABLED: z.boolean(), + SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: z.coerce.number().min(0), +}) + +const UserSessionsSchema = z.object({ + SESSIONS_TIMEBOX: z.coerce.number().min(0), + SESSIONS_INACTIVITY_TIMEOUT: z.coerce.number().min(0), + SESSIONS_SINGLE_PER_USER: z.boolean(), +}) + +function HoursOrNeverText({ value }: { value: number }) { + if (value === 0) { + return 'never' + } else if (value === 1) { + return 'hour' + } else { + return 'hours' + } +} + +export default function PageLayoutSettings() { + const refreshTokenForm = useForm>({ + resolver: zodResolver(RefreshTokenSchema), + defaultValues: { + REFRESH_TOKEN_ROTATION_ENABLED: false, + SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 10, + }, + }) + + const userSessionsForm = useForm>({ + resolver: zodResolver(UserSessionsSchema), + defaultValues: { + SESSIONS_TIMEBOX: 0, + SESSIONS_INACTIVITY_TIMEOUT: 0, + SESSIONS_SINGLE_PER_USER: false, + }, + }) + + return ( +
+ + + User Sessions + + Configure settings for user sessions and refresh tokens + + + + + + + + Refresh Tokens + + Configure refresh token rotation and security settings. + + + + +
+ + + ( + + + + + + )} + /> + + + ( + + + + + + + + )} + /> + + + {refreshTokenForm.formState.isDirty && ( + + )} + + + +
+
+
+
+ + + + User Sessions + + Configure session timeout and single session enforcement settings. + + + + +
+ + + ( + + + + + + )} + /> + + + + ( + +
+ + }> + + + +
+
+ )} + /> +
+ + + ( + +
+ + }> + + + +
+
+ )} + /> +
+ + + {userSessionsForm.formState.isDirty && ( + + )} + + +
+
+
+
+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/page-section-demo.tsx b/apps/design-system/registry/default/example/page-section-demo.tsx new file mode 100644 index 00000000000..f39953ac260 --- /dev/null +++ b/apps/design-system/registry/default/example/page-section-demo.tsx @@ -0,0 +1,31 @@ +import { PageSection } from 'ui-patterns/PageSection' +import { Button, Card, CardContent } from 'ui' + +export default function PageSectionDemo() { + return ( +
+ + + Section Title + + This is a section with a title and description, plus optional actions. + + + + + + + + +

+ Section content goes here. This could be forms, tables, or any other content. +

+
+
+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/page-section-horizontal.tsx b/apps/design-system/registry/default/example/page-section-horizontal.tsx new file mode 100644 index 00000000000..a6900cc186c --- /dev/null +++ b/apps/design-system/registry/default/example/page-section-horizontal.tsx @@ -0,0 +1,29 @@ +import { PageSection } from 'ui-patterns/PageSection' +import { Card, CardContent } from 'ui' + +export default function PageSectionHorizontal() { + return ( +
+ + + Section Title + + In horizontal orientation, the summary (title and description) appears on the left, with + content arranged on the right on larger screens. This is useful for detailed sections + where you want to maintain context while viewing content. + + + + + +

+ The content area appears alongside the summary in a horizontal layout. On smaller + screens, it will stack vertically. +

+
+
+
+
+
+ ) +} diff --git a/apps/design-system/registry/default/example/page-section-with-aside.tsx b/apps/design-system/registry/default/example/page-section-with-aside.tsx new file mode 100644 index 00000000000..ec9131540d9 --- /dev/null +++ b/apps/design-system/registry/default/example/page-section-with-aside.tsx @@ -0,0 +1,35 @@ +import { PageSection } from 'ui-patterns/PageSection' +import { Button, Card, CardContent } from 'ui' + +export default function PageSectionWithAside() { + return ( +
+ + + Section Title + + This demonstrates PageSection with actions in the Aside component. + + + + + + + + + +

+ The Aside component positions actions horizontally aligned with the section summary, + providing a clear separation between description and actions. +

+
+
+
+
+
+ ) +} diff --git a/apps/design-system/registry/examples.ts b/apps/design-system/registry/examples.ts index 622605d6896..5944aeeab91 100644 --- a/apps/design-system/registry/examples.ts +++ b/apps/design-system/registry/examples.ts @@ -1078,6 +1078,56 @@ export const examples: Registry = [ type: 'components:example', files: ['example/info-tooltip-demo.tsx'], }, + { + name: 'page-components-integrated', + type: 'components:example', + files: ['example/page-components-integrated.tsx'], + }, + { + name: 'page-container-demo', + type: 'components:example', + files: ['example/page-container-demo.tsx'], + }, + { + name: 'page-layout-detail', + type: 'components:example', + files: ['example/page-layout-detail.tsx'], + }, + { + name: 'page-layout-full-page', + type: 'components:example', + files: ['example/page-layout-full-page.tsx'], + }, + { + name: 'page-layout-list', + type: 'components:example', + files: ['example/page-layout-list.tsx'], + }, + { + name: 'page-layout-settings', + type: 'components:example', + files: ['example/page-layout-settings.tsx'], + }, + { + name: 'page-header-demo', + type: 'components:example', + files: ['example/page-header-demo.tsx'], + }, + { + name: 'page-section-demo', + type: 'components:example', + files: ['example/page-section-demo.tsx'], + }, + { + name: 'page-section-horizontal', + type: 'components:example', + files: ['example/page-section-horizontal.tsx'], + }, + { + name: 'page-section-with-aside', + type: 'components:example', + files: ['example/page-section-with-aside.tsx'], + }, { name: 'inner-side-menu-demo', type: 'components:example', diff --git a/apps/design-system/registry/fragments.ts b/apps/design-system/registry/fragments.ts index 91c01149067..7534016172c 100644 --- a/apps/design-system/registry/fragments.ts +++ b/apps/design-system/registry/fragments.ts @@ -19,4 +19,22 @@ export const fragments: Registry = [ files: ['/Dialogs/ConfirmDialog.tsx'], optionalPath: '/Dialogs', }, + { + name: 'PageContainer', + type: 'components:fragment', + files: ['/PageContainer/index.tsx'], + optionalPath: '/PageContainer', + }, + { + name: 'PageHeader', + type: 'components:fragment', + files: ['/PageHeader/index.tsx'], + optionalPath: '/PageHeader', + }, + { + name: 'PageSection', + type: 'components:fragment', + files: ['/PageSection/index.tsx'], + optionalPath: '/PageSection', + }, ] diff --git a/apps/design-system/scripts/build-registry.mts b/apps/design-system/scripts/build-registry.mts index e2a9c5799da..392fa324ce8 100644 --- a/apps/design-system/scripts/build-registry.mts +++ b/apps/design-system/scripts/build-registry.mts @@ -207,17 +207,28 @@ export const Index: Record = { // console.log('item', item) let packagePath = '' + let importPath = '' if (type === 'ui') { packagePath = `../../packages/ui/src/components/shadcn/ui` + importPath = `${packagePath}/${item.name}` } if (type === 'fragment') { packagePath = `../../packages/ui-patterns/src${item.optionalPath}` + // If the file is index.tsx, import from the directory directly (without appending component name) + const firstFile = item.files[0] || '' + if (firstFile.endsWith('/index.tsx')) { + importPath = packagePath + } else { + importPath = `${packagePath}/${item.name}` + } } if (type === 'example') { packagePath = `registry/${style.name}/${type}` + importPath = `${packagePath}/${item.name}` } if (type === 'block') { packagePath = `registry/${style.name}/${type}` + importPath = `${packagePath}/${item.name}` } index += ` @@ -225,7 +236,7 @@ export const Index: Record = { name: "${item.name}", type: "${item.type}", registryDependencies: ${JSON.stringify(item.registryDependencies)}, - component: React.lazy(() => import("@/${packagePath}/${item.name}")), + component: React.lazy(() => import("@/${importPath}")), source: "${sourceFilename}", files: [${resolveFiles.map((file) => `"${file}"`)}], category: "${item.category}", diff --git a/apps/design-system/tailwind.config.js b/apps/design-system/tailwind.config.js index 10c142cc9cb..4d875930e75 100644 --- a/apps/design-system/tailwind.config.js +++ b/apps/design-system/tailwind.config.js @@ -10,6 +10,7 @@ module.exports = config({ './../../packages/ui/src/**/*.{tsx,ts,js}', './../../packages/ui-patterns/src/**/*.{tsx,ts,js}', ], + plugins: [require('@tailwindcss/container-queries')], theme: { extend: { maxWidth: { diff --git a/apps/studio/components/interfaces/Auth/AuthProvidersForm/index.tsx b/apps/studio/components/interfaces/Auth/AuthProvidersForm/index.tsx index 7bce15fc046..2a6ede20291 100644 --- a/apps/studio/components/interfaces/Auth/AuthProvidersForm/index.tsx +++ b/apps/studio/components/interfaces/Auth/AuthProvidersForm/index.tsx @@ -2,7 +2,7 @@ import { ExternalLink } from 'lucide-react' import Link from 'next/link' import { useParams } from 'common' -import { PageSection } from 'components/ui/PageSection' +import { PageSection } from 'ui-patterns/PageSection' import AlertError from 'components/ui/AlertError' import { ResourceList } from 'components/ui/Resource/ResourceList' import { HorizontalShimmerWithIcon } from 'components/ui/Shimmers' diff --git a/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm.tsx b/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm.tsx index d0d3a704c0c..fe9f25c069e 100644 --- a/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm.tsx +++ b/apps/studio/components/interfaces/Auth/BasicAuthSettingsForm.tsx @@ -8,7 +8,7 @@ import { toast } from 'sonner' import { boolean, object, string } from 'yup' import { useParams } from 'common' -import { PageSection } from 'components/ui/PageSection' +import { PageSection } from 'ui-patterns/PageSection' import AlertError from 'components/ui/AlertError' import { InlineLink } from 'components/ui/InlineLink' import NoPermission from 'components/ui/NoPermission' @@ -119,80 +119,112 @@ export const BasicAuthSettingsForm = () => { User Signups + {isError && ( + + )} - {isError && ( - - )} + {isPermissionsLoaded && !canReadConfig && ( +
+ +
+ )} - {isPermissionsLoaded && !canReadConfig && ( -
- -
- )} + {isLoading && ( + + + + + + + + + + + + + + + + )} - {isLoading && ( - - - - - - - - - - - - - - - - )} - - {isSuccess && ( - -
- - - ( - - - - - - )} - /> - - {showManualLinking && ( + {isSuccess && ( + + + ( + + + + + )} + /> + + {showManualLinking && ( + + ( + + Enable{' '} + + manual linking APIs + {' '} + for your project + + } + > + + + + + )} + /> + + )} + + ( + Enable{' '} - manual linking APIs + anonymous sign-ins {' '} for your project @@ -208,132 +240,99 @@ export const BasicAuthSettingsForm = () => { )} /> - - )} - - ( - - Enable{' '} - - anonymous sign-ins - {' '} - for your project - - } + + {form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && ( + - - - - - )} - /> - - {form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && ( - - -
- - Anonymous users will use the authenticated{' '} - role when signing in - - -

- As a result, anonymous users will be subjected to RLS policies that apply - to the public and{' '} - authenticated roles. We strongly advise{' '} - - reviewing your RLS policies - {' '} - to ensure that access to your data is restricted where required. -

- -
-
-
- )} - - {!authConfig?.SECURITY_CAPTCHA_ENABLED && - form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && ( - - - We highly recommend{' '} - - enabling captcha - {' '} - for anonymous sign-ins - - - This will prevent potential abuse on sign-ins which may bloat your database - and incur costs for monthly active users (MAU) - +
+ + Anonymous users will use the{' '} + authenticated role when signing in + + +

+ As a result, anonymous users will be subjected to RLS policies that + apply to the public and{' '} + authenticated roles. We strongly advise{' '} + + reviewing your RLS policies + {' '} + to ensure that access to your data is restricted where required. +

+ +
+
)} -
- - ( - - - - - + + {!authConfig?.SECURITY_CAPTCHA_ENABLED && + form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && ( + + + + We highly recommend{' '} + + enabling captcha + {' '} + for anonymous sign-ins + + + This will prevent potential abuse on sign-ins which may bloat your + database and incur costs for monthly active users (MAU) + + + )} + + + ( + + + + + + )} + /> + + + {form.formState.isDirty && ( + )} - /> - - - {form.formState.isDirty && ( - - )} - - -
- -
- )} + +
+ +
+ )}
) diff --git a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/index.tsx b/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/index.tsx index b982aef64b9..1be3308c694 100644 --- a/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/index.tsx +++ b/apps/studio/components/interfaces/Auth/ThirdPartyAuthForm/index.tsx @@ -4,7 +4,7 @@ import { useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' -import { PageSection } from 'components/ui/PageSection' +import { PageSection } from 'ui-patterns/PageSection' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import { InlineLink } from 'components/ui/InlineLink' diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx index ac248334869..e42ed912ae7 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails.tsx @@ -51,8 +51,8 @@ import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import CommandRender from '../CommandRender' import { INVOCATION_TABS } from './EdgeFunctionDetails.constants' import { generateCLICommands } from './EdgeFunctionDetails.utils' -import { PageContainer } from 'components/ui/PageContainer' -import { PageSection } from 'components/ui/PageSection' +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageSection } from 'ui-patterns/PageSection' const FormSchema = z.object({ name: z.string().min(0, 'Name is required'), diff --git a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx index bfe92b0dcb1..c7181c85f2c 100644 --- a/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx +++ b/apps/studio/components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout.tsx @@ -9,7 +9,7 @@ import { BlobReader, BlobWriter, ZipWriter } from '@zip.js/zip.js' import { useParams } from 'common' import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext' import { EdgeFunctionTesterSheet } from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionTesterSheet' -import { PageHeader } from 'components/ui/PageHeader' +import { PageHeader } from 'ui-patterns/PageHeader' import { APIDocsButton } from 'components/ui/APIDocsButton' import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' diff --git a/apps/studio/pages/project/[ref]/auth/providers.tsx b/apps/studio/pages/project/[ref]/auth/providers.tsx index 30d73f57ce3..ac9e13311f7 100644 --- a/apps/studio/pages/project/[ref]/auth/providers.tsx +++ b/apps/studio/pages/project/[ref]/auth/providers.tsx @@ -7,8 +7,8 @@ import { AuthProvidersForm } from 'components/interfaces/Auth/AuthProvidersForm' import { BasicAuthSettingsForm } from 'components/interfaces/Auth/BasicAuthSettingsForm' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' -import { PageContainer } from 'components/ui/PageContainer' -import { PageHeader } from 'components/ui/PageHeader' +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageHeader } from 'ui-patterns/PageHeader' import { NavMenu, NavMenuItem } from 'ui' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { UnknownInterface } from 'components/ui/UnknownInterface' diff --git a/apps/studio/pages/project/[ref]/auth/third-party.tsx b/apps/studio/pages/project/[ref]/auth/third-party.tsx index 50bd036090f..0096b861f6a 100644 --- a/apps/studio/pages/project/[ref]/auth/third-party.tsx +++ b/apps/studio/pages/project/[ref]/auth/third-party.tsx @@ -8,8 +8,8 @@ import { useParams } from 'common' import { ThirdPartyAuthForm } from 'components/interfaces/Auth/ThirdPartyAuthForm' import AuthLayout from 'components/layouts/AuthLayout/AuthLayout' import DefaultLayout from 'components/layouts/DefaultLayout' -import { PageContainer } from 'components/ui/PageContainer' -import { PageHeader } from 'components/ui/PageHeader' +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageHeader } from 'ui-patterns/PageHeader' import { NavMenu, NavMenuItem } from 'ui' import NoPermission from 'components/ui/NoPermission' import { UnknownInterface } from 'components/ui/UnknownInterface' diff --git a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx index d5de0fe2f1b..40e917b6077 100644 --- a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx @@ -8,7 +8,7 @@ import DatabaseBackupsNav from 'components/interfaces/Database/Backups/DatabaseB import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' -import { PageHeader } from 'components/ui/PageHeader' +import { PageHeader } from 'ui-patterns/PageHeader' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import InformationBox from 'components/ui/InformationBox' diff --git a/apps/studio/pages/project/[ref]/database/functions.tsx b/apps/studio/pages/project/[ref]/database/functions.tsx index 65faf633ad9..3fcb201ab13 100644 --- a/apps/studio/pages/project/[ref]/database/functions.tsx +++ b/apps/studio/pages/project/[ref]/database/functions.tsx @@ -9,8 +9,8 @@ import FunctionsList from 'components/interfaces/Database/Functions/FunctionsLis import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout' import DefaultLayout from 'components/layouts/DefaultLayout' import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' -import { PageContainer } from 'components/ui/PageContainer' -import { PageHeader } from 'components/ui/PageHeader' +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageHeader } from 'ui-patterns/PageHeader' import { DocsButton } from 'components/ui/DocsButton' import NoPermission from 'components/ui/NoPermission' import { DatabaseFunction } from 'data/database-functions/database-functions-query' @@ -19,7 +19,7 @@ import { DOCS_URL } from 'lib/constants' import { useEditorPanelStateSnapshot } from 'state/editor-panel-state' import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' import type { NextPageWithLayout } from 'types' -import { PageSection } from 'components/ui/PageSection' +import { PageSection } from 'ui-patterns/PageSection' const DatabaseFunctionsPage: NextPageWithLayout = () => { const [selectedFunction, setSelectedFunction] = useState() diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx index d22c0aabd4c..484bb196161 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/details.tsx @@ -1,8 +1,8 @@ import { EdgeFunctionDetails } from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionDetails' import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout' -import { PageContainer } from 'components/ui/PageContainer' -import { PageSection } from 'components/ui/PageSection' +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageSection } from 'ui-patterns/PageSection' import type { NextPageWithLayout } from 'types' const PageLayout: NextPageWithLayout = () => diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx index 581200ddec7..502a9d489d8 100644 --- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx @@ -11,7 +11,7 @@ import { useFlag } from 'common' import ReportWidget from 'components/interfaces/Reports/ReportWidget' import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout' -import { PageContainer } from 'components/ui/PageContainer' +import { PageContainer } from 'ui-patterns/PageContainer' import AreaChart from 'components/ui/Charts/AreaChart' import StackedBarChart from 'components/ui/Charts/StackedBarChart' import NoPermission from 'components/ui/NoPermission' @@ -30,7 +30,7 @@ import { Button, WarningIcon, } from 'ui' -import { PageSection } from 'components/ui/PageSection' +import { PageSection } from 'ui-patterns/PageSection' const CHART_INTERVALS: ChartIntervals[] = [ { @@ -135,7 +135,7 @@ const PageLayout: NextPageWithLayout = () => { return ( -
+
{CHART_INTERVALS.map((item, i) => { const classes = [] diff --git a/apps/studio/pages/project/[ref]/functions/index.tsx b/apps/studio/pages/project/[ref]/functions/index.tsx index 39532bca581..4128e4d79e0 100644 --- a/apps/studio/pages/project/[ref]/functions/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/index.tsx @@ -10,8 +10,8 @@ import { } from 'components/interfaces/Functions/FunctionsEmptyState' import DefaultLayout from 'components/layouts/DefaultLayout' import EdgeFunctionsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout' -import { PageContainer } from 'components/ui/PageContainer' -import { PageHeader } from 'components/ui/PageHeader' +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageHeader } from 'ui-patterns/PageHeader' import AlertError from 'components/ui/AlertError' import { DocsButton } from 'components/ui/DocsButton' import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader' @@ -19,7 +19,7 @@ import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query' import { DOCS_URL, IS_PLATFORM } from 'lib/constants' import type { NextPageWithLayout } from 'types' import { Button, Card, Table, TableBody, TableHead, TableHeader, TableRow } from 'ui' -import { PageSection } from 'components/ui/PageSection' +import { PageSection } from 'ui-patterns/PageSection' const EdgeFunctionsPage: NextPageWithLayout = () => { const { ref } = useParams() diff --git a/packages/ui-patterns/index.tsx b/packages/ui-patterns/index.tsx index f33a01ebe8d..6d6b600d4a3 100644 --- a/packages/ui-patterns/index.tsx +++ b/packages/ui-patterns/index.tsx @@ -12,6 +12,9 @@ export * from './src/FilterBar' export * from './src/GlassPanel' export * from './src/InnerSideMenu' export * from './src/McpUrlBuilder' +export * from './src/PageContainer' +export * from './src/PageHeader' +export * from './src/PageSection' export * from './src/PopupFrame' export * from './src/ShimmeringLoader' export * from './src/TimestampInfo' diff --git a/packages/ui-patterns/package.json b/packages/ui-patterns/package.json index 7d4cc23fdf2..3db1341fde0 100644 --- a/packages/ui-patterns/package.json +++ b/packages/ui-patterns/package.json @@ -318,6 +318,18 @@ "import": "./src/McpUrlBuilder/index.ts", "types": "./src/McpUrlBuilder/index.ts" }, + "./PageContainer": { + "import": "./src/PageContainer/index.tsx", + "types": "./src/PageContainer/index.tsx" + }, + "./PageHeader": { + "import": "./src/PageHeader/index.tsx", + "types": "./src/PageHeader/index.tsx" + }, + "./PageSection": { + "import": "./src/PageSection/index.tsx", + "types": "./src/PageSection/index.tsx" + }, "./MobileSheetNav/MobileSheetNav": { "import": "./src/MobileSheetNav/MobileSheetNav.tsx", "types": "./src/MobileSheetNav/MobileSheetNav.tsx" diff --git a/apps/studio/components/ui/PageContainer/index.tsx b/packages/ui-patterns/src/PageContainer/index.tsx similarity index 99% rename from apps/studio/components/ui/PageContainer/index.tsx rename to packages/ui-patterns/src/PageContainer/index.tsx index b6c40f0a9fe..37a2a086337 100644 --- a/apps/studio/components/ui/PageContainer/index.tsx +++ b/packages/ui-patterns/src/PageContainer/index.tsx @@ -51,3 +51,4 @@ export const PageContainer = forwardRef( ) PageContainer.displayName = 'PageContainer' + diff --git a/apps/studio/components/ui/PageHeader/index.tsx b/packages/ui-patterns/src/PageHeader/index.tsx similarity index 100% rename from apps/studio/components/ui/PageHeader/index.tsx rename to packages/ui-patterns/src/PageHeader/index.tsx diff --git a/apps/studio/components/ui/PageSection/index.tsx b/packages/ui-patterns/src/PageSection/index.tsx similarity index 99% rename from apps/studio/components/ui/PageSection/index.tsx rename to packages/ui-patterns/src/PageSection/index.tsx index 15691ca4417..576b19d7039 100644 --- a/apps/studio/components/ui/PageSection/index.tsx +++ b/packages/ui-patterns/src/PageSection/index.tsx @@ -217,3 +217,4 @@ export const PageSection = Object.assign(PageSectionRoot, { Aside: PageSectionAside, Content: PageSectionContent, }) + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c3f383b382..2a383421c13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -320,6 +320,9 @@ importers: '@shikijs/compat': specifier: ^1.1.7 version: 1.29.2 + '@tailwindcss/container-queries': + specifier: ^0.1.1 + version: 0.1.1(tailwindcss@3.4.1) '@types/lodash.template': specifier: 4.5.0 version: 4.5.0