shadimage
plate
filefumadocssource: ok · hit

Fumadocs app

Fumadocs app for plate

npx shadcn add "https://platejs.org/r/fumadocs.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@radix-ui/react-separator@radix-ui/react-accordion
iconslucide-react

Registry dependencies

https://platejs.org/r/docs.json

Files3 · source cached 10/8/2026, 1:43:22 PM

src/registry/blocks/fumadocs/content/docs/index.mdx
---
title: Home
description: Select a library to explore its documentation, guides, and API references.
---

<Cards>

<Card icon={<MinusIcon className="text-black dark:text-white" />} title='Plate' href='/docs/plate'>

The editor framework.

</Card>

<Card icon={<SquareAsteriskIcon className="text-black dark:text-white" />} title='Add Your Library'>

Edit page in `content/docs/index.mdx`

</Card>

</Cards>
src/registry/blocks/fumadocs/fumadocs-mdx-components.tsx
import type { MDXComponents } from 'mdx/types';

import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { MinusIcon, SquareAsteriskIcon } from 'lucide-react';

import {
  API,
  APIAttributes,
  APIItem,
  APIList,
  APIListAPI,
  APIMethods,
  APIOptions,
  APIParameters,
  APIProps,
  APIReturns,
  APIState,
  APISubList,
  APISubListItem,
  APITransforms,
  EmptyComponent,
  KeyTable,
  KeyTableItem,
  Steps,
} from './mdx-plate-components';

// use this function to get MDX components, you will need it for rendering MDX
export function getMDXComponents(components?: MDXComponents): MDXComponents {
  return {
    ...defaultMdxComponents,
    ...components,
    ...plateMdxComponents,
  };
}

const plateMdxComponents = {
  Accordion,
  Accordions,
  API,
  APIAttributes,
  APIItem,
  APIList,
  APIListAPI,
  APIMethods,
  APIOptions,
  APIParameters,
  APIProps,
  APIReturns,
  APIState,
  APISubList,
  APISubListItem,
  APITransforms,
  ComponentExample: EmptyComponent,
  ComponentInstallation: EmptyComponent,
  ComponentPreview: EmptyComponent,
  ComponentPreviewPro: EmptyComponent,
  ComponentSource: EmptyComponent,
  KeyTable,
  KeyTableItem,
  MinusIcon,
  PackageInfo: EmptyComponent,
  ReleaseIndex: EmptyComponent,
  SquareAsteriskIcon,
  Steps,
};
src/registry/blocks/fumadocs/mdx-plate-components.tsx
'use client';

import React, { type ReactNode, createContext, useState } from 'react';

import * as AccordionPrimitive from '@radix-ui/react-accordion';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { type VariantProps, cva } from 'class-variance-authority';
import { cn } from 'fumadocs-ui/utils/cn';
import { ChevronDownIcon, PlusIcon } from 'lucide-react';

type Item = {
  children: ReactNode;
  name: string;
  type: string;
  default?: boolean | string;
  description?: string;
  optional?: boolean;
  required?: boolean;
  value?: string;
};

const APIContext = createContext<{ listType?: string; name?: string }>({});

const listTypeToId: Record<string, string> = {
  api: 'api',
  attributes: 'attrs',
  methods: 'methods',
  options: 'opt',
  parameters: 'params',
  props: 'props',
  returns: 'returns',
  state: 'state',
  transforms: 'tf',
};

const listTypeToBadgeStyles: Record<string, string> = {
  api: 'bg-pink-50 text-pink-700 dark:bg-pink-950 dark:text-pink-300',
  attributes: 'bg-cyan-50 text-cyan-700 dark:bg-cyan-950 dark:text-cyan-300',
  methods: 'bg-cyan-50 text-cyan-700 dark:bg-cyan-950 dark:text-cyan-300',
  options: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
  parameters: 'bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300',
  props: 'bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300',
  returns: 'bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300',
  state: 'bg-orange-50 text-orange-700 dark:bg-orange-950 dark:text-orange-300',
  transforms:
    'bg-purple-50 text-purple-700 dark:bg-purple-950 dark:text-purple-300',
};

export function API({ children, name }: { children: ReactNode; name: string }) {
  return (
    <APIContext.Provider value={{ name }}>
      <Card className="mt-6 mb-16 bg-white p-0 dark:bg-zinc-800">
        <CardContent className="space-y-6 py-6 **:[p]:m-0">
          {children}
        </CardContent>
      </Card>
    </APIContext.Provider>
  );
}

export function APIItem({ children, name, optional, required, type }: Item) {
  const { listType, name: contextName } = React.useContext(APIContext);

  const id = contextName
    ? `${contextName}-${listType ? `${listTypeToId[listType]}-` : ''}${name}`
        .toLowerCase()
        .replace(/[^\da-z]+/g, '-')
        .replace(/^-|-$/g, '')
    : undefined;

  return (
    <div className="select-text border-none">
      <div className="group p-0 hover:no-underline">
        <li id={id} className="scroll-mt-20">
          <h4 className="!my-0 relative py-2 text-start font-semibold leading-none tracking-tight">
            {id && (
              <a
                className={cn(
                  'opacity-0 hover:opacity-100 group-hover:opacity-100'
                )}
                onClick={(e) => e.stopPropagation()}
                href={`#${id}`}
              >
                <div className="-left-5 absolute top-2 pr-1 leading-none">
                  <LinkIcon className="size-4 text-muted-foreground" />
                </div>
              </a>
            )}
            <span className="font-mono font-semibold text-sm leading-none group-hover:underline">
              {name}
            </span>
            {required && (
              <span className="font-mono text-orange-500 text-xs leading-none">
                {' '}
                REQUIRED
              </span>
            )}
            <span className="text-left font-medium font-mono text-muted-foreground text-sm leading-none">
              {!required && optional && ' optional'} {type}
            </span>
          </h4>
        </li>
      </div>
      <div className="pt-2 pb-0">{children}</div>
    </div>
  );
}

export function APIAttributes({ children, ...props }: APIListProps) {
  return (
    <APIList listType="attributes" {...props}>
      {children}
    </APIList>
  );
}

export function APIOptions({ children, ...props }: APIListProps) {
  return (
    <APIList listType="options" {...props}>
      {children}
    </APIList>
  );
}

export function APIProps({ children, ...props }: APIListProps) {
  return (
    <APIList listType="props" {...props}>
      {children}
    </APIList>
  );
}

export function APIState({ children, ...props }: APIListProps) {
  return (
    <APIList listType="state" {...props}>
      {children}
    </APIList>
  );
}

export function APIReturns({ children, ...props }: APIListProps) {
  return (
    <APIList listType="returns" {...props}>
      {children}
    </APIList>
  );
}

export function APIParameters({ children, ...props }: APIListProps) {
  return (
    <APIList listType="parameters" {...props}>
      {children}
    </APIList>
  );
}

export function APIListAPI({ children, ...props }: APIListProps) {
  return (
    <APIList listType="api" {...props}>
      {children}
    </APIList>
  );
}

export function APITransforms({ children, ...props }: APIListProps) {
  return (
    <APIList listType="transforms" {...props}>
      {children}
    </APIList>
  );
}

export function APIMethods({ children, ...props }: APIListProps) {
  return (
    <APIList listType="methods" {...props}>
      {children}
    </APIList>
  );
}

type APIListProps = {
  children: ReactNode;
  collapsed?: boolean;
  listType?: string;
  type?: string;
};

export function APIList({
  children,
  listType = 'parameters',
  type,
}: APIListProps) {
  const { name } = React.useContext(APIContext);
  const childCount = React.Children.count(children);

  if (listType === 'returns' && !childCount) return null;

  const id = name ? `${name}-${listTypeToId[listType]}` : undefined;

  return (
    <APIContext.Provider value={{ listType, name }}>
      <section className="flex w-full flex-col items-center">
        <div className="w-full">
          <div className="">
            <div className="flex items-center justify-between pb-4">
              <h3
                id={id}
                className="group !my-0 relative scroll-mt-20 font-medium text-lg leading-none tracking-tight"
              >
                {id && (
                  <a
                    className={cn(
                      'opacity-0 hover:opacity-100 group-hover:opacity-100'
                    )}
                    onClick={(e) => e.stopPropagation()}
                    href={`#${id}`}
                  >
                    <div className="-left-5 absolute top-0 pr-1 leading-none">
                      <LinkIcon className="size-4 text-muted-foreground" />
                    </div>
                  </a>
                )}

                <span
                  className={cn(
                    'inline-flex items-center rounded-md px-2 py-0.5 font-medium text-base',
                    'ring-1 ring-black/5 ring-inset dark:ring-white/5',
                    listTypeToBadgeStyles[listType]
                  )}
                >
                  {listType === 'parameters' && 'Parameters'}
                  {listType === 'attributes' && 'Attributes'}
                  {listType === 'returns' && 'Returns'}
                  {listType === 'props' && 'Props'}
                  {listType === 'state' && 'State'}
                  {listType === 'options' && 'Options'}
                  {listType === 'api' && 'API'}
                  {listType === 'methods' && 'Methods'}
                  {listType === 'transforms' && 'Transforms'}
                </span>

                {type && (
                  <span className="ml-2 font-medium font-mono text-muted-foreground text-sm">
                    {type}
                  </span>
                )}
              </h3>
            </div>

            <ul className="!m-0 list-none p-0">
              {/* {listType !== 'returns' && <Separator />} */}
              <Separator />

              <div className="w-full space-y-2 py-4">
                {childCount > 0 ? (
                  children
                ) : (
                  <div className="py-4 text-muted-foreground text-sm">
                    No parameters.
                  </div>
                )}
              </div>
            </ul>
          </div>
        </div>
      </section>
    </APIContext.Provider>
  );
}

export function APISubListItem({
  children,
  name,
  optional,
  parent,
  required,
  type,
}: {
  children: ReactNode;
  name: string;
  parent: string;
  type: string;
  optional?: boolean;
  required?: boolean;
}) {
  const { listType, name: contextName } = React.useContext(APIContext);

  const id = contextName
    ? `${contextName}-${
        listType ? `${listTypeToId[listType]}-` : ''
      }${parent}-${name}`
        .toLowerCase()
        .replace(/[^\da-z]+/g, '-')
        .replace(/^-|-$/g, '')
    : undefined;

  return (
    <div className="border-t border-t-border p-3">
      <h4 className="relative py-2 font-mono font-semibold tracking-tight">
        {id && (
          <a
            className={cn(
              'opacity-0 hover:opacity-100 group-hover:opacity-100'
            )}
            onClick={(e) => e.stopPropagation()}
            href={`#${id}`}
          >
            <div className="-left-5 absolute top-2 pr-1 leading-none">
              <LinkIcon className="size-4 text-muted-foreground" />
            </div>
          </a>
        )}
        <span className="font-semibold text-muted-foreground leading-none">
          {parent}.
        </span>
        <span className="font-semibold leading-none">{name}</span>
        {required && (
          <span className="ml-1 font-mono text-orange-500 text-xs leading-none">
            {' '}
            REQUIRED
          </span>
        )}
        <span className="text-left font-medium font-mono text-muted-foreground text-sm leading-none group-hover:no-underline">
          {!required && optional && ' optional'} {type}
        </span>
      </h4>
      <div>{children}</div>
    </div>
  );
}

export function APISubList({
  children,
  open,
}: {
  children: ReactNode;
  open?: boolean;
}) {
  const [value, setValue] = useState(open ? '1' : '');

  return (
    <Card className="my-2 p-0">
      <Accordion
        className="w-full py-0"
        defaultValue={open ? '1' : ''}
        onValueChange={setValue}
        type="single"
        collapsible
      >
        <AccordionItem className="border-none" value="1">
          <AccordionTrigger className="group px-3 py-2" iconVariant="plus">
            {value ? 'Hide' : 'Show'} child attributes
          </AccordionTrigger>
          <AccordionContent>{children}</AccordionContent>
        </AccordionItem>
      </Accordion>
    </Card>
  );
}

function LinkIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      aria-label="Link to section"
      fill="none"
      height="24"
      stroke="currentColor"
      strokeLinecap="round"
      strokeLinejoin="round"
      strokeWidth="2"
      viewBox="0 0 24 24"
      width="24"
      xmlns="http://www.w3.org/2000/svg"
    >
      <title>Link to section</title>
      <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
      <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
    </svg>
  );
}

function Separator({
  className,
  decorative = true,
  orientation = 'horizontal',
  ...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
  return (
    <SeparatorPrimitive.Root
      orientation={orientation}
      className={cn(
        'shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px',
        className
      )}
      data-slot="separator-root"
      decorative={decorative}
      {...props}
    />
  );
}

function Accordion({
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
  return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
}

function AccordionItem({
  className,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
  return (
    <AccordionPrimitive.Item
      className={cn('border-b **:my-0 last:border-b-0', className)}
      data-slot="accordion-item"
      {...props}
    />
  );
}

function AccordionTrigger({
  children,
  className,
  iconVariant = 'chevron',
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Trigger> & {
  iconVariant?: 'chevron' | 'plus';
}) {
  return (
    <AccordionPrimitive.Header className="flex">
      <AccordionPrimitive.Trigger
        className={cn(
          'flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left font-medium text-sm outline-none transition-all **:my-0 hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
          className
        )}
        data-slot="accordion-trigger"
        {...props}
      >
        {iconVariant === 'plus' && (
          <PlusIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
        )}
        {children}
        {iconVariant === 'chevron' && (
          <ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
        )}
      </AccordionPrimitive.Trigger>
    </AccordionPrimitive.Header>
  );
}

function AccordionContent({
  children,
  className,
  ...props
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
  return (
    <AccordionPrimitive.Content
      className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
      data-slot="accordion-content"
      {...props}
    >
      <div className={cn('pt-0 pb-4', className)}>{children}</div>
    </AccordionPrimitive.Content>
  );
}

export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };

export const Steps = ({ ...props }) => (
  <div
    className={cn(
      'mb-12 ml-4 border-l pl-8 [counter-reset:step]',
      '*:[h3,h4]:[counter-increment:step]',
      '*:[h3,h4]:first-child:mt-0',
      '*:[h3,h4]:mt-8',
      '*:[h3,h4]:mb-4',
      '*:[h3,h4]:text-base',
      '*:[h3,h4]:font-semibold',
      '*:[h3,h4]:before:absolute',
      '*:[h3,h4]:before:inline-flex',
      '*:[h3,h4]:before:h-9',
      '*:[h3,h4]:before:w-9',
      '*:[h3,h4]:before:items-center',
      '*:[h3,h4]:before:justify-center',
      '*:[h3,h4]:before:rounded-full',
      '*:[h3,h4]:before:border-4',
      '*:[h3,h4]:before:border-background',
      '*:[h3,h4]:before:bg-muted',
      '*:[h3,h4]:before:text-center',
      '*:[h3,h4]:before:-indent-px',
      '*:[h3,h4]:before:font-mono',
      '*:[h3,h4]:before:text-base',
      '*:[h3,h4]:before:font-medium',
      '*:[h3,h4]:before:mt-[-4px]',
      '*:[h3,h4]:before:ml-[-50px]',
      '*:[h3,h4]:before:[content:counter(step)]'
    )}
    {...props}
  />
);

function Table({ className, ...props }: React.ComponentProps<'table'>) {
  return (
    <div
      className="relative w-full overflow-x-auto"
      data-slot="table-container"
    >
      <table
        className={cn('w-full caption-bottom text-sm', className)}
        data-slot="table"
        {...props}
      />
    </div>
  );
}

function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
  return (
    <thead
      className={cn('[&_tr]:border-b', className)}
      data-slot="table-header"
      {...props}
    />
  );
}

function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
  return (
    <tbody
      className={cn('[&_tr:last-child]:border-0', className)}
      data-slot="table-body"
      {...props}
    />
  );
}

function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
  return (
    <tfoot
      className={cn(
        'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
        className
      )}
      data-slot="table-footer"
      {...props}
    />
  );
}

function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
  return (
    <tr
      className={cn(
        'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
        className
      )}
      data-slot="table-row"
      {...props}
    />
  );
}

function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
  return (
    <th
      className={cn(
        'h-10 whitespace-nowrap px-2 text-left align-middle font-medium text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
        className
      )}
      data-slot="table-head"
      {...props}
    />
  );
}

function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
  return (
    <td
      className={cn(
        'whitespace-nowrap p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
        className
      )}
      data-slot="table-cell"
      {...props}
    />
  );
}

function TableCaption({
  className,
  ...props
}: React.ComponentProps<'caption'>) {
  return (
    <caption
      className={cn('mt-4 text-muted-foreground text-sm', className)}
      data-slot="table-caption"
      {...props}
    />
  );
}

export {
  Table,
  TableBody,
  TableCaption,
  TableCell,
  TableFooter,
  TableHead,
  TableHeader,
  TableRow,
};

const alertVariants = cva(
  'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
  {
    defaultVariants: {
      variant: 'default',
    },
    variants: {
      variant: {
        default: 'bg-card text-card-foreground',
        destructive:
          'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current',
      },
    },
  }
);

function Alert({
  className,
  variant,
  ...props
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
  return (
    <div
      className={cn(alertVariants({ variant }), className)}
      data-slot="alert"
      role="alert"
      {...props}
    />
  );
}

function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn(
        'col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight',
        className
      )}
      data-slot="alert-title"
      {...props}
    />
  );
}

function AlertDescription({
  className,
  ...props
}: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn(
        'col-start-2 grid justify-items-start gap-1 text-muted-foreground text-sm [&_p]:leading-relaxed',
        className
      )}
      data-slot="alert-description"
      {...props}
    />
  );
}

export { Alert, AlertDescription, AlertTitle };

function Card({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn(
        'flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm',
        className
      )}
      data-slot="card"
      {...props}
    />
  );
}

function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn(
        '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
        className
      )}
      data-slot="card-header"
      {...props}
    />
  );
}

function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn('font-semibold leading-none', className)}
      data-slot="card-title"
      {...props}
    />
  );
}

function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn('text-muted-foreground text-sm', className)}
      data-slot="card-description"
      {...props}
    />
  );
}

function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn(
        'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
        className
      )}
      data-slot="card-action"
      {...props}
    />
  );
}

function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn('px-6', className)}
      data-slot="card-content"
      {...props}
    />
  );
}

function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
      data-slot="card-footer"
      {...props}
    />
  );
}

export {
  Card,
  CardAction,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
};

export const KeyTable = ({
  children,
  className,
  ...props
}: React.ComponentProps<typeof Table>) => (
  <Table className={cn('my-4', className)} {...props}>
    <TableHeader>
      <TableRow>
        <TableHead className="w-[100px]">Key</TableHead>
        <TableHead>Description</TableHead>
      </TableRow>
    </TableHeader>
    <TableBody>{children}</TableBody>
  </Table>
);

export const KeyTableItem = ({
  children,
  hotkey,
}: {
  children: React.ReactNode;
  hotkey: string;
}) => (
  <TableRow>
    <TableCell>
      <kbd className="inline-flex min-w-[8px] items-center justify-center whitespace-nowrap rounded bg-muted px-2 shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(248,_249,_250)_0px_1px_5px_0px_inset,_rgb(193,_200,_205)_0px_0px_0px_0.5px,_rgb(193,_200,_205)_0px_2px_1px_-1px,_rgb(193,_200,_205)_0px_1px_0px_0px] dark:shadow-[rgba(255,_255,_255,_0.1)_0px_0.5px_0px_0px_inset,_rgb(26,_29,_30)_0px_1px_5px_0px_inset,_rgb(76,_81,_85)_0px_0px_0px_0.5px,_rgb(76,_81,_85)_0px_2px_1px_-1px,_rgb(76,_81,_85)_0px_1px_0px_0px]">
        {hotkey}
      </kbd>
    </TableCell>
    <TableCell>{children}</TableCell>
  </TableRow>
);

export const EmptyComponent = ({
  children,
}: {
  children?: React.ReactNode;
}) => <>{children}</>;

More from plate

Browse all components in this library →