Layout

Guidelines to create consistent layouts across Studio pages using a set of page components.

Build every Studio page with PageContainer, optional page chrome (Page Breadcrumbs, Page Nav), PageHeader when you need a title block, and PageSection. These components handle width, spacing, and content structure so pages feel consistent across the app.

Core rules

  1. Breadcrumbs first. Every page starts with a bordered breadcrumb row (PageBreadcrumbs) at the top. Place it as a sibling above PageHeader or page content — not inside PageHeader.
  2. Sub navigation below breadcrumbs. When a page has tabs or section links, place PageNav directly under the breadcrumb row, aligned top left. Also a sibling — not inside PageHeader.
  3. Parent pages with sub navigation stay compact.

    The parent omits PageHeader (no title or description). Breadcrumbs name the parent; child routes render their own PageHeader with meta.
  4. Pick width by content, not page type.
WidthUse when
smallSettings, forms, and focused configuration (including child pages under a settings parent)
defaultLists, tables, and detail pages that stay readable without full viewport width
fullDense horizontal content: logs, code, editors, charts, or tables that need the viewport

A route can mix widths across child pages (for example, a full-width parent with a small settings tab and a full logs tab).

  1. Page header meta is optional. Add PageHeader with PageHeaderMeta (icon, title, description, aside) only when that context helps the user. Skip it when the work area is self-explanatory (for example, logs with filters and a table).
  2. Put actions where the user is already looking.

SituationWhere actions go
Parent with sub navigationPageBreadcrumbsActions on the breadcrumb row
Child page with PageHeaderMeta and no filter rowPageHeaderAside
Table or list with a filter/search rowRight side of that row (not header aside)
Simple list with no filter rowPageHeaderAside or PageSectionAside
Compact chrome, no meta (for example logs)Breadcrumb row or in-page controls (filter bar, toolbar)
  1. Section titles for multiple sections. When a page has no PageHeader title and the content is split into multiple PageSections, use PageSectionTitle and PageSectionDescription to label each section (see Page Section).

Patterns

Settings

Single-column configuration. Use size="small" or size="default" for both header and container. Group fields with PageSection.

Loading...
import { zodResolver } from '@hookform/resolvers/zod'
import Link from 'next/link'
import { useForm } from 'react-hook-form'
import {
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
  Button,
  Card,
  CardContent,
  CardFooter,
  Form,
  FormControl,
  FormField,
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupText,
  Switch,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { PageBreadcrumbs } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
  PageHeader,
  PageHeaderDescription,
  PageHeaderMeta,
  PageHeaderSummary,
  PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import {
  PageSection,
  PageSectionContent,
  PageSectionDescription,
  PageSectionMeta,
  PageSectionSummary,
  PageSectionTitle,
} from 'ui-patterns/PageSection'
import * as z from 'zod'
 
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 function PageLayoutSettings() {
  const refreshTokenForm = useForm<z.infer<typeof RefreshTokenSchema>>({
    resolver: zodResolver(RefreshTokenSchema),
    defaultValues: {
      REFRESH_TOKEN_ROTATION_ENABLED: false,
      SECURITY_REFRESH_TOKEN_REUSE_INTERVAL: 10,
    },
  })
 
  const userSessionsForm = useForm<z.infer<typeof UserSessionsSchema>>({
    resolver: zodResolver(UserSessionsSchema),
    defaultValues: {
      SESSIONS_TIMEBOX: 0,
      SESSIONS_INACTIVITY_TIMEOUT: 0,
      SESSIONS_SINGLE_PER_USER: false,
    },
  })
 
  return (
    <div className="w-full">
      <PageBreadcrumbs>
        <BreadcrumbList>
          <BreadcrumbItem>
            <BreadcrumbLink asChild>
              <Link href="/project/demo/auth">Authentication</Link>
            </BreadcrumbLink>
          </BreadcrumbItem>
          <BreadcrumbSeparator />
          <BreadcrumbItem>
            <BreadcrumbPage>User Sessions</BreadcrumbPage>
          </BreadcrumbItem>
        </BreadcrumbList>
      </PageBreadcrumbs>
 
      <PageHeader size="default">
        <PageHeaderMeta>
          <PageHeaderSummary>
            <PageHeaderTitle>User Sessions</PageHeaderTitle>
            <PageHeaderDescription>
              Configure settings for user sessions and refresh tokens
            </PageHeaderDescription>
          </PageHeaderSummary>
        </PageHeaderMeta>
      </PageHeader>
 
      <PageContainer size="default">
        <PageSection>
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>Refresh Tokens</PageSectionTitle>
              <PageSectionDescription>
                Configure refresh token rotation and security settings.
              </PageSectionDescription>
            </PageSectionSummary>
          </PageSectionMeta>
          <PageSectionContent>
            <Form {...refreshTokenForm}>
              <form className="space-y-4">
                <Card>
                  <CardContent className="pt-6">
                    <FormField
                      control={refreshTokenForm.control}
                      name="REFRESH_TOKEN_ROTATION_ENABLED"
                      render={({ field }) => (
                        <FormItemLayout
                          layout="flex-row-reverse"
                          label="Detect and revoke potentially compromised refresh tokens"
                          description="Prevent replay attacks from potentially compromised refresh tokens."
                        >
                          <FormControl>
                            <Switch checked={field.value} onCheckedChange={field.onChange} />
                          </FormControl>
                        </FormItemLayout>
                      )}
                    />
                  </CardContent>
                  <CardContent>
                    <FormField
                      control={refreshTokenForm.control}
                      name="SECURITY_REFRESH_TOKEN_REUSE_INTERVAL"
                      render={({ field }) => (
                        <FormItemLayout
                          layout="flex-row-reverse"
                          label="Refresh token reuse interval"
                          description="Time interval where the same refresh token can be used multiple times to request for an access token. Recommendation: 10 seconds."
                        >
                          <FormControl>
                            <InputGroup>
                              <InputGroupAddon align="inline-end">
                                <InputGroupText>seconds</InputGroupText>
                              </InputGroupAddon>
                              <InputGroupInput type="number" min={0} {...field} />
                            </InputGroup>
                          </FormControl>
                        </FormItemLayout>
                      )}
                    />
                  </CardContent>
                  <CardFooter className="justify-end space-x-2">
                    {refreshTokenForm.formState.isDirty && (
                      <Button type="default" onClick={() => refreshTokenForm.reset()}>
                        Cancel
                      </Button>
                    )}
                    <Button
                      type="primary"
                      htmlType="submit"
                      disabled={!refreshTokenForm.formState.isDirty}
                    >
                      Save changes
                    </Button>
                  </CardFooter>
                </Card>
              </form>
            </Form>
          </PageSectionContent>
        </PageSection>
 
        <PageSection>
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>User Sessions</PageSectionTitle>
              <PageSectionDescription>
                Configure session timeout and single session enforcement settings.
              </PageSectionDescription>
            </PageSectionSummary>
          </PageSectionMeta>
          <PageSectionContent>
            <Form {...userSessionsForm}>
              <form className="space-y-4">
                <Card>
                  <CardContent>
                    <FormField
                      control={userSessionsForm.control}
                      name="SESSIONS_SINGLE_PER_USER"
                      render={({ field }) => (
                        <FormItemLayout
                          layout="flex-row-reverse"
                          label="Enforce single session per user"
                          description="If enabled, all but a user's most recently active session will be terminated."
                        >
                          <FormControl>
                            <Switch checked={field.value} onCheckedChange={field.onChange} />
                          </FormControl>
                        </FormItemLayout>
                      )}
                    />
                  </CardContent>
 
                  <CardContent>
                    <FormField
                      control={userSessionsForm.control}
                      name="SESSIONS_TIMEBOX"
                      render={({ field }) => (
                        <FormItemLayout
                          layout="flex-row-reverse"
                          label="Time-box user sessions"
                          description="The amount of time before a user is forced to sign in again. Use 0 for never."
                        >
                          <div className="flex items-center">
                            <FormControl>
                              <InputGroup>
                                <InputGroupAddon align="inline-end">
                                  <InputGroupText>
                                    <HoursOrNeverText value={field.value || 0} />
                                  </InputGroupText>
                                </InputGroupAddon>
                                <InputGroupInput type="number" min={0} {...field} />
                              </InputGroup>
                            </FormControl>
                          </div>
                        </FormItemLayout>
                      )}
                    />
                  </CardContent>
 
                  <CardContent>
                    <FormField
                      control={userSessionsForm.control}
                      name="SESSIONS_INACTIVITY_TIMEOUT"
                      render={({ field }) => (
                        <FormItemLayout
                          layout="flex-row-reverse"
                          label="Inactivity timeout"
                          description="The amount of time a user needs to be inactive to be forced to sign in again. Use 0 for never."
                        >
                          <div className="flex items-center">
                            <FormControl>
                              <InputGroup>
                                <InputGroupAddon align="inline-end">
                                  <InputGroupText>
                                    <HoursOrNeverText value={field.value || 0} />
                                  </InputGroupText>
                                </InputGroupAddon>
                                <InputGroupInput type="number" {...field} />
                              </InputGroup>
                            </FormControl>
                          </div>
                        </FormItemLayout>
                      )}
                    />
                  </CardContent>
 
                  <CardFooter className="justify-end space-x-2">
                    {userSessionsForm.formState.isDirty && (
                      <Button type="default" onClick={() => userSessionsForm.reset()}>
                        Cancel
                      </Button>
                    )}
                    <Button
                      type="primary"
                      htmlType="submit"
                      disabled={!userSessionsForm.formState.isDirty}
                    >
                      Save changes
                    </Button>
                  </CardFooter>
                </Card>
              </form>
            </Form>
          </PageSectionContent>
        </PageSection>
      </PageContainer>
    </div>
  )
}

Page sub navigation sits below page breadcrumbs.

Loading...
'use client'
 
import { zodResolver } from '@hookform/resolvers/zod'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import {
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
  Button,
  Card,
  CardContent,
  CardFooter,
  Form,
  FormControl,
  FormField,
  Input,
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  InputGroupText,
  NavMenu,
  NavMenuItem,
  Switch,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { PageBreadcrumbs } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
  PageHeader,
  PageHeaderDescription,
  PageHeaderMeta,
  PageHeaderSummary,
  PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { PageNav } from 'ui-patterns/PageNav'
import {
  PageSection,
  PageSectionContent,
  PageSectionMeta,
  PageSectionSummary,
  PageSectionTitle,
} from 'ui-patterns/PageSection'
import * as z from 'zod'
 
type SubPageId = 'templates' | 'smtp'
 
const subPages: { id: SubPageId; label: string }[] = [
  { id: 'templates', label: 'Templates' },
  { id: 'smtp', label: 'SMTP Settings' },
]
 
const AUTHENTICATION_TEMPLATES = [
  {
    title: 'Confirm signup',
    purpose: 'Ask users to confirm their email address after signing up',
  },
  {
    title: 'Invite user',
    purpose: 'Invite users who do not yet have an account to join your application',
  },
  {
    title: 'Magic link',
    purpose: 'Allow users to sign in via a one-time link sent to their email',
  },
  {
    title: 'Change email address',
    purpose: 'Ask users to verify their new email address after changing it',
  },
  {
    title: 'Reset password',
    purpose: 'Allow users to reset their password if they forget it',
  },
]
 
const SECURITY_TEMPLATES = [
  {
    id: 'PASSWORD_CHANGED',
    title: 'Password changed',
    purpose: 'Notify users when their password has been changed',
    enabled: true,
  },
  {
    id: 'EMAIL_CHANGED',
    title: 'Email address changed',
    purpose: 'Notify users when their email address has been changed',
    enabled: true,
  },
  {
    id: 'PHONE_CHANGED',
    title: 'Phone number changed',
    purpose: 'Notify users when their phone number has been changed',
    enabled: false,
  },
]
 
const NotificationsFormSchema = z.object({
  MAILER_NOTIFICATIONS_PASSWORD_CHANGED_ENABLED: z.boolean(),
  MAILER_NOTIFICATIONS_EMAIL_CHANGED_ENABLED: z.boolean(),
  MAILER_NOTIFICATIONS_PHONE_CHANGED_ENABLED: z.boolean(),
})
 
const SmtpFormSchema = z.object({
  ENABLE_SMTP: z.boolean(),
  SMTP_ADMIN_EMAIL: z.string().optional(),
  SMTP_SENDER_NAME: z.string().optional(),
  SMTP_HOST: z.string().optional(),
  SMTP_PORT: z.coerce.number().optional(),
  SMTP_MAX_FREQUENCY: z.coerce.number().optional(),
  SMTP_USER: z.string().optional(),
})
 
export function PageLayoutAuthEmails() {
  const [activePage, setActivePage] = useState<SubPageId>('templates')
 
  return (
    <div className="w-full">
      <PageBreadcrumbs>
        <BreadcrumbList>
          <BreadcrumbItem>
            <BreadcrumbLink asChild>
              <Link href="/project/demo/auth">Authentication</Link>
            </BreadcrumbLink>
          </BreadcrumbItem>
          <BreadcrumbSeparator />
          <BreadcrumbItem>
            <BreadcrumbPage>Emails</BreadcrumbPage>
          </BreadcrumbItem>
        </BreadcrumbList>
      </PageBreadcrumbs>
 
      <PageNav>
        <NavMenu>
          {subPages.map((page) => (
            <NavMenuItem key={page.id} active={activePage === page.id}>
              <button
                type="button"
                aria-pressed={activePage === page.id}
                className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
                onClick={() => setActivePage(page.id)}
              >
                {page.label}
              </button>
            </NavMenuItem>
          ))}
        </NavMenu>
      </PageNav>
 
      {activePage === 'templates' && (
        <TemplatesPage onNavigateToSmtp={() => setActivePage('smtp')} />
      )}
      {activePage === 'smtp' && <SmtpPage />}
    </div>
  )
}
 
function TemplatesPage({ onNavigateToSmtp }: { onNavigateToSmtp: () => void }) {
  const notificationsForm = useForm<z.infer<typeof NotificationsFormSchema>>({
    resolver: zodResolver(NotificationsFormSchema),
    defaultValues: {
      MAILER_NOTIFICATIONS_PASSWORD_CHANGED_ENABLED: true,
      MAILER_NOTIFICATIONS_EMAIL_CHANGED_ENABLED: true,
      MAILER_NOTIFICATIONS_PHONE_CHANGED_ENABLED: false,
    },
  })
 
  const handleSave = (values: z.infer<typeof NotificationsFormSchema>) => {
    notificationsForm.reset(values)
    notificationsForm.clearErrors()
  }
 
  return (
    <>
      <PageHeader size="small">
        <PageHeaderMeta>
          <PageHeaderSummary>
            <PageHeaderTitle>Templates</PageHeaderTitle>
            <PageHeaderDescription>
              Configure what emails your users receive and how they are sent
            </PageHeaderDescription>
          </PageHeaderSummary>
        </PageHeaderMeta>
      </PageHeader>
 
      <PageContainer size="small" className="pb-16">
        <PageSection>
          <Admonition
            type="warning"
            title="Set up custom SMTP"
            description="You're using the built-in email service. This service has rate limits and is not meant to be used for production apps."
            layout="horizontal"
            className="mb-4"
            actions={
              <Button type="default" size="tiny" onClick={onNavigateToSmtp}>
                Set up SMTP
              </Button>
            }
          />
 
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>Authentication</PageSectionTitle>
            </PageSectionSummary>
          </PageSectionMeta>
          <PageSectionContent>
            <Card>
              {AUTHENTICATION_TEMPLATES.map((template) => (
                <CardContent key={template.title} className="p-0">
                  <button
                    type="button"
                    className="flex w-full items-center justify-between px-6 py-4 text-left transition-colors hover:bg-surface-200"
                  >
                    <div className="flex flex-col">
                      <h3 className="text-sm text-foreground">{template.title}</h3>
                      <p className="text-sm text-foreground-lighter">{template.purpose}</p>
                    </div>
                    <ChevronRight size={16} className="text-foreground-muted" />
                  </button>
                </CardContent>
              ))}
            </Card>
          </PageSectionContent>
        </PageSection>
 
        <PageSection>
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>Security</PageSectionTitle>
            </PageSectionSummary>
          </PageSectionMeta>
          <PageSectionContent>
            <Form {...notificationsForm}>
              <form className="space-y-4" onSubmit={notificationsForm.handleSubmit(handleSave)}>
                <Card>
                  {SECURITY_TEMPLATES.map((template) => {
                    const fieldName =
                      `MAILER_NOTIFICATIONS_${template.id}_ENABLED` as keyof z.infer<
                        typeof NotificationsFormSchema
                      >
 
                    return (
                      <CardContent
                        key={template.id}
                        className="flex h-full w-full items-center justify-between p-0 transition-colors hover:bg-surface-200"
                      >
                        <button type="button" className="flex flex-1 flex-col px-6 py-4 text-left">
                          <h3 className="text-sm text-foreground">{template.title}</h3>
                          <p className="text-sm text-foreground-lighter">{template.purpose}</p>
                        </button>
 
                        <div className="relative flex h-full items-center gap-4 pl-2">
                          <FormField
                            control={notificationsForm.control}
                            name={fieldName}
                            render={({ field }) => (
                              <FormControl>
                                <Switch checked={field.value} onCheckedChange={field.onChange} />
                              </FormControl>
                            )}
                          />
                          <button type="button" className="py-6 pr-6" aria-label="Edit template">
                            <ChevronRight size={16} className="text-foreground-muted" />
                          </button>
                        </div>
                      </CardContent>
                    )
                  })}
                  <CardFooter className="justify-end space-x-2">
                    {notificationsForm.formState.isDirty && (
                      <Button type="default" onClick={() => notificationsForm.reset()}>
                        Cancel
                      </Button>
                    )}
                    <Button
                      type="primary"
                      htmlType="submit"
                      disabled={!notificationsForm.formState.isDirty}
                    >
                      Save changes
                    </Button>
                  </CardFooter>
                </Card>
              </form>
            </Form>
          </PageSectionContent>
        </PageSection>
      </PageContainer>
    </>
  )
}
 
function SmtpPage() {
  const form = useForm<z.infer<typeof SmtpFormSchema>>({
    resolver: zodResolver(SmtpFormSchema),
    defaultValues: {
      ENABLE_SMTP: false,
      SMTP_ADMIN_EMAIL: '',
      SMTP_SENDER_NAME: '',
      SMTP_HOST: '',
      SMTP_PORT: undefined,
      SMTP_MAX_FREQUENCY: 60,
      SMTP_USER: '',
    },
  })
  const enableSmtp = form.watch('ENABLE_SMTP')
 
  return (
    <>
      <PageHeader size="small">
        <PageHeaderMeta>
          <PageHeaderSummary>
            <PageHeaderTitle>SMTP Settings</PageHeaderTitle>
            <PageHeaderDescription>
              Configure a custom SMTP provider for sending authentication and security emails
            </PageHeaderDescription>
          </PageHeaderSummary>
        </PageHeaderMeta>
      </PageHeader>
 
      <PageContainer size="small">
        <PageSection>
          <PageSectionContent>
            <Form {...form}>
              <form className="space-y-4" onSubmit={form.handleSubmit(() => undefined)}>
                <Card>
                  <CardContent>
                    <FormField
                      control={form.control}
                      name="ENABLE_SMTP"
                      render={({ field }) => (
                        <FormItemLayout
                          layout="flex-row-reverse"
                          label="Enable custom SMTP"
                          description="Emails will be sent using your custom SMTP provider. Email rate limits can be adjusted in rate limits settings."
                        >
                          <FormControl>
                            <Switch checked={field.value} onCheckedChange={field.onChange} />
                          </FormControl>
                        </FormItemLayout>
                      )}
                    />
 
                    {enableSmtp && (
                      <Admonition
                        type="warning"
                        title="All fields must be filled"
                        description="Each of the fields below must be filled before custom SMTP can be enabled."
                        className="mt-4 border-warning-400 bg-warning-200"
                      />
                    )}
                  </CardContent>
 
                  {enableSmtp && (
                    <>
                      <CardContent className="py-6">
                        <div className="grid grid-cols-12 gap-6">
                          <div className="col-span-4">
                            <h3 className="mb-1 text-sm">Sender details</h3>
                            <p className="text-balance text-sm text-foreground-lighter">
                              Configure the sender information for your emails.
                            </p>
                          </div>
                          <div className="col-span-8 space-y-4">
                            <FormField
                              control={form.control}
                              name="SMTP_ADMIN_EMAIL"
                              render={({ field }) => (
                                <FormItemLayout
                                  label="Sender email address"
                                  description="The email address the emails are sent from."
                                >
                                  <FormControl>
                                    <Input {...field} placeholder="noreply@yourdomain.com" />
                                  </FormControl>
                                </FormItemLayout>
                              )}
                            />
                            <FormField
                              control={form.control}
                              name="SMTP_SENDER_NAME"
                              render={({ field }) => (
                                <FormItemLayout
                                  label="Sender name"
                                  description="Name displayed in the recipient's inbox."
                                >
                                  <FormControl>
                                    <Input {...field} placeholder="Your App" />
                                  </FormControl>
                                </FormItemLayout>
                              )}
                            />
                          </div>
                        </div>
                      </CardContent>
 
                      <CardContent className="py-6">
                        <div className="grid grid-cols-12 gap-6">
                          <div className="col-span-4">
                            <h3 className="mb-1 text-sm">SMTP provider settings</h3>
                            <p className="text-balance text-sm text-foreground-lighter">
                              Your SMTP credentials will always be encrypted in our database.
                            </p>
                          </div>
                          <div className="col-span-8 space-y-4">
                            <FormField
                              control={form.control}
                              name="SMTP_HOST"
                              render={({ field }) => (
                                <FormItemLayout
                                  label="Host"
                                  description="Hostname or IP address of your SMTP server."
                                >
                                  <FormControl>
                                    <Input {...field} placeholder="smtp.yourdomain.com" />
                                  </FormControl>
                                </FormItemLayout>
                              )}
                            />
                            <FormField
                              control={form.control}
                              name="SMTP_PORT"
                              render={({ field }) => (
                                <FormItemLayout
                                  label="Port number"
                                  description="Port used by your SMTP server. Common ports include 465 and 587."
                                >
                                  <FormControl>
                                    <Input
                                      type="number"
                                      value={field.value ?? ''}
                                      onChange={(event) => field.onChange(event.target.value)}
                                      placeholder="587"
                                    />
                                  </FormControl>
                                </FormItemLayout>
                              )}
                            />
                            <FormField
                              control={form.control}
                              name="SMTP_MAX_FREQUENCY"
                              render={({ field }) => (
                                <FormItemLayout
                                  label="Minimum interval per user"
                                  description="The minimum time in seconds between emails before another email can be sent to the same user."
                                >
                                  <FormControl>
                                    <InputGroup>
                                      <InputGroupInput
                                        type="number"
                                        value={field.value ?? ''}
                                        onChange={(event) => field.onChange(event.target.value)}
                                      />
                                      <InputGroupAddon align="inline-end">
                                        <InputGroupText>seconds</InputGroupText>
                                      </InputGroupAddon>
                                    </InputGroup>
                                  </FormControl>
                                </FormItemLayout>
                              )}
                            />
                            <FormField
                              control={form.control}
                              name="SMTP_USER"
                              render={({ field }) => (
                                <FormItemLayout
                                  label="Username"
                                  description="Username for your SMTP server."
                                >
                                  <FormControl>
                                    <Input {...field} placeholder="SMTP Username" />
                                  </FormControl>
                                </FormItemLayout>
                              )}
                            />
                          </div>
                        </div>
                      </CardContent>
                    </>
                  )}
 
                  <CardFooter className="justify-end space-x-2">
                    {form.formState.isDirty && (
                      <Button type="default" onClick={() => form.reset()}>
                        Cancel
                      </Button>
                    )}
                    <Button type="primary" htmlType="submit" disabled={!form.formState.isDirty}>
                      Save changes
                    </Button>
                  </CardFooter>
                </Card>
              </form>
            </Form>
          </PageSectionContent>
        </PageSection>
      </PageContainer>
    </>
  )
}

List

Collections of objects (tables, triggers, functions). Default to size="default"; use full when columns need the width.

Loading...
import { Search } from 'lucide-react'
import Link from 'next/link'
import React from 'react'
import {
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
  Button,
  Card,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from 'ui'
import { PageBreadcrumbs } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
  PageHeader,
  PageHeaderDescription,
  PageHeaderMeta,
  PageHeaderSummary,
  PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
 
export 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 (
    <div className="w-full">
      <PageBreadcrumbs>
        <BreadcrumbList>
          <BreadcrumbItem>
            <BreadcrumbLink asChild>
              <Link href="/project/demo/database">Database</Link>
            </BreadcrumbLink>
          </BreadcrumbItem>
          <BreadcrumbSeparator />
          <BreadcrumbItem>
            <BreadcrumbPage>Functions</BreadcrumbPage>
          </BreadcrumbItem>
        </BreadcrumbList>
      </PageBreadcrumbs>
 
      <PageHeader size="default">
        <PageHeaderMeta>
          <PageHeaderSummary>
            <PageHeaderTitle>Database Functions</PageHeaderTitle>
            <PageHeaderDescription>Manage your database functions</PageHeaderDescription>
          </PageHeaderSummary>
        </PageHeaderMeta>
      </PageHeader>
 
      <PageContainer size="default">
        <PageSection>
          <PageSectionContent>
            <div className="w-full space-y-4">
              <div className="flex flex-col lg:flex-row lg:items-center justify-between gap-2 flex-wrap">
                <div className="flex flex-col lg:flex-row lg:items-center gap-2">
                  <InputGroup>
                    <InputGroupInput
                      placeholder="Search for a function"
                      size="tiny"
                      className="w-full lg:w-52"
                    />
                    <InputGroupAddon>
                      <Search />
                    </InputGroupAddon>
                  </InputGroup>
                </div>
                <Button type="primary">Create a new function</Button>
              </div>
 
              <Card>
                <Table className="table-fixed overflow-x-auto">
                  <TableHeader>
                    <TableRow>
                      <TableHead>Name</TableHead>
                      <TableHead className="table-cell">Arguments</TableHead>
                      <TableHead className="table-cell">Return type</TableHead>
                      <TableHead className="table-cell w-[100px]">Security</TableHead>
                      <TableHead className="w-1/6"></TableHead>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {functions.map((fn) => (
                      <TableRow key={fn.id}>
                        <TableCell className="font-medium">{fn.name}</TableCell>
                        <TableCell>{fn.arguments}</TableCell>
                        <TableCell>{fn.return_type}</TableCell>
                        <TableCell>{fn.security}</TableCell>
                        <TableCell>
                          <DropdownMenu>
                            <DropdownMenuTrigger asChild>
                              <Button type="text" size="small">
                                Actions
                              </Button>
                            </DropdownMenuTrigger>
                            <DropdownMenuContent align="end">
                              <DropdownMenuItem>Edit</DropdownMenuItem>
                              <DropdownMenuItem>Duplicate</DropdownMenuItem>
                              <DropdownMenuSeparator />
                              <DropdownMenuItem className="text-destructive">
                                Delete
                              </DropdownMenuItem>
                            </DropdownMenuContent>
                          </DropdownMenu>
                        </TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                </Table>
              </Card>
            </div>
          </PageSectionContent>
        </PageSection>
      </PageContainer>
    </div>
  )
}
Loading...
import Link from 'next/link'
import {
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
  Button,
  Card,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from 'ui'
import { PageBreadcrumbs } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
  PageHeader,
  PageHeaderAside,
  PageHeaderDescription,
  PageHeaderMeta,
  PageHeaderSummary,
  PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
 
export 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 (
    <div className="w-full">
      <PageBreadcrumbs>
        <BreadcrumbList>
          <BreadcrumbItem>
            <BreadcrumbLink asChild>
              <Link href="/org/demo">Organization</Link>
            </BreadcrumbLink>
          </BreadcrumbItem>
          <BreadcrumbSeparator />
          <BreadcrumbItem>
            <BreadcrumbPage>Projects</BreadcrumbPage>
          </BreadcrumbItem>
        </BreadcrumbList>
      </PageBreadcrumbs>
 
      <PageHeader size="default">
        <PageHeaderMeta>
          <PageHeaderSummary>
            <PageHeaderTitle>Projects</PageHeaderTitle>
            <PageHeaderDescription>
              Manage and view all your projects in one place.
            </PageHeaderDescription>
          </PageHeaderSummary>
          <PageHeaderAside>
            <Button type="primary" size="small">
              Create Project
            </Button>
          </PageHeaderAside>
        </PageHeaderMeta>
      </PageHeader>
 
      <PageContainer size="default">
        <PageSection>
          <PageSectionContent>
            <Card>
              <Table>
                <TableHeader>
                  <TableRow>
                    <TableHead>Name</TableHead>
                    <TableHead>Status</TableHead>
                    <TableHead>Members</TableHead>
                    <TableHead className="text-right">Actions</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {items.map((item) => (
                    <TableRow key={item.id}>
                      <TableCell className="font-medium">{item.name}</TableCell>
                      <TableCell>{item.status}</TableCell>
                      <TableCell>{item.members}</TableCell>
                      <TableCell className="text-right">
                        <DropdownMenu>
                          <DropdownMenuTrigger asChild>
                            <Button type="text" size="small">
                              Actions
                            </Button>
                          </DropdownMenuTrigger>
                          <DropdownMenuContent align="end">
                            <DropdownMenuItem>Edit</DropdownMenuItem>
                            <DropdownMenuItem>Duplicate</DropdownMenuItem>
                            <DropdownMenuSeparator />
                            <DropdownMenuItem className="text-destructive">Delete</DropdownMenuItem>
                          </DropdownMenuContent>
                        </DropdownMenu>
                      </TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            </Card>
          </PageSectionContent>
        </PageSection>
      </PageContainer>
    </div>
  )
}

Detail

Dense content split into sections. Use size="default" and PageSection with orientation="horizontal" where a summary sits beside content.

Loading...
import Link from 'next/link'
import {
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
  Button,
  Card,
  CardContent,
} from 'ui'
import { PageBreadcrumbs } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
  PageHeader,
  PageHeaderDescription,
  PageHeaderMeta,
  PageHeaderSummary,
  PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import {
  PageSection,
  PageSectionContent,
  PageSectionDescription,
  PageSectionMeta,
  PageSectionSummary,
  PageSectionTitle,
} from 'ui-patterns/PageSection'
 
export function PageLayoutDetail() {
  return (
    <div className="w-full">
      <PageBreadcrumbs>
        <BreadcrumbList>
          <BreadcrumbItem>
            <BreadcrumbLink asChild>
              <Link href="/org/demo">Organization</Link>
            </BreadcrumbLink>
          </BreadcrumbItem>
          <BreadcrumbSeparator />
          <BreadcrumbItem>
            <BreadcrumbPage>Billing</BreadcrumbPage>
          </BreadcrumbItem>
        </BreadcrumbList>
      </PageBreadcrumbs>
 
      <PageHeader size="default">
        <PageHeaderMeta>
          <PageHeaderSummary>
            <PageHeaderTitle>Billing</PageHeaderTitle>
            <PageHeaderDescription>
              Manage your organization&apos;s billing and subscription settings.
            </PageHeaderDescription>
          </PageHeaderSummary>
        </PageHeaderMeta>
      </PageHeader>
 
      <PageContainer size="default">
        <PageSection orientation="horizontal">
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>Subscription</PageSectionTitle>
              <PageSectionDescription>
                View and manage your current subscription plan and billing cycle.
              </PageSectionDescription>
            </PageSectionSummary>
          </PageSectionMeta>
          <PageSectionContent>
            <Card>
              <CardContent className="p-6">
                <div className="space-y-4">
                  <div>
                    <p className="text-xs text-foreground-lighter mb-1">Current Plan</p>
                    <p className="text-sm font-medium">Pro Plan</p>
                  </div>
                  <div>
                    <p className="text-xs text-foreground-lighter mb-1">Billing Cycle</p>
                    <p className="text-sm">Monthly</p>
                  </div>
                  <div>
                    <p className="text-xs text-foreground-lighter mb-1">Next Billing Date</p>
                    <p className="text-sm">March 15, 2024</p>
                  </div>
                  <div className="pt-2">
                    <Button type="default" size="small">
                      Change Plan
                    </Button>
                  </div>
                </div>
              </CardContent>
            </Card>
          </PageSectionContent>
        </PageSection>
 
        <PageSection orientation="horizontal">
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>Cost control</PageSectionTitle>
              <PageSectionDescription>
                Set spending limits and alerts to manage your organization&apos;s costs.
              </PageSectionDescription>
            </PageSectionSummary>
          </PageSectionMeta>
          <PageSectionContent>
            <Card>
              <CardContent className="p-6">
                <div className="space-y-4">
                  <div>
                    <p className="text-xs text-foreground-lighter mb-1">Monthly Spending Limit</p>
                    <p className="text-sm">$500.00</p>
                  </div>
                  <div>
                    <p className="text-xs text-foreground-lighter mb-1">Current Month Spend</p>
                    <p className="text-sm">$234.50</p>
                  </div>
                  <div className="pt-2">
                    <Button type="default" size="small">
                      Configure Limits
                    </Button>
                  </div>
                </div>
              </CardContent>
            </Card>
          </PageSectionContent>
        </PageSection>
 
        <PageSection orientation="horizontal">
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>Payment Methods</PageSectionTitle>
              <PageSectionDescription>
                Manage payment methods and billing information for your organization.
              </PageSectionDescription>
            </PageSectionSummary>
          </PageSectionMeta>
          <PageSectionContent>
            <Card>
              <CardContent className="p-6">
                <div className="space-y-4">
                  <div>
                    <p className="text-xs text-foreground-lighter mb-1">Primary Payment Method</p>
                    <p className="text-sm">•••• •••• •••• 4242</p>
                  </div>
                  <div>
                    <p className="text-xs text-foreground-lighter mb-1">Expires</p>
                    <p className="text-sm">12/2025</p>
                  </div>
                  <div className="pt-2">
                    <Button type="default" size="small">
                      Update Payment Method
                    </Button>
                  </div>
                </div>
              </CardContent>
            </Card>
          </PageSectionContent>
        </PageSection>
      </PageContainer>
    </div>
  )
}

Full width

Logs, code, editors, and other dense views. Use size="full". Keep the top compact: breadcrumbs, optional breadcrumb-row actions, then sub navigation when needed. Omit PageHeader when there is no title block. Page-level actions live on the breadcrumb row or in the content (filter bar, toolbar).

Loading...
'use client'
 
import Link from 'next/link'
import {
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
  Button,
} from 'ui'
import { PageBreadcrumbs, PageBreadcrumbsActions } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
 
import { PageLayoutLogsContent } from './page-layout-logs-content'
 
export function PageLayoutFullWidth() {
  return (
    <div className="w-full">
      <PageBreadcrumbs
        actions={
          <PageBreadcrumbsActions>
            <Button type="default" size="tiny">
              Docs
            </Button>
          </PageBreadcrumbsActions>
        }
      >
        <BreadcrumbList>
          <BreadcrumbItem>
            <BreadcrumbLink asChild>
              <Link href="/project/demo">Project</Link>
            </BreadcrumbLink>
          </BreadcrumbItem>
          <BreadcrumbSeparator />
          <BreadcrumbItem>
            <BreadcrumbPage>Logs</BreadcrumbPage>
          </BreadcrumbItem>
        </BreadcrumbList>
      </PageBreadcrumbs>
 
      <PageContainer size="full" className="px-0 xl:px-0">
        <PageLayoutLogsContent />
      </PageContainer>
    </div>
  )
}

Parent with mixed child widths

Some parents are full width because a child needs it. The parent chrome stays compact (breadcrumbs, actions, sub navigation). Each child picks its own container width: overview charts in full, settings in small, logs and code in full without extra outer padding.

Loading...
'use client'
 
import { Check, Clock, CornerDownLeft, ExternalLink, File, Plus } from 'lucide-react'
import Link from 'next/link'
import { useCallback, useMemo, useState } from 'react'
import {
  Badge,
  BreadcrumbItem,
  BreadcrumbLink,
  BreadcrumbList,
  BreadcrumbPage,
  BreadcrumbSeparator,
  Button,
  Card,
  CardContent,
  cn,
  flattenTree,
  NavMenu,
  NavMenuItem,
  Tooltip,
  TooltipContent,
  TooltipTrigger,
  TreeView,
  TreeViewItem,
  type ChartConfig,
  type INodeRendererProps,
} from 'ui'
import {
  Chart,
  ChartActions,
  ChartCard,
  ChartContent,
  ChartHeader,
  ChartLine,
  ChartLoadingState,
  ChartMetric,
} from 'ui-patterns/Chart'
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
import { PageBreadcrumbs, PageBreadcrumbsActions } from 'ui-patterns/PageBreadcrumbs'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
  PageHeader,
  PageHeaderDescription,
  PageHeaderMeta,
  PageHeaderSummary,
  PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { PageNav } from 'ui-patterns/PageNav'
import {
  PageSection,
  PageSectionAside,
  PageSectionContent,
  PageSectionDescription,
  PageSectionMeta,
  PageSectionSummary,
  PageSectionTitle,
} from 'ui-patterns/PageSection'
 
import { PageLayoutLogsContent } from './page-layout-logs-content'
 
type PageId = 'overview' | 'logs' | 'code' | 'settings'
 
const pages: { id: PageId; label: string }[] = [
  { id: 'overview', label: 'Overview' },
  { id: 'logs', label: 'Logs' },
  { id: 'code', label: 'Code' },
  { id: 'settings', label: 'Settings' },
]
 
const CHART_INTERVALS = [
  { key: '15min', label: '15 min', format: 'MMM D, h:mm:ssa', minutes: 15 },
  { key: '1hr', label: '1 hour', format: 'MMM D, h:mma', minutes: 60 },
  { key: '3hr', label: '3 hours', format: 'MMM D, h:mma', minutes: 180 },
  { key: '1day', label: '1 day', format: 'MMM D, h:mma', minutes: 24 * 60 },
] as const
 
const FUNCTION_URL = 'https://demo.supabase.co/functions/v1/hello-world'
 
type MockInvocationDatum = {
  timestamp: string
  ok_count: number
  warning_count: number
  error_count: number
}
 
type MockMetricsDatum = MockInvocationDatum & {
  avg_execution_time: number
  max_execution_time: number
  avg_cpu_time_used: number
  max_cpu_time_used: number
  avg_memory_used: number
  avg_heap_memory_used: number
  avg_external_memory_used: number
}
 
const pseudoNoise = (seed: number, amplitude = 1) => {
  const value = Math.sin(seed * 12.9898) * 43758.5453
  return (value - Math.floor(value)) * amplitude
}
 
const getIntervalMinutes = (intervalKey: string) =>
  CHART_INTERVALS.find((item) => item.key === intervalKey)?.minutes ?? 60
 
const buildMockChartData = (intervalKey: string) => {
  const minutes = getIntervalMinutes(intervalKey)
  const end = new Date()
  end.setSeconds(0, 0)
 
  const invocation: MockInvocationDatum[] = Array.from({ length: minutes }, (_, index) => {
    const timestamp = new Date(end)
    timestamp.setMinutes(end.getMinutes() - (minutes - 1 - index))
 
    const progress = minutes <= 1 ? 0 : index / (minutes - 1)
    const baseLoad = 12 + Math.round(progress * 28)
    const noise = Math.round(pseudoNoise(index + minutes * 0.1, 8))
 
    const ok_count = baseLoad + noise
    const warning_count = index % 17 === 0 ? 2 + (index % 3) : index % 11 === 0 ? 1 : 0
    const error_count = index % 43 === 0 ? 3 + (index % 2) : 0
 
    return {
      timestamp: timestamp.toISOString(),
      ok_count,
      warning_count,
      error_count,
    }
  })
 
  const metrics: MockMetricsDatum[] = invocation.map((datum, index) => {
    const avg_execution_time = Math.round(68 + pseudoNoise(index + 2, 24) + index * 0.15)
    const max_execution_time = Math.round(
      avg_execution_time * (1.25 + pseudoNoise(index + minutes, 0.35))
    )
 
    return {
      ...datum,
      avg_execution_time,
      max_execution_time,
      avg_cpu_time_used: Math.round(8 + pseudoNoise(index + 4, 10) + index * 0.05),
      max_cpu_time_used: Math.round(16 + pseudoNoise(index + 6, 18) + index * 0.1),
      avg_memory_used: Number((40 + pseudoNoise(index + 8, 12) + index * 0.08).toFixed(1)),
      avg_heap_memory_used: Number((24 + pseudoNoise(index + 10, 8) + index * 0.05).toFixed(1)),
      avg_external_memory_used: Number((14 + pseudoNoise(index + 12, 6) + index * 0.04).toFixed(1)),
    }
  })
 
  return { invocation, metrics }
}
 
const EXECUTION_TIME_CHART_CONFIG = {
  avg_execution_time: {
    label: 'Average Execution Time',
    color: 'hsl(var(--foreground-default))',
  },
  max_execution_time: {
    label: 'Max Execution Time',
    color: 'hsl(var(--brand-default))',
  },
} satisfies ChartConfig
 
const CPU_TIME_CHART_CONFIG = {
  max_cpu_time_used: {
    label: 'Max CPU Time',
    color: 'hsl(var(--brand-default))',
  },
} satisfies ChartConfig
 
const MEMORY_CHART_CONFIG = {
  avg_memory_used: {
    label: 'Memory Usage',
    color: 'hsl(var(--brand-default))',
  },
} satisfies ChartConfig
 
type MockFileData = {
  id: number
  name: string
  content: string
  state: 'new' | 'modified' | 'unchanged'
}
 
const MOCK_CODE_FILES: MockFileData[] = [
  {
    id: 1,
    name: 'index.ts',
    state: 'modified',
    content: `import { serve } from 'https://deno.land/std/http/server.ts'
 
serve(async (req) => {
  const { name = 'World' } = await req.json().catch(() => ({}))
 
  return new Response(
    JSON.stringify({ message: \`Hello \${name}\` }),
    { headers: { 'content-type': 'application/json' } }
  )
})`,
  },
  {
    id: 2,
    name: 'deno.json',
    state: 'unchanged',
    content: JSON.stringify({ imports: {} }, null, '\t'),
  },
]
 
const formatRate = (count: number, total: number) =>
  new Intl.NumberFormat('en-US', {
    style: 'percent',
    maximumFractionDigits: 1,
  }).format(total === 0 ? 0 : count / total)
 
const formatMetric = (value: number, unit?: string) => {
  const formatted = unit === 'MB' ? value.toFixed(1) : Math.round(value).toLocaleString('en-US')
  return unit ? `${formatted}${unit}` : formatted
}
 
const getSegmentedButtonClassName = (index: number, total: number) => {
  if (index === 0) return 'rounded-tr-none rounded-br-none'
  if (index === total - 1) return 'rounded-tl-none rounded-bl-none'
  return 'rounded-none'
}
 
const sumBy = <T,>(items: T[], getValue: (item: T) => number) =>
  items.reduce((total, item) => total + getValue(item), 0)
 
const meanBy = <T,>(items: T[], getValue: (item: T) => number) =>
  items.length === 0 ? 0 : sumBy(items, getValue) / items.length
 
export function PageLayoutEdgeFunction() {
  const [activePage, setActivePage] = useState<PageId>('overview')
 
  return (
    <div className="w-full">
      <PageBreadcrumbs
        actions={
          <PageBreadcrumbsActions>
            <Button type="default" size="tiny">
              Test
            </Button>
            <Button type="primary" size="tiny">
              Deploy
            </Button>
          </PageBreadcrumbsActions>
        }
      >
        <BreadcrumbList>
          <BreadcrumbItem>
            <BreadcrumbLink asChild>
              <Link href="/project/demo/functions">Edge Functions</Link>
            </BreadcrumbLink>
          </BreadcrumbItem>
          <BreadcrumbSeparator />
          <BreadcrumbItem>
            <BreadcrumbPage>hello-world</BreadcrumbPage>
          </BreadcrumbItem>
        </BreadcrumbList>
      </PageBreadcrumbs>
 
      <PageNav>
        <NavMenu>
          {pages.map((page) => (
            <NavMenuItem key={page.id} active={activePage === page.id}>
              <button
                type="button"
                aria-pressed={activePage === page.id}
                className="h-full cursor-pointer appearance-none bg-transparent text-inherit"
                onClick={() => setActivePage(page.id)}
              >
                {page.label}
              </button>
            </NavMenuItem>
          ))}
        </NavMenu>
      </PageNav>
 
      {activePage === 'overview' && <OverviewPage />}
      {activePage === 'logs' && (
        <PageContainer size="full" className="px-0 xl:px-0">
          <PageLayoutLogsContent />
        </PageContainer>
      )}
      {activePage === 'code' && (
        <PageContainer size="full" className="px-0 xl:px-0">
          <CodePage />
        </PageContainer>
      )}
      {activePage === 'settings' && <SettingsPage />}
    </div>
  )
}
 
function OverviewPage() {
  const [interval, setInterval] = useState<string>('1hr')
  const selectedInterval =
    CHART_INTERVALS.find((item) => item.key === interval) ?? CHART_INTERVALS[1]
  const dateTimeFormat = selectedInterval.format
 
  const { invocation: mockInvocationChartData, metrics: mockMetricsChartData } = useMemo(
    () => buildMockChartData(interval),
    [interval]
  )
 
  const { totalInvocationCount, totalWarningCount, totalErrorCount } = useMemo(() => {
    const totalInvocationCount = sumBy(
      mockInvocationChartData,
      (datum) => datum.ok_count + datum.warning_count + datum.error_count
    )
 
    return {
      totalInvocationCount,
      totalWarningCount: sumBy(mockInvocationChartData, (datum) => datum.warning_count),
      totalErrorCount: sumBy(mockInvocationChartData, (datum) => datum.error_count),
    }
  }, [mockInvocationChartData])
 
  const { averageExecutionTime, maxExecutionTime } = useMemo(
    () => ({
      averageExecutionTime: meanBy(mockMetricsChartData, (datum) => datum.avg_execution_time),
      maxExecutionTime: Math.max(...mockMetricsChartData.map((datum) => datum.max_execution_time)),
    }),
    [mockMetricsChartData]
  )
 
  const { averageCpuTime, maxCpuTime, averageMemoryUsage, totalHeapMemory, totalExternalMemory } =
    useMemo(() => {
      const totalHeapMemory = sumBy(mockMetricsChartData, (datum) => datum.avg_heap_memory_used)
      const totalExternalMemory = sumBy(
        mockMetricsChartData,
        (datum) => datum.avg_external_memory_used
      )
 
      return {
        averageCpuTime: meanBy(mockMetricsChartData, (datum) => datum.avg_cpu_time_used),
        maxCpuTime: Math.max(...mockMetricsChartData.map((datum) => datum.max_cpu_time_used)),
        averageMemoryUsage: meanBy(mockMetricsChartData, (datum) => datum.avg_memory_used),
        totalHeapMemory,
        totalExternalMemory,
      }
    }, [mockMetricsChartData])
 
  const invocationActions = [
    {
      label: 'Open logs',
      href: '#',
      icon: <ExternalLink size={12} />,
    },
  ]
 
  return (
    <>
      <PageHeader size="small" className="pb-12">
        <PageHeaderMeta>
          <PageHeaderSummary>
            <PageHeaderTitle>hello-world</PageHeaderTitle>
            <PageHeaderDescription className="flex flex-row flex-wrap items-center gap-x-4 gap-y-1 text-sm!">
              <span>{FUNCTION_URL}</span>
              <span className="flex items-center gap-2">
                <Clock size={16} strokeWidth={1.5} className="text-foreground-lighter" />
                <span>Last deployed 2 hours ago</span>
              </span>
            </PageHeaderDescription>
          </PageHeaderSummary>
        </PageHeaderMeta>
      </PageHeader>
 
      <PageSection className="border-t bg-surface-100/50 border-b pb-8 pt-0">
        <PageContainer size="full">
          <div className="flex flex-col gap-5">
            <PageSectionMeta className="items-center! pt-8">
              <PageSectionSummary>
                <div className="flex flex-wrap items-start gap-x-8 gap-y-4">
                  <ChartMetric
                    label="Total Invocations"
                    value={totalInvocationCount}
                    status="default"
                    tooltip="Total number of invocations"
                  />
                  <ChartMetric
                    label="5xx Rate"
                    value={formatRate(totalErrorCount, totalInvocationCount)}
                    status="negative"
                    tooltip="Share of invocations that returned a 5xx status code"
                  />
                  <ChartMetric
                    label="4xx Rate"
                    value={formatRate(totalWarningCount, totalInvocationCount)}
                    status="warning"
                    tooltip="Share of invocations that returned a 4xx status code"
                  />
                </div>
              </PageSectionSummary>
              <PageSectionAside className="flex-wrap @xl:self-center">
                <div className="flex items-center">
                  {CHART_INTERVALS.map((item, index) => (
                    <Button
                      key={item.key}
                      type={interval === item.key ? 'secondary' : 'default'}
                      onClick={() => setInterval(item.key)}
                      className={getSegmentedButtonClassName(index, CHART_INTERVALS.length)}
                    >
                      {item.label}
                    </Button>
                  ))}
                </div>
                <ChartActions actions={invocationActions} />
              </PageSectionAside>
            </PageSectionMeta>
            <Chart>
              <div className="h-40">
                <LogsBarChart
                  data={mockInvocationChartData}
                  DateTimeFormat={dateTimeFormat}
                  isFullHeight
                />
              </div>
            </Chart>
          </div>
        </PageContainer>
      </PageSection>
 
      <PageContainer size="small">
        <PageSection>
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>Errors since last deploy</PageSectionTitle>
            </PageSectionSummary>
            <PageSectionAside>
              <Button type="default" size="tiny" icon={<ExternalLink size={14} />}>
                View logs
              </Button>
            </PageSectionAside>
          </PageSectionMeta>
          <PageSectionContent>
            <div className="rounded-md border border-dashed px-5 py-6 text-sm text-foreground-light">
              <div className="flex items-start gap-3">
                <Check
                  size={16}
                  strokeWidth={1.5}
                  className="mt-0.5 shrink-0 text-brand"
                  aria-hidden="true"
                />
                <div>
                  There have been <span className="text-foreground">847 invocations</span> since
                  last deploy and no errors.
                </div>
              </div>
            </div>
          </PageSectionContent>
        </PageSection>
 
        <PageSection>
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>Performance</PageSectionTitle>
            </PageSectionSummary>
          </PageSectionMeta>
          <PageSectionContent>
            <Chart>
              <ChartCard>
                <ChartHeader align="start">
                  <div className="flex flex-wrap gap-x-8 gap-y-4">
                    <ChartMetric
                      label="Average Execution Time"
                      value={formatMetric(averageExecutionTime, 'ms')}
                      tooltip="Average execution time of function invocations"
                    />
                    <ChartMetric
                      label="Max Execution Time"
                      value={formatMetric(maxExecutionTime, 'ms')}
                      tooltip="Maximum execution time of function invocations"
                    />
                  </div>
                </ChartHeader>
                <ChartContent loadingState={<ChartLoadingState />}>
                  <div className="h-40">
                    <ChartLine
                      data={mockMetricsChartData}
                      dataKey="max_execution_time"
                      dataKeys={['avg_execution_time', 'max_execution_time']}
                      DateTimeFormat={dateTimeFormat}
                      config={EXECUTION_TIME_CHART_CONFIG}
                      isFullHeight
                      showYAxis
                      referenceLines={[
                        {
                          y: averageExecutionTime,
                          label: 'average',
                          stroke: 'hsl(var(--foreground-default))',
                          strokeWidth: 1.5,
                        },
                      ]}
                      YAxisProps={{
                        width: 64,
                        tickFormatter: (value: number) => `${Math.round(value)}ms`,
                      }}
                    />
                  </div>
                </ChartContent>
              </ChartCard>
            </Chart>
          </PageSectionContent>
        </PageSection>
 
        <PageSection>
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>Usage</PageSectionTitle>
            </PageSectionSummary>
          </PageSectionMeta>
          <PageSectionContent>
            <div className="flex flex-col gap-6">
              <Chart>
                <ChartCard>
                  <ChartHeader align="start">
                    <div className="flex flex-wrap gap-x-8 gap-y-4">
                      <ChartMetric
                        label="Average CPU Time"
                        value={formatMetric(averageCpuTime, 'ms')}
                        tooltip="Average CPU time usage for the function"
                      />
                      <ChartMetric
                        label="Max CPU Time"
                        value={formatMetric(maxCpuTime, 'ms')}
                        tooltip="Maximum CPU time usage for the function"
                      />
                    </div>
                  </ChartHeader>
                  <ChartContent loadingState={<ChartLoadingState />}>
                    <div className="h-40">
                      <ChartLine
                        data={mockMetricsChartData}
                        dataKey="max_cpu_time_used"
                        DateTimeFormat={dateTimeFormat}
                        config={CPU_TIME_CHART_CONFIG}
                        isFullHeight
                        showYAxis
                        referenceLines={[
                          {
                            y: averageCpuTime,
                            label: 'average',
                            stroke: 'hsl(var(--foreground-default))',
                            strokeWidth: 1.5,
                          },
                        ]}
                        YAxisProps={{
                          width: 64,
                          tickFormatter: (value: number) => `${Math.round(value)}ms`,
                        }}
                      />
                    </div>
                  </ChartContent>
                </ChartCard>
              </Chart>
 
              <Chart>
                <ChartCard>
                  <ChartHeader align="start">
                    <div className="flex flex-wrap gap-x-8 gap-y-4">
                      <ChartMetric
                        label="Average Memory Usage"
                        value={formatMetric(averageMemoryUsage, 'MB')}
                        tooltip="Average memory usage for the function"
                      />
                      <ChartMetric
                        label="Heap"
                        value={formatRate(totalHeapMemory, totalHeapMemory + totalExternalMemory)}
                        tooltip="Share of memory attributed to heap usage over the selected interval"
                      />
                      <ChartMetric
                        label="External"
                        value={formatRate(
                          totalExternalMemory,
                          totalHeapMemory + totalExternalMemory
                        )}
                        tooltip="Share of memory attributed to external usage over the selected interval"
                      />
                    </div>
                  </ChartHeader>
                  <ChartContent loadingState={<ChartLoadingState />}>
                    <div className="h-40">
                      <ChartLine
                        data={mockMetricsChartData}
                        dataKey="avg_memory_used"
                        DateTimeFormat={dateTimeFormat}
                        config={MEMORY_CHART_CONFIG}
                        isFullHeight
                        showYAxis
                        referenceLines={[
                          {
                            y: averageMemoryUsage,
                            label: 'average',
                            stroke: 'hsl(var(--foreground-default))',
                            strokeWidth: 1.5,
                          },
                        ]}
                        YAxisProps={{
                          width: 64,
                          tickFormatter: (value: number) => `${Number(value).toFixed(1)}MB`,
                        }}
                      />
                    </div>
                  </ChartContent>
                </ChartCard>
              </Chart>
            </div>
          </PageSectionContent>
        </PageSection>
      </PageContainer>
    </>
  )
}
 
function CodePage() {
  const initialFiles = useMemo(() => MOCK_CODE_FILES, [])
  const [files, setFiles] = useState<MockFileData[]>(initialFiles)
  const [selectedFileId, setSelectedFileId] = useState(initialFiles[0]?.id ?? 1)
 
  const selectedFile = files.find((file) => file.id === selectedFileId)
 
  const treeData = useMemo(
    () => ({
      name: '',
      children: files.map((file) => ({
        id: file.id.toString(),
        name: file.name,
        metadata: { originalId: file.id, state: file.state },
      })),
    }),
    [files]
  )
 
  const handleContentChange = (value: string) => {
    setFiles((currentFiles) =>
      currentFiles.map((file) => {
        if (file.id !== selectedFileId) return file
 
        const originalFile = initialFiles.find((item) => item.id === file.id)
        if (!originalFile) return { ...file, content: value, state: 'new' }
        if (originalFile.content !== value) return { ...file, content: value, state: 'modified' }
        return { ...file, content: value, state: 'unchanged' }
      })
    )
  }
 
  const addNewFile = () => {
    const newId = Math.max(0, ...files.map((file) => file.id)) + 1
    const newFile: MockFileData = {
      id: newId,
      name: `file-${newId}.ts`,
      content: '',
      state: 'new',
    }
 
    setFiles((currentFiles) => [...currentFiles, newFile])
    setSelectedFileId(newId)
  }
 
  const renderFileNode = useCallback(
    ({ element, isBranch, isExpanded, getNodeProps, level }: INodeRendererProps) => {
      const originalId =
        typeof element.metadata?.originalId === 'number' ? element.metadata.originalId : null
      const state = element.metadata?.state as MockFileData['state']
 
      return (
        <TreeViewItem
          {...getNodeProps()}
          isExpanded={isExpanded}
          isBranch={isBranch}
          isSelected={originalId === selectedFileId}
          level={level}
          xPadding={16}
          name={element.name}
          className={cn(
            state === 'new' ? 'text-brand-600' : state === 'modified' ? 'text-code_block-2' : ''
          )}
          icon={<File size={14} className="text-foreground-light shrink-0" />}
          onClick={() => {
            if (originalId !== null) setSelectedFileId(originalId)
          }}
          actions={
            state !== 'unchanged' && (
              <div className="flex items-center justify-center w-3">
                <Tooltip>
                  <TooltipTrigger className="text-xs">{state === 'new' ? 'U' : 'M'}</TooltipTrigger>
                  <TooltipContent side="bottom">
                    {state === 'new' ? 'Unsaved' : 'Modified'}
                  </TooltipContent>
                </Tooltip>
              </div>
            )
          }
        />
      )
    },
    [selectedFileId]
  )
 
  return (
    <div className="flex min-h-[480px] flex-col">
      <div className="flex flex-1 overflow-hidden bg-surface-100">
        <div className="flex min-w-64 w-64 flex-col border-r bg-surface-200">
          <div className="flex items-center justify-between border-b px-4 py-4">
            <h3 className="text-sm font-normal font-mono uppercase text-lighter tracking-wide">
              Files
            </h3>
            <Button size="tiny" type="default" icon={<Plus size={14} />} onClick={addNewFile}>
              Add File
            </Button>
          </div>
          <div className="flex-1 overflow-y-auto py-2">
            <TreeView
              data={flattenTree(treeData)}
              aria-label="files tree"
              nodeRenderer={renderFileNode}
            />
          </div>
        </div>
 
        <div className="min-w-0 grow">
          {selectedFile ? (
            <textarea
              aria-label={`Edit ${selectedFile.name}`}
              className="h-full min-h-[420px] w-full resize-none border-0 bg-transparent px-5 py-5 font-mono text-[13px] leading-6 text-foreground focus:outline-none"
              spellCheck={false}
              value={selectedFile.content}
              onChange={(event) => handleContentChange(event.target.value)}
            />
          ) : (
            <div className="flex h-full min-h-[420px] items-center justify-center text-sm text-foreground-light">
              Select a file to edit
            </div>
          )}
        </div>
      </div>
 
      <div className="flex shrink-0 items-center justify-end border-t bg-surface-100 p-4">
        <Button
          type="primary"
          size="medium"
          iconRight={<CornerDownLeft size={10} strokeWidth={1.5} />}
        >
          Deploy updates
        </Button>
      </div>
    </div>
  )
}
 
function SettingsPage() {
  return (
    <>
      <PageHeader size="small" className="pb-12">
        <PageHeaderMeta>
          <PageHeaderSummary>
            <PageHeaderTitle>Settings</PageHeaderTitle>
            <PageHeaderDescription>
              Configure function behavior, security, and deployment options.
            </PageHeaderDescription>
          </PageHeaderSummary>
        </PageHeaderMeta>
      </PageHeader>
 
      <PageContainer size="small">
        <PageSection className="pt-0">
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>Function configuration</PageSectionTitle>
            </PageSectionSummary>
          </PageSectionMeta>
          <PageSectionContent>
            <Card>
              <CardContent className="grid gap-4 p-4">
                <div className="flex items-center justify-between gap-4">
                  <div>
                    <p className="text-sm font-medium">Name</p>
                    <p className="text-sm text-foreground-light">
                      Your slug and endpoint URL will remain the same
                    </p>
                  </div>
                  <span className="font-mono text-sm">hello-world</span>
                </div>
                <div className="flex items-center justify-between gap-4">
                  <div>
                    <p className="text-sm font-medium">Verify JWT with legacy secret</p>
                    <p className="text-sm text-foreground-light">
                      Require a JWT signed by the legacy secret in the Authorization header.
                    </p>
                  </div>
                  <Badge variant="default">Enabled</Badge>
                </div>
              </CardContent>
            </Card>
          </PageSectionContent>
        </PageSection>
 
        <PageSection>
          <PageSectionMeta>
            <PageSectionSummary>
              <PageSectionTitle>Import map</PageSectionTitle>
              <PageSectionDescription>
                Control which import map this function uses at deploy time.
              </PageSectionDescription>
            </PageSectionSummary>
          </PageSectionMeta>
          <PageSectionContent>
            <Card>
              <CardContent className="p-4">
                <div className="flex items-center justify-between gap-4">
                  <div>
                    <p className="text-sm font-medium">Import map</p>
                    <p className="text-sm text-foreground-light">Use the project import map.</p>
                  </div>
                  <Badge variant="default">Default</Badge>
                </div>
              </CardContent>
            </Card>
          </PageSectionContent>
        </PageSection>
      </PageContainer>
    </>
  )
}

Components