Some updates

This commit is contained in:
2026-09-07 00:39:19 -03:00
parent 2b3fc987a9
commit e89550b675
8 changed files with 878 additions and 45 deletions
@@ -1,5 +1,8 @@
---
title: Tune a Protection Profile
description:
Learn how to tune your protection profiles to better protect your application while enhancing
your users' experiences.
---
### Throttling Windows
+81 -9
View File
@@ -4,15 +4,87 @@ description: Welcome to Duckity's documentation site!
icon: Fish
---
Welcome to Duckity's documentation site. Here you'll find everything you need to know about
Duckity, plus some extras to help you use Duckity more efficiently and effectively.
Welcome to Duckity's documentation site. Here you'll find everything you need to know about Duckity,
plus some extras to help you use Duckity more efficiently and effectively.
Duckity is an API-protection software. Unlike CAPTCHAs, which are black boxes and depend on AI
(known to make mistakes), Duckity relies on well-studied algorithms and even some things commonly
used to give you full control over access to your API.
## What Duckity Is
Duckity is NOT a replacement for basic security practices like rate limiting, and neither it is a
DDoS protection service. Use Cloudflare, Bunny.net, or similar for protection of that kind.
Duckity simplifies your API's protection, makes it predictable, and increases the cost an attacker
has to pay to abuse your system.
<Cards>
<Card title="Impose a Cost on Abuse" icon={<LiHandCoins />}>
Duckity adds an invisible-to-the-user non-parallelizable CPU cost on every protected request
that increases the cost of abuse.
</Card>
<Card title="Keep Control" icon={<LiFerrisWheel />}>
Keep full control over your protection's behavior by tuning parameters from the dashboard.
Nothing is implicit.
</Card>
<Card title="Know What Goes On" icon={<LiSearchCheck />}>
Monitor the system's state at any time. Access patterns are logged for as long as you
configure them to audit behavior in real time.
</Card>
<Card title="Set Up Once, Use Everywhere" icon={<LiMonitorSmartphone />}>
Duckity provides cross-platform SDKs for you to use the same protection system everywhere.
Get started in less than 5 minutes.
</Card>
</Cards>
By layering dynamic CPU cost on top of configurable access rules and real-time adaptive costs, users
stop being annoyed by intrusive CAPTCHA puzzles while abusers get an increasing cost.
Optimized for scale and heavy concurrent access, Duckity automatically scales with your system
without hassle.
## Benefits Over CAPTCHAs and Alternatives
Duckity provides several improvements over standard CAPTCHAs and CAPTCHA alternatives:
<Accordions>
<Accordion title="Predictable, Invisible Behavior">
By relying on modern verifiable delay functions, Duckity is able to provide hands-off
security to your system and customers without sacrificing on user experience. Abuse cost
scales dynamically while your users get a predictable application experience, unlike
alternatives where delays vary substantially depending on randomness.
</Accordion>
<Accordion title="Set Up Once, Use Everywhere">
Duckity provides multiple easy-to-use cross-platform SDKs for you to use a single
configuration across all your clients. From web to native desktop and mobile applications,
CLI applications, M2M connections, videogames, IoT, and more.
</Accordion>
<Accordion title="Made to Suit your Application">
Configure Duckity's behavior to suit your application's precise needs. Multiple features can
get individual configurations to beter suit each of their access patterns and requirements.
</Accordion>
<Accordion title="Simple to Integrate">
Most SDKs require no more than 4 lines of code to integrate with your application. Duckity
provides detailed documentation for every feature to help you make the most out of it.
</Accordion>
</Accordions>
Duckity does not try to tell humans and bots apart. Instead, it enforces a background computational
cost that humans do not notice.
| | Duckity | CAPTCHAs |
| --------------------------- | ------- | -------- |
| Simple | ✅ | ✅ |
| Transparent | ✅ | ❌ |
| Predictable behavior | ✅ | ❌ |
| Customizable behavior | ✅ | ❌ |
| Frictionless | ✅ | ❌ |
| Cross-platform | ✅ | ❌ |
| Accessible | ✅ | ❌ |
| Tells humans and bots apart | ❌ | ❓ |
| Cost-effective | ✅ | ❓ |
| AI-resistant | ✅ | ❓ |
## Get Started
You can make a free account by heading over to the [Duckity Dashboard](//app.duckity.com/hello). To
learn how to integrate with your stack in some minutes, head over to the Quick Start page.
<Card href="/quick-start" title="Quick Start" icon={<LiPencil />}>
Integrate Duckity in less than 5 minutes.
</Card>
If you're new to Duckity, head over to the [Quick Start](/quick-start). It'll guide you on how to get started and
give you some templates to reduce the boilerplate configuration tasks.
+2 -6
View File
@@ -4,12 +4,8 @@
"index",
"quick-start",
"---[BookOpenText]Guides---",
"guides/tune-a-protection-profile",
"guides/write-your-own-sdk",
"...guides",
"---[CodeXml]Integrate---",
"sdks/javascript",
"sdks/react",
"sdks/python",
"sdks/rust"
"...sdks"
]
}
+556 -17
View File
@@ -1,6 +1,6 @@
---
title: Quick Start
description: Learn how to use Duckity in some minutes.
description: Integrate Duckity in less than 5 minutes.
icon: Zap
---
@@ -44,36 +44,575 @@ to integrate it into your application.
Don't worry about all the settings displayed for now, you'll learn to tune them later.
</Callout>
## Install an SDK [step]
## Install a Client SDK [step]
To integrate your protection profile with your application, you'll need a client-side SDK and a
server-side SDK.
To integrate your protection profile into your application, you'll need to install a client SDK in
your client and validate solution tokens from your server. This is very simple to do and will take
you little to no time.
Duckity has multiple official SDKs for you to plug and play.
<Tabs items={["JavaScript", "React", "Python", "Rust", "Other"]} groupId="sdk-language">
<Tab value="JavaScript">
<Tabs items={["Via NPM", "Via CDN"]}>
<Tab>
Run the following line in your terminal to install the Duckity SDK.
import { SiC, SiJavascript, SiPython, SiRust, SiReact } from "@icons-pack/react-simple-icons";
```package-install
@duckity/js
```
Then import it in your code:
```ts
import * as duckity from "@duckity/js";
```
</Tab>
<Tab>
If you're using the SDK from a static site, import it using a CDN like
[esm.sh](https://esm.sh/) instead.
```html
<script type="module">
// Using esm.sh
import * as duckity from "https://esm.sh/@duckity/js";
// Using jsdelivr.net
import * as duckity from "https://cdn.jsdelivr.net/npm/@duckity/js";
// Using UNPKG
import * as duckity from "https://unpkg.com/@duckity/js";
</script>
```
</Tab>
</Tabs>
Once you've imported the SDK, solve a challenge like follows:
```ts
async function handleSubmit(e) {
e.preventDefault();
const solution: string = await duckity.solve(PROTECTION_PROFILE_ID);
// Send it to your backend.
}
```
Send that solution token to your backend server. It's usually a good idea to send it in a
`X-Duckity-Solution` header in your request, for example
```ts
await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Duckity-Solution": solution
},
body: JSON.stringify({
login: username,
password: password,
}),
});
```
You can read about advanced usage in the SDK's documentation page:
<Cards>
<Card href="/sdks/javascript" title="JavaScript" icon={<SiJavascript />}>
Integrate Duckity into your application using the JavaScript SDK.
</Card>
</Tab>
<Tab value="React">
Run the following line in your terminal to install the Duckity SDK.
```package-install
@duckity/react
```
Then import the React hook in your code:
```ts
import { useChallenge } from "@duckity/react";
```
You can then use the hook in your client components:
<Tabs items={["Next.js", "Vite", "Remix", "Gatsby", "Expo Web", "Astro"]} groupId="react-framework" persist>
<Tab value="Next.js" id="nextjs">
```ts
"use client";
import { useChallenge } from "@duckity/react";
function MyComponent() {
const duckity = useChallenge(process.env.NEXT_PUBLIC_DUCKITY_PROTECTION_PROFILE_ID);
}
```
</Tab>
<Tab value="Vite" id="vite">
```ts
import { useChallenge } from "@duckity/react";
function MyComponent() {
const duckity = useChallenge(import.meta.env.VITE_DUCKITY_PROTECTION_PROFILE_ID);
}
```
</Tab>
<Tab value="Remix" id="remix">
```ts
import { json } from "@remix-run/node"; // Or cloudflare/deno
import { useLoaderData } from "@remix-run/react";
import { useChallenge } from "@duckity/react";
export async function loader() {
return json({
ENV: {
DUCKITY_PROTECTION_PROFILE_ID: process.env.DUCKITY_PROTECTION_PROFILE_ID,
},
});
}
function MyComponent() {
const data = useLoaderData<typeof loader>();
const duckity = useChallenge(data.ENV.DUCKITY_PROTECTION_PROFILE_ID);
}
```
</Tab>
<Tab value="Gatsby" id="gatsby">
```ts
import { useChallenge } from "@duckity/react";
function MyComponent() {
const duckity = useChallenge(process.env.GATSBY_DUCKITY_PROTECTION_PROFILE_ID);
}
```
</Tab>
<Tab value="Expo Web" id="expo-web">
```ts
import { useChallenge } from "@duckity/react";
function MyComponent() {
const duckity = useChallenge(process.env.EXPO_PUBLIC_DUCKITY_PROTECTION_PROFILE_ID);
}
```
</Tab>
<Tab value="Astro" id="astro">
```ts
import { useChallenge } from "@duckity/react";
function MyComponent() {
const duckity = useChallenge(import.meta.env.PUBLIC_DUCKITY_PROTECTION_PROFILE_ID);
}
```
This is a client component, so use it as follows when rendering it:
```tsx
<MyComponent client:load />
```
</Tab>
</Tabs>
When the component is first rendered, it'll start fetching and solving a challenge.
You can then wait for and use a solution token:
```ts
async function handleSubmit(e) {
e.preventDefault();
const solution = await duckity.wait();
// Send the solution token to your backend.
}
```
Send that solution token to your backend server. It's usually a good idea to send it in a
`X-Duckity-Solution` header in your request, for example
```ts
await fetch("/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
"X-Duckity-Solution": solution
},
body: JSON.stringify({
login: username,
password: password,
}),
});
```
You can read about advanced usage in the SDK's documentation page:
<Card href="/sdks/react" title="React" icon={<SiReact />}>
Integrate Duckity into your React app using the React SDK.
Integrate Duckity into your application using the React SDK.
</Card>
<Card href="/sdks/python" title="Python 3" icon={<SiPython />}>
</Tab>
<Tab value="Python">
Install the SDK from PyPI using your favorite package manager:
{/* prettier-ignore */}
<Tabs items={["pip", "uv", "poetry"]} groupId="python-package-manager" persist>
<Tab value="pip" id="pip">
```sh
pip install duckity
```
</Tab>
<Tab value="uv" id="uv">
```sh
uv add duckity
```
</Tab>
<Tab value="poetry" id="poetry">
```sh
poetry add duckity
```
</Tab>
</Tabs>
Solving a challenge only requires your protection profile ID.
<Tabs items={["Async", "Sync"]} groupId="python-sync" persist>
<Tab value="Async" id="async">
```py
import duckity
solution: str = await duckity.solve(PROTECTION_PROFILE_ID)
```
The CPU-intensive part of solving the challenge is done in a
`ProcessPoolExecutor()`. Neither the GIL nor the async event loop will be blocked.
</Tab>
<Tab value="Sync" id="sync">
```py
import asyncio
import duckity
solution: str = asyncio.run(duckity.solve(PROTECTION_PROFILE_ID))
```
The CPU-intensive part of solving the challenge is done in a
`ProcessPoolExecutor()`. The GIL will not be blocked.
</Tab>
</Tabs>
Once you have a solution token, send it to your backend for validation. It's usually a good
idea to send it in a `X-Duckity-Solution` request header, like follows:
```py
import requests
solution: str = asyncio.run(duckity.solve(PROTECTION_PROFILE_ID))
requests.post(
"https://api.example.com/login",
headers={
"Accept": "application/json",
"X-Duckity-Solution": solution,
},
json={
"login": username,
"password": password
}
)
```
You can read about advanced usage in the SDK's documentation page:
<Card href="/sdks/python" title="Python" icon={<SiPython />}>
Integrate Duckity into your application using the Python SDK.
</Card>
</Tab>
<Tab value="Rust">
Run the following cargo command in your terminal to add the `duckity` crate to your
project:
```sh
cargo add duckity
```
The crate depends on `tokio`, you can use `async-compat` to make it work in `smol`
contexts.
To get and solve a challenge, use `duckity::solve()` like follows:
```rs
let solution: str = duckity::solve(PROTECTION_PROFILE_ID).await?;
```
That's it! Once you have your solution token, you can send it over to your backend for
validation. It's usually a good idea to send it in a `X-Duckity-Solution` request header,
like follows:
```rs
reqwest::Client::new()
.post("https://api.example.com/login")
.header("x-duckity-solution", solution)
.json(&Login {
login: username,
password: password,
})
.send()
.await?;
```
You can read about advanced usage in the SDK's documentation page:
<Card href="/sdks/rust" title="Rust" icon={<SiRust />}>
Integrate Duckity into your application using the Rust SDK. Supports no-std environments.
Integrate Duckity into your application using the Rust SDK.
</Card>
</Cards>
</Tab>
<Tab value="Other">
In case your platform is a different one, we provide a detailed step-by-step guide on how
to write your own client-side SDK implementation in any language, with code examples and
pseudocode. It's written so that anyone can implement a working client-side implementation
for any platform, so if you have a bit of time, you may be able to get it working in less
than an hour.
In case we don't have a client SDK for your language yet, you can use the C SDK. It's written in
Rust, you can link to it through the exposed C API, and it wraps all the required encoding and
decoding in a simple API. You may also find the following guide useful:
<Card href="/guides/write-your-own-sdk" title="Write Your Own SDK" icon={<LiPencil />}>
Integrate Duckity into your application in any programming language and platform.
</Card>
import { FileCodeCorner } from "lucide-react";
We constantly improve this guide with incoming feedback from developers like you. If you
find anything that could be improved, let us know!
<Card title="Write Your Own SDK" icon={<FileCodeCorner />} href="/guides/write-your-own-sdk">
Learn how to write your own SDK in any language.
Additionally, in case anything in the guide is not clear, open a support ticket in the
dashboard and we'll try to help you out. Note that we're not experts in every language nor
platform, so we likely won't be able to debug your code, rather help you understand what
the guide says.
</Tab>
</Tabs>
## Install a Server SDK [step]
Once you have a solution token in your server, validating it is quite easy.
<Tabs items={["JavaScript", "Python", "Rust", "Manual HTTP"]} groupId="sdk-language">
<Tab value="JavaScript">
Run the following line in your terminal to install the Duckity SDK.
```package-install
@duckity/js
```
Then import it in your code:
```ts
import * as duckity from "@duckity/js";
```
To validate the solution token, use `duckity.validate()`.
```ts
let isValid = await duckity.validate(
solution, // The solution token submitted by the client.
clientIp, // The IP address of the client that submitted the solution.
applicationSecret, // Your application's secret. Get it from the dashboard.
protectionProfileId, // The protection profile ID used to generate the challenge.
);
```
That's it! If the solution is not valid, reject the request with an error. Otherwise,
proceed to process the rest of the request.
</Tab>
<Tab value="Python">
Install the SDK from PyPI using your favorite package manager:
{/* prettier-ignore */}
<Tabs items={["pip", "uv", "poetry"]} groupId="python-package-manager" persist>
<Tab value="pip" id="pip">
```sh
pip install duckity
```
</Tab>
<Tab value="uv" id="uv">
```sh
uv add duckity
```
</Tab>
<Tab value="poetry" id="poetry">
```sh
poetry add duckity
```
</Tab>
</Tabs>
Once you have done it, validate the solution token as follows:
<Tabs items={["Async", "Sync"]} groupId="python-sync" persist>
<Tab value="Async" id="async">
```py
import duckity
solution: str # The solution token submitted by the client.
client_ip: str # The IP address of the client that submitted the solution.
application_secret: str # Your application's secret. Get it from the dashboard.
protection_profile_id: str # The protection profile ID used to generate the challenge.
is_valid: bool = await duckity.validate(
solution, client_ip, application_secret, protection_profile_id
)
```
</Tab>
<Tab value="Sync" id="sync">
```py
import asyncio
import duckity
solution: str # The solution token submitted by the client.
client_ip: str # The IP address of the client that submitted the solution.
application_secret: str # Your application's secret. Get it from the dashboard.
protection_profile_id: str # The protection profile ID used to generate the challenge.
is_valid: bool = asyncio.run(
duckity.validate(solution, client_ip, application_secret, protection_profile_id)
)
```
</Tab>
</Tabs>
That's it! If the solution token is valid, you can proceed to process your request. If it's
not, return an error to the client and do not further process the request.
</Tab>
<Tab value="Rust">
Run the following cargo command in your terminal to add the `duckity` crate to your
project:
```sh
cargo add duckity
```
Once you have it installed, you can validate solution tokens as follows:
```rs
let is_valid: bool = duckity::validate(
solution, // The solution token submitted by the client.
client_ip, // The IP address of the client that submitted the solution.
APPLICATION_SECRET, // Your application's secret. Get it from the dashboard.
PROTECTION_PROFILE_ID // The protection profile ID used to generate the challenge.
)
.await?;
```
That's it! If the solution token is valid, you can proceed to process your request. If it's
not, return an error to the client and do not further process the request.
</Tab>
<Tab value="Manual HTTP">
In case you cannot or do not want to install an SDK, you can validate solution tokens by
hand by making the HTTP request by hand instead.
```http
POST /d1/challenges/{protection_profile_id}/validate HTTP/1.1
Host: api.duckity.com
Accept: application/json
Content-Type: application/json
Authorization: Bearer {application_secret}
{
"solution": "<solution-token-submitted-by-the-client>",
"ip": "<ip-address-of-the-client-that-submitted-the-solution>"
}
```
Validation responses are simple:
<Tabs items={["200", "401", "404", "422", "429", "500"]}>
<Tab value="200">
```http
HTTP/1.1 200 OK
Content-Type: application/json
{
"is_valid": true // or false
}
```
</Tab>
<Tab value="401">
```http
HTTP/1.1 401 Unauthorized
Content-Type: application/json
{
"title": "Invalid Application Secret",
"message": "The application secret you provided was not valid."
}
```
</Tab>
<Tab value="404">
```http
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"title": "Protection Profile Not Found",
"message": "The protection profile ID you specified was not found."
}
```
</Tab>
<Tab value="422">
```http
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
{
"title": "Invalid Request Body",
"message": "Are you sure the JSON body you sent in the request was valid?"
}
```
</Tab>
<Tab value="429">
```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"title": "Too Many Requests",
"message": "Wohoah, you're going too fast, buddy. Slow down a bit."
}
```
</Tab>
<Tab value="500">
```http
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"title": "Internal Server Error",
"message": "The server crashed. Please try again."
}
```
</Tab>
</Tabs>
That's it! If the solution token is valid, you can proceed to process your request. If it's
not, return an error to the client and do not further process the request.
Note that status codes are related to server actions, not to client actions. An invalid
solution token submitted by the client will return `200` because the token was validated
successfully (even if the result was that it was invalid), while a wrong application secret
or an incorrect protection profile ID will return `4xx` status codes.
</Tab>
</Tabs>
## Next Steps
Once you have successfully integrated duckity into your application, you're done with the code part.
The behavior of challenges issued is fully customizable from the Duckity dashboard. Almost every
configuration can be easily understood by their dashboard description. However, to learn how to
combine settings to produce a more powerful setup, check out the following guide. It goes
configuration by configuration to teach you how to configure the parameters you need to suit your
application's needs.
<Card
href="/guides/tune-a-protection-profile"
title="Tune a Protection Profile"
icon={<LiPencil />}
>
Learn how to tune your protection profiles to better protect your application while enhancing
your users' experiences.
</Card>
+10
View File
@@ -13,6 +13,16 @@ const config = {
images: {
unoptimized: true,
},
async rewrites() {
return {
beforeFiles: [
{
source: "/:path*.md",
destination: "/llms.mdx/docs/:path*",
},
],
};
},
};
export default withMDX(config);
+105
View File
@@ -0,0 +1,105 @@
'use client';
import { Check, LinkIcon } from 'lucide-react';
import { type ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react';
import { cn } from '../lib/cn';
import { useCopyButton } from '@fumadocs/base-ui/utils/use-copy-button';
import { buttonVariants } from './ui/button';
import { mergeRefs } from '../lib/merge-refs';
import { useTranslations } from '@fuma-translate/react';
import {
Accordion as Root,
AccordionContent,
AccordionHeader,
AccordionItem,
AccordionTrigger,
} from './ui/accordion';
export function Accordions({
ref,
className,
defaultValue,
...props
}: ComponentProps<typeof Root>) {
const rootRef = useRef<HTMLDivElement>(null);
const composedRef = mergeRefs(ref, rootRef);
const [value, setValue] = useState<unknown[]>(defaultValue ?? []);
useEffect(() => {
const id = window.location.hash.substring(1);
const element = rootRef.current;
if (!element || id.length === 0) return;
const selected = document.getElementById(id);
if (!selected || !element.contains(selected)) return;
const value = selected.getAttribute('data-accordion-value');
if (value) setValue((prev) => [value, ...prev]);
}, []);
return (
<Root
ref={composedRef}
value={value}
onValueChange={setValue}
className={(s) =>
cn(
'divide-y divide-fd-border overflow-hidden rounded-lg border bg-fd-card',
typeof className === 'function' ? className(s) : className,
)
}
{...props}
/>
);
}
export function Accordion({
title,
id,
value = String(title),
children,
...props
}: Omit<ComponentProps<typeof AccordionItem>, 'value' | 'title'> & {
title: string | ReactNode;
value?: string;
}) {
return (
<AccordionItem value={value} {...props}>
<AccordionHeader id={id} data-accordion-value={value}>
<AccordionTrigger>{title}</AccordionTrigger>
{id ? <CopyButton id={id} /> : null}
</AccordionHeader>
<AccordionContent hiddenUntilFound>
<div className="px-4 pb-2 text-[0.9375rem] prose-no-margin [&[hidden]:not([hidden='until-found'])]:hidden">
{children}
</div>
</AccordionContent>
</AccordionItem>
);
}
function CopyButton({ id }: { id: string }) {
const t = useTranslations({ note: 'accordion' });
const [checked, onClick] = useCopyButton(() => {
const url = new URL(window.location.href);
url.hash = id;
return navigator.clipboard.writeText(url.toString());
});
return (
<button
type="button"
aria-label={t('Copy Link', { note: 'aria-label' })}
className={cn(
buttonVariants({
color: 'ghost',
className: 'text-fd-muted-foreground me-2',
}),
)}
onClick={onClick}
>
{checked ? <Check className="size-3.5" /> : <LinkIcon className="size-3.5" />}
</button>
);
}
+27 -4
View File
@@ -1,12 +1,31 @@
import defaultMdxComponents from "fumadocs-ui/mdx";
import * as TabsComponents from 'fumadocs-ui/components/tabs';
import * as TabsComponents from "fumadocs-ui/components/tabs";
import Image from "next/image";
import { Mermaid } from '@/components/mermaid';
import { Mermaid } from "@/components/mermaid";
import type { MDXComponents } from "mdx/types";
import * as Twoslash from 'fumadocs-twoslash/ui';
import { TypeTable } from 'fumadocs-ui/components/type-table';
import * as Twoslash from "fumadocs-twoslash/ui";
import { TypeTable } from "fumadocs-ui/components/type-table";
import * as reactSimpleIcons from "@icons-pack/react-simple-icons";
import { icons as reactLucideIcons } from "lucide-react";
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
export function getMDXComponents(components?: MDXComponents) {
let simpleIcons: { [key: string]: any } = {};
for (const key in reactSimpleIcons) {
if (!key.endsWith("Hex")) {
let newKey = "Si" + key.substring(2);
simpleIcons[newKey] = (reactSimpleIcons as { [key: string]: any })[key];
}
}
let lucideIcons: { [key: string]: any } = {};
for (const key in reactLucideIcons) {
if (!key.endsWith("Icon")) {
let newKey = "Li" + key;
lucideIcons[newKey] = (reactLucideIcons as { [key: string]: any })[key].render;
}
}
return {
...defaultMdxComponents,
img: (props) => <Image {...(props as any)} />,
@@ -14,6 +33,10 @@ export function getMDXComponents(components?: MDXComponents) {
...Twoslash,
Mermaid,
TypeTable,
...simpleIcons,
...lucideIcons,
Accordion,
Accordions,
...components,
} satisfies MDXComponents;
}
+85
View File
@@ -0,0 +1,85 @@
'use client';
import { Accordion as Primitive } from '@base-ui/react/accordion';
import { ChevronRight } from 'lucide-react';
import { type ComponentProps } from 'react';
import { cn } from '../../lib/cn';
export function Accordion({ className, ...props }: ComponentProps<typeof Primitive.Root>) {
return (
<Primitive.Root
className={(s) =>
cn(
'divide-y divide-fd-border overflow-hidden rounded-lg border bg-fd-card',
typeof className === 'function' ? className(s) : className,
)
}
{...props}
/>
);
}
export function AccordionItem({ children, ...props }: ComponentProps<typeof Primitive.Item>) {
return <Primitive.Item {...props}>{children}</Primitive.Item>;
}
export function AccordionHeader({
className,
children,
...props
}: ComponentProps<typeof Primitive.Header>) {
return (
<Primitive.Header
className={(s) =>
cn(
'scroll-m-24 not-prose flex flex-row items-center text-fd-card-foreground font-medium has-focus-visible:bg-fd-accent',
typeof className === 'function' ? className(s) : className,
)
}
{...props}
>
{children}
</Primitive.Header>
);
}
export function AccordionTrigger({
className,
children,
...props
}: ComponentProps<typeof Primitive.Trigger>) {
return (
<Primitive.Trigger
className={(s) =>
cn(
'group flex flex-1 items-center gap-2 px-3 py-2.5 text-start focus-visible:outline-none',
typeof className === 'function' ? className(s) : className,
)
}
{...props}
>
<ChevronRight className="size-4 shrink-0 text-fd-muted-foreground transition-transform duration-200 group-data-panel-open:rotate-90" />
{children}
</Primitive.Trigger>
);
}
export function AccordionContent({
className,
children,
...props
}: ComponentProps<typeof Primitive.Panel>) {
return (
<Primitive.Panel
className={(s) =>
cn(
'h-(--accordion-panel-height) overflow-hidden transition-[height] ease-out data-ending-style:h-0 data-starting-style:h-0',
typeof className === 'function' ? className(s) : className,
)
}
{...props}
>
{children}
</Primitive.Panel>
);
}