feat: initial commit

This commit is contained in:
DecDuck
2025-08-23 16:50:30 +10:00
commit 22332a1b06
86 changed files with 19224 additions and 0 deletions

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

35
.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

158
CHANGELOG.md Normal file
View File

@@ -0,0 +1,158 @@
# Changelog
## 2025-07-29
- Update to React 19 and Next.js 15.4
## 2025-04-28
- Update template to Tailwind CSS v4.1.4
## 2025-04-17
- Fix header opacity
- Organize imports
- Fix scrolling issues when navigating from the mobile nav ([#1387](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1387), [#1666](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1666))
## 2025-04-10
- Update template to Tailwind CSS v4.1.3
## 2025-03-22
- Update template to Tailwind CSS v4.0.15
## 2025-02-18
- Fix responsive design issue in footer
## 2025-02-10
- Update template to Tailwind CSS v4.0.6
## 2025-01-23
- Update template to Tailwind CSS v4.0
## 2024-11-01
- Fix code block rendering when no snippet language is specified ([#1643](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1643))
## 2024-08-08
- Configure experimental `outputFileTracingIncludes` for hosting on Vercel
## 2024-06-21
- Bump Headless UI dependency to v2.1
- Update to new data-attribute-based transition API
## 2024-06-18
- Update `prettier` and `prettier-plugin-tailwindcss` dependencies
## 2024-05-31
- Fix `npm audit` warnings
## 2024-05-07
- Bump Headless UI dependency to v2.0
## 2024-01-17
- Fix `sharp` dependency issues ([#1549](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1549))
## 2024-01-16
- Replace Twitter with X
## 2024-01-10
- Update Tailwind CSS, Next.js, Prettier, TypeScript, ESLint, and other dependencies
- Update Tailwind `darkMode` setting to new `selector` option
- Fix `not-prose` typography alignment issues
- Add name to MDX search function
- Sort classes
## 2023-10-03
- Add missing `@types/mdx` dependency ([#1512](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1512))
## 2023-09-07
- Added TypeScript version of template
## 2023-08-15
- Bump Next.js dependency
## 2023-07-31
- Port template to Next.js app router
## 2023-07-24
- Fix search rendering bug in Safari ([#1470](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1470))
## 2023-07-18
- Add 404 page
- Sort imports and other formatting
## 2023-05-16
- Bump Next.js dependency
## 2023-05-15
- Replace Algolia DocSearch with basic built-in search ([#1395](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1395))
## 2023-04-11
- Bump Next.js dependency
## 2023-03-29
- Bump Tailwind CSS and Prettier dependencies
- Sort classes
## 2023-03-22
- Bump Headless UI dependency
## 2023-02-15
- Fix scroll restoration bug ([#1387](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1387))
## 2023-02-02
- Bump Headless UI dependency
## 2023-01-16
- Fixes yarn compatibility ([#1403](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1403))
- Bump `zustand` dependency
## 2023-01-07
- Enable markdown table support in using `remark-gfm` plugin ([#1398](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1398))
- Fix SVG attribute casing ([#1402](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1402))
## 2023-01-03
- Fix header disappearing in Safari ([#1392](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1392))
## 2022-12-17
- Bump `mdx-annotations` dependency
## 2022-12-16
- Fix scroll jumping issue with Dialog in Safari ([#1387](https://github.com/tailwindlabs/tailwind-plus-issues/issues/1387))
- Update "API" item in header navigation link to home page
- Bump Headless UI dependency
## 2022-12-15
- Initial release

129
LICENSE.md Normal file
View File

@@ -0,0 +1,129 @@
# Tailwind Plus License
## Personal License
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
The license grants permission to **one individual** (the Licensee) to access and use the Components and Templates.
You **can**:
- Use the Components and Templates to create unlimited End Products.
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
You **cannot**:
- Use the Components and Templates to create End Products that are designed to allow an End User to build their own End Products using the Components and Templates or derivatives of the Components and Templates.
- Re-distribute the Components and Templates or derivatives of the Components and Templates separately from an End Product, neither in code or as design assets.
- Share your access to the Components and Templates with any other individuals.
- Use the Components and Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
### Example usage
Examples of usage **allowed** by the license:
- Creating a personal website by yourself.
- Creating a website or web application for a client that will be owned by that client.
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components (like a conference organization app that uses the components for its UI for example) that is free and open source, where the source code is publicly available.
Examples of usage **not allowed** by the license:
- Creating a repository of your favorite Tailwind Plus components or templates (or derivatives based on Tailwind Plus components or templates) and publishing it publicly.
- Creating a React or Vue version of Tailwind Plus and making it available either for sale or for free.
- Create a Figma or Sketch UI kit based on the Tailwind Plus component designs.
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind Plus.
- Creating a theme, template, or project starter kit using the components or templates and making it available either for sale or for free.
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
In simple terms, use Tailwind Plus for anything you like as long as it doesn't compete with Tailwind Plus.
### Personal License Definitions
Licensee is the individual who has purchased a Personal License.
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind Plus license.
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
End User is a user of an End Product.
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
## Team License
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
The license grants permission for **up to 25 Employees and Contractors of the Licensee** to access and use the Components and Templates.
You **can**:
- Use the Components and Templates to create unlimited End Products.
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
You **cannot**:
- Use the Components or Templates to create End Products that are designed to allow an End User to build their own End Products using the Components or Templates or derivatives of the Components or Templates.
- Re-distribute the Components or Templates or derivatives of the Components or Templates separately from an End Product.
- Use the Components or Templates to create End Products that are the property of any individual or entity other than the Licensee or Clients of the Licensee.
- Use the Components or Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
### Example usage
Examples of usage **allowed** by the license:
- Creating a website for your company.
- Creating a website or web application for a client that will be owned by that client.
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components or templates (like a conference organization app that uses the components or a template for its UI for example) that is free and open source, where the source code is publicly available.
Examples of use **not allowed** by the license:
- Creating a repository of your favorite Tailwind Plus components or template (or derivatives based on Tailwind Plus components or templates) and publishing it publicly.
- Creating a React or Vue version of Tailwind Plus and making it available either for sale or for free.
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind Plus.
- Creating a theme or template using the components or templates and making it available either for sale or for free.
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
- Creating any End Product that is not the sole property of either your company or a client of your company. For example your employees/contractors can't use your company Tailwind Plus license to build their own websites or side projects.
### Team License Definitions
Licensee is the business entity who has purchased a Team License.
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind Plus license.
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
End User is a user of an End Product.
Employee is a full-time or part-time employee of the Licensee.
Contractor is an individual or business entity contracted to perform services for the Licensee.
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
## Enforcement
If you are found to be in violation of the license, access to your Tailwind Plus account will be terminated, and a refund may be issued at our discretion. When license violation is blatant and malicious (such as intentionally redistributing the Components or Templates through private warez channels), no refund will be issued.
The copyright of the Components and Templates is owned by Tailwind Labs Inc. You are granted only the permissions described in this license; all other rights are reserved. Tailwind Labs Inc. reserves the right to pursue legal remedies for any unauthorized use of the Components or Templates outside the scope of this license.
## Liability
Tailwind Labs Inc.s liability to you for costs, damages, or other losses arising from your use of the Components or Templates — including third-party claims against you — is limited to a refund of your license fee. Tailwind Labs Inc. may not be held liable for any consequential damages related to your use of the Components or Templates.
This Agreement is governed by the laws of the Province of Ontario and the applicable laws of Canada. Legal proceedings related to this Agreement may only be brought in the courts of Ontario. You agree to service of process at the e-mail address on your original order.
## Questions?
Unsure which license you need, or unsure if your use case is covered by our licenses?
Email us at [support@tailwindcss.com](mailto:support@tailwindcss.com) with your questions.

46
README.md Normal file
View File

@@ -0,0 +1,46 @@
# Protocol
Protocol is a [Tailwind Plus](https://tailwindcss.com/plus) site template built using [Tailwind CSS](https://tailwindcss.com) and [Next.js](https://nextjs.org).
## Getting started
To get started with this template, first install the npm dependencies:
```bash
npm install
```
Next, run the development server:
```bash
npm run dev
```
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website.
## Customizing
You can start editing this template by modifying the files in the `/src` folder. The site will auto-update as you edit these files.
## Global search
This template includes a global search that's powered by the [FlexSearch](https://github.com/nextapps-de/flexsearch) library. It's available by clicking the search input or by using the `⌘K` shortcut.
This feature requires no configuration, and works out of the box by automatically scanning your documentation pages to build its index. You can adjust the search parameters by editing the `/src/mdx/search.mjs` file.
## License
This site template is a commercial product and is licensed under the [Tailwind Plus license](https://tailwindcss.com/plus/license).
## Learn more
To learn more about the technologies used in this site template, see the following resources:
- [Tailwind CSS](https://tailwindcss.com/docs) - the official Tailwind CSS documentation
- [Next.js](https://nextjs.org/docs) - the official Next.js documentation
- [Headless UI](https://headlessui.dev) - the official Headless UI documentation
- [Framer Motion](https://www.framer.com/docs/) - the official Framer Motion documentation
- [MDX](https://mdxjs.com/) - the official MDX documentation
- [Algolia Autocomplete](https://www.algolia.com/doc/ui-libraries/autocomplete/introduction/what-is-autocomplete/) - the official Algolia Autocomplete documentation
- [FlexSearch](https://github.com/nextapps-de/flexsearch) - the official FlexSearch documentation
- [Zustand](https://docs.pmnd.rs/zustand/getting-started/introduction) - the official Zustand documentation

10
mdx-components.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { type MDXComponents } from 'mdx/types'
import * as mdxComponents from '@/components/mdx'
export function useMDXComponents(components: MDXComponents) {
return {
...components,
...mdxComponents,
}
}

24
next.config.mjs Normal file
View File

@@ -0,0 +1,24 @@
import nextMDX from '@next/mdx'
import { recmaPlugins } from './src/mdx/recma.mjs'
import { rehypePlugins } from './src/mdx/rehype.mjs'
import { remarkPlugins } from './src/mdx/remark.mjs'
import withSearch from './src/mdx/search.mjs'
const withMDX = nextMDX({
options: {
remarkPlugins,
rehypePlugins,
recmaPlugins,
},
})
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
outputFileTracingIncludes: {
'/**/*': ['./src/app/**/*.mdx'],
},
}
export default withSearch(withMDX(nextConfig))

9292
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

57
package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"name": "tailwind-plus-protocol",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"browserslist": "defaults, not ie <= 11",
"dependencies": {
"@algolia/autocomplete-core": "^1.19.2",
"@headlessui/react": "^2.2.6",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/mdx": "^15",
"@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/typography": "^0.5.16",
"@types/mdx": "^2.0.13",
"@types/react-highlight-words": "^0.20.0",
"acorn": "^8.15.0",
"clsx": "^2.1.1",
"fast-glob": "^3.3.3",
"flexsearch": "^0.8.205",
"framer-motion": "^12.23.11",
"mdast-util-to-string": "^4.0.0",
"mdx-annotations": "^0.1.4",
"next": "^15",
"next-themes": "^0.4.6",
"prettier-plugin-organize-imports": "^4.2.0",
"react": "^19",
"react-dom": "^19",
"react-highlight-words": "^0.21.0",
"remark": "^15.0.1",
"remark-gfm": "^4.0.1",
"remark-mdx": "^3.1.0",
"shiki": "^0.14.7",
"simple-functional-loader": "^1.2.1",
"tailwindcss": "^4.1.11",
"typescript": "^5.8.3",
"unist-util-filter": "^5.0.1",
"unist-util-visit": "^5.0.0",
"zustand": "^5.0.6"
},
"devDependencies": {
"@types/node": "^24.1.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9.32.0",
"eslint-config-next": "^15",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"sharp": "0.34.3"
}
}

5
postcss.config.js Normal file
View File

@@ -0,0 +1,5 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
},
}

7
prettier.config.js Normal file
View File

@@ -0,0 +1,7 @@
/** @type {import('prettier').Options} */
module.exports = {
singleQuote: true,
semi: false,
plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'],
tailwindStylesheet: './src/styles/tailwind.css',
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,3 @@
# Client API
Under construction.

View File

@@ -0,0 +1,2 @@
# Plugins
Plugins are not implemented yet.

View File

@@ -0,0 +1,100 @@
export const metadata = {
title: 'Quickstart',
description:
'This guide will help you setup a new Drop instance, and how to use it for development purposes.',
}
# Quickstart
This guide will help you setup a small Drop instance to develop against on your local development machine. {{ className: 'lead' }}
<Note>
This setup is not intended for deployment. If you are looking to setup a Drop
instance for actual use, check out the [official
docs](https://docs.droposs.org/docs/guides/quickstart).
</Note>
## Install Docker
Docker is required to start the database, and optionally run a pre-built version of Drop. You should follow the Docker guide for your system on the [official Docker documentation](https://docs.docker.com/engine/install/).
## Option 1: Full Docker setup
The easier and fastest option is to spin up an instance of Drop within a Docker container. While you can't develop against Drop's `nightly` version, it's fast and headache-free.
This `compose.yaml` requires no additional configuration (other than the Drop setup wizard) to start up:
```yaml
services:
postgres:
image: postgres:14-alpine
ports:
- 5432:5432
healthcheck:
test: pg_isready -d drop -U drop
interval: 30s
timeout: 60s
retries: 5
start_period: 10s
volumes:
- ./db:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=drop
- POSTGRES_USER=drop
- POSTGRES_DB=drop
drop:
image: ghcr.io/drop-oss/drop:latest
depends_on:
postgres:
condition: service_healthy
ports:
- 3000:3000
volumes:
- ./library:/library
- ./data:/data
environment:
- DATABASE_URL=postgres://drop:drop@postgres:5432/drop
- EXTERNAL_URL=http://localhost:3000
```
## Option 2: From source (`nightly`)
You can alternatively run Drop from source, and use Docker for the database.
### Clone the GitHub repository
First, clone the Drop repository:
```bash
git clone https://github.com/Drop-OSS/drop.git
```
### Checkout your preferred branch
By default, Git selects the `develop` branch, which is what `nightly` is released from. You may want to checkout a tag (e.g. `v0.3.2`), or the `main` branch (latest stable).
### Install dependencies
We use the `yarn` package manager. Install dependencies with:
```bash
yarn
```
### Start the database
Open another shell (or the same one, if you're okay with running the database in the background), and `cd` into the `dev-tools` directory. Then, start the database:
```bash
docker compose up [-d]
```
(`-d` if you want to run it in the background)
### Start Drop
Then, move back to the root of the repository, and then start Drop:
```bash
yarn dev
```

103
src/app/guides/web/page.mdx Normal file
View File

@@ -0,0 +1,103 @@
export const metadata = {
title: 'Web API',
description:
"On this page, we'll get started with the Web API and generating an API token.",
}
# Web API
The Web API is used by the frontend to render and perform all its actions. Every function in the frontend is available through the Web API, except API token & client management for security reasons. {{ className: 'lead' }}
## Tokens
The Web API uses Bearer tokens to authenticate. They are called API tokens for this documentation. There are two kinds of API tokens:
- **System**: System tokens operate on the same level as "admins" in the Drop UI. They manage most things, can create users, and configure the instance. System API tokens can only be created by admins.
- **User**: User tokens operate **as a user**, essentially impersonating them. User tokens generally have only read access, except to user-specific resources like libraries/collections.
Each token has a list of ACL (access control list) permissions. Each kind of token has different ACLs (for the different resources they can access). If system and user-level ACLs are referenced together, they are prefixed by `system:` and `user:`.
ACL permissions are defined by the user when they create the API token. As a third-party developer, you can either list the necessary ACLs in your documentation or application, or Drop supports pre-filling the API token creation form by creating a special URL.
### API token creation URL
The 'payload' for the API token creation is a Base64 encoded JSON object that encodes the following information:
```json
{
"name": "My Application's Token",
"acls": [
"read",
"store:read",
"object:read",
"object:update",
"object:delete",
"clients:read",
"clients:revoke",
"..."
]
}
```
You can use the following script in your browser's console to generate the payload:
```javascript
btoa(JSON.stringify({ ... }))
```
With the payload, you can create a URL, depending on the kind of token you want to create:
- **System**: `http[s]://[Drop instance URL]/admin/settings/tokens?payload={ payload }`
- **User**: `http[s]://[Drop instance URL]/account/tokens?payload={ ... }`
When the user visits the URL, it will automatically open the token generation modal with your values pre-filled.
## Authentication
To authenticate requests to the Drop API, use your token in the Authorization header, like so:
```
Authorization: Bearer {token}
```
## Checking your ACLs
<Row>
<Col>
To verify the ACLs granted to your token, you can use the `/api/v1/token` endpoint. It returns a JSON array of all the ACLs granted to your token.
The ACLs are in global format, which means they're prefixed with `user:` or `system:`.
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/token">
```bash {{ title: 'cURL' }}
curl -G http://localhost:3000/api/v1/token \
-H "Authorization: Bearer {token}"
```
```javascript
const response = await fetch("http://localhost:3000/api/v1/token", {
headers: {
Authorization: "Bearer {token}"
}
});
const data = await response.json();
console.log(data) // [ "user:read", "user:store:read" ]
```
</CodeGroup>
```json {{ title: 'Response' }}
[
"user:read",
"user:store:read"
]
```
</Col>
</Row>

42
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,42 @@
import glob from 'fast-glob'
import { type Metadata } from 'next'
import { Providers } from '@/app/providers'
import { Layout } from '@/components/Layout'
import { type Section } from '@/components/SectionProvider'
import '@/styles/tailwind.css'
export const metadata: Metadata = {
title: {
template: '%s - Drop API Reference',
default: 'Drop API Reference',
},
}
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
let pages = await glob('**/*.mdx', { cwd: 'src/app' })
let allSectionsEntries = (await Promise.all(
pages.map(async (filename) => [
'/' + filename.replace(/(^|\/)page\.mdx$/, ''),
(await import(`./${filename}`)).sections,
]),
)) as Array<[string, Array<Section>]>
let allSections = Object.fromEntries(allSectionsEntries)
return (
<html lang="en" className="h-full" suppressHydrationWarning>
<body className="flex min-h-full bg-white antialiased dark:bg-zinc-900">
<Providers>
<div className="w-full">
<Layout allSections={allSections}>{children}</Layout>
</div>
</Providers>
</body>
</html>
)
}

24
src/app/not-found.tsx Normal file
View File

@@ -0,0 +1,24 @@
import { Button } from '@/components/Button'
import { HeroPattern } from '@/components/HeroPattern'
export default function NotFound() {
return (
<>
<HeroPattern />
<div className="mx-auto flex h-full max-w-xl flex-col items-center justify-center py-16 text-center">
<p className="text-sm font-semibold text-zinc-900 dark:text-white">
404
</p>
<h1 className="mt-2 text-2xl font-bold text-zinc-900 dark:text-white">
Page not found
</h1>
<p className="mt-2 text-base text-zinc-600 dark:text-zinc-400">
Sorry, we couldnt find the page youre looking for.
</p>
<Button href="/" arrow="right" className="mt-8">
Back to docs
</Button>
</div>
</>
)
}

48
src/app/page.mdx Normal file
View File

@@ -0,0 +1,48 @@
import { Guides } from '@/components/Guides'
import { Resources } from '@/components/Resources'
import { HeroPattern } from '@/components/HeroPattern'
export const metadata = {
title: 'API Documentation',
description:
'Learn everything there is to know about the Protocol API and integrate Protocol into your product.',
}
export const sections = [
{ title: 'Guides', id: 'guides' },
{ title: 'Resources', id: 'resources' },
]
<HeroPattern />
# Drop API Documentation
Use the Drop API to interact with Drop-instances, on a per-instance level, to build third-party integrations and applications. Drop is built with an API-first development model, which means the API can do nearly anything users can, and more. {{ className: 'lead' }}
<div className="not-prose mt-6 mb-16 flex gap-3">
<Button href="/quickstart" arrow="right">
<>Quickstart</>
</Button>
</div>
## Getting started {{ anchor: false }}
To get started, you will need to decide on the type of application you are building. Drop has two sets of APIs:
- **Web API**: interaction with most of the application, used for management and integration.
- **Client API**: a specific subset of APIs, plus additional APIs used to do client-only actions, like download content and networking features.
Then, use the associated authentication mechanism to authenticate with Drop.
<div className="not-prose mt-6 mb-16 flex gap-3">
<Button href="/quickstart" arrow="right" variant="outline">
<>Generate a Web API token</>
</Button>
<Button href="/quickstart" arrow="right" variant="outline">
<>Authenticate as a client</>
</Button>
</div>
<Guides />
<Resources />

37
src/app/providers.tsx Normal file
View File

@@ -0,0 +1,37 @@
'use client'
import { ThemeProvider, useTheme } from 'next-themes'
import { useEffect } from 'react'
function ThemeWatcher() {
let { resolvedTheme, setTheme } = useTheme()
useEffect(() => {
let media = window.matchMedia('(prefers-color-scheme: dark)')
function onMediaChange() {
let systemTheme = media.matches ? 'dark' : 'light'
if (resolvedTheme === systemTheme) {
setTheme('system')
}
}
onMediaChange()
media.addEventListener('change', onMediaChange)
return () => {
media.removeEventListener('change', onMediaChange)
}
}, [resolvedTheme, setTheme])
return null
}
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" disableTransitionOnChange>
<ThemeWatcher />
{children}
</ThemeProvider>
)
}

View File

@@ -0,0 +1,3 @@
# Objects
Under construction.

499
src/app/web/users/page.mdx Normal file
View File

@@ -0,0 +1,499 @@
export const metadata = {
title: 'Users',
description:
"On this page, we'll dive into users and how to interact with them through the Web API.",
}
# Users
Users, like most other web applications, are usually owned by a single person, and are used to categorize their activities and data. As Drop is intended to be a private platform, most requests need to be authenticated by a user first. {{ className: 'lead' }}
## The user model
The user model contains most of the profile information about a user, including their username and profile picture. User endpoints should be used to fetch, interact and manage users, and update information about them.
### Properties
<Properties>
<Property name="id" type="string">
UUID of the user. Used internally to refer to this user.
</Property>
<Property name="username" type="string">
Unique alphanumeric username (with length limitations), used for
character-restricted representations of users (like IGNs).
</Property>
<Property name="displayName" type="string">
Unconstrained preferred name of this user. Used when possible.
</Property>
<Property name="email" type="string">
Email of this user. Not currently used, but collected in case of future
functionality.
</Property>
<Property name="profilePictureObjectId" type="string">
The object ID of this user's profile picture. Check out the
[Objects](../objects) API on how to fetch the image.
</Property>
<Property name="admin" type="boolean">
Admin flag. If enabled, this user has access to system-level endpoints.
</Property>
<Property name="enabled" type="boolean">
If this user is currently enabled, and can be access.
</Property>
</Properties>
---
## Fetch current user {{ tag: 'GET', label: '/api/v1/user', apilevel: "user", acl: "read" }}
<Row>
<Col>
This endpoint fetches the current user. If you are authenticated, it returns the user. If you are not, it returns `null`.
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/user">
```bash {{ title: 'cURL' }}
curl -G http://localhost:3000/api/v1/user \
-H "Authorization: Bearer {token}"
```
```js
const response = await fetch("http://localhost:3000/api/v1/user", {
headers: {
Authorization: "Bearer {token}"
}
});
const user = await response.json();
```
</CodeGroup>
```json {{ title: 'Response' }}
{
"id": "d2b487be-f796-4259-83d9-6ec27ad2947e",
"username": "exampleusername",
"email": "example@example.com",
"displayName": "Example Username",
"profilePictureObjectId": "1d5f7aeb-df75-439c-a2e9-b875af4724d2",
"admin": true,
"enabled": true
}
```
</Col>
</Row>
---
## List all users {{ tag: 'GET', label: '/api/v1/admin/users', apilevel: "system", acl: "user:read" }}
<Row>
<Col>
This endpoint fetches all users on this instance.
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/admin/users">
```bash {{ title: 'cURL' }}
curl -G http://localhost:3000/api/v1/admin/users \
-H "Authorization: Bearer {token}"
```
```js
const response = await fetch("http://localhost:3000/api/v1/admin/users", {
headers: {
Authorization: "Bearer {token}"
}
});
const users = await response.json();
```
</CodeGroup>
```json {{ title: 'Response' }}
[
{
"id": "d2b487be-f796-4259-83d9-6ec27ad2947e",
"username": "exampleusername",
"email": "example@example.com",
"displayName": "Example Username",
"profilePictureObjectId": "1d5f7aeb-df75-439c-a2e9-b875af4724d2",
"admin": true,
"enabled": true
}
]
```
</Col>
</Row>
---
## Fetch user {{ tag: 'GET', label: '/api/v1/admin/users/:id', apilevel: "system", acl: "user:read" }}
<Row>
<Col>
This endpoint fetches a user by ID.
### Route parameters
<Properties>
<Property name="id" type="string">
ID of user you want to fetch.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/admin/users/:id">
```bash {{ title: 'cURL' }}
curl -G http://localhost:3000/api/v1/admin/users/{id} \
-H "Authorization: Bearer {token}"
```
```js
const response = await fetch("http://localhost:3000/api/v1/admin/users/{id}", {
headers: {
Authorization: "Bearer {token}"
}
});
const user = await response.json();
```
</CodeGroup>
```json {{ title: 'Response' }}
{
"id": "d2b487be-f796-4259-83d9-6ec27ad2947e",
"username": "exampleusername",
"email": "example@example.com",
"displayName": "Example Username",
"profilePictureObjectId": "1d5f7aeb-df75-439c-a2e9-b875af4724d2",
"admin": true,
"enabled": true
}
```
</Col>
</Row>
---
## Delete user {{ tag: 'DELETE', label: '/api/v1/admin/users/:id', apilevel: "system", acl: "user:delete" }}
<Row>
<Col>
This endpoint deletes a user by ID
### Route parameters
<Properties>
<Property name="id" type="string">
ID of user you want to delete.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/admin/users/:id">
```bash {{ title: 'cURL' }}
curl -X DELETE http://localhost:3000/api/v1/admin/users/{id} \
-H "Authorization: Bearer {token}"
```
```js
const response = await fetch("http://localhost:3000/api/v1/admin/users/{id}", {
headers: {
Authorization: "Bearer {token}"
},
method: "DELETE"
});
```
</CodeGroup>
``` {{ title: 'Response' }}
No response returned.
```
</Col>
</Row>
---
## Authentication mechanisms
With these endpoint above, you might be wondering, "well, how do you actually _create_ a user?". That job, in Drop, is given to authentication mechanism. Each user can multiple, but at least one, authentication "mechanism" attached to their account. These may be (non-exhaustive):
- **Simple**: username & password combination
- **OIDC**: OpenID Connect, allowing for Single Sign On (SSO),
These authentication mechanisms are also responsible for _creating_ the user, since each user must have at least one mechanism active.
---
## Fetch authentication mechanisms {{ tag: 'GET', label: '/api/v1/admin/auth', apilevel: "system", acl: "auth:read" }}
<Row>
<Col>
This endpoint returns all authentication mechanisms, and a configuration object for each if enabled, otherwise `undefined`. For mechanisms that require no authentication, the object is simply `true` or `false` depending on whether or not it's enabled.
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/admin/auth">
```bash {{ title: 'cURL' }}
curl -G http://localhost:3000/api/v1/admin/auth \
-H "Authorization: Bearer {token}"
```
```js
const response = await fetch("http://localhost:3000/api/v1/admin/auth", {
headers: {
Authorization: "Bearer {token}"
}
});
const authenticationMechanisms = await response.json();
```
</CodeGroup>
```json {{ title: 'Response' }}
{
"Simple": true,
"OpenID": {
"authorizationUrl": "https://my.oidc.app/application/o/authorize/",
"scopes": "openid, email, profile, offline_access",
"adminGroup": "admin",
"usernameClaim": "preferred_username",
"externalUrl": "http://localhost:3000"
}
}
```
</Col>
</Row>
---
## Simple authentication
Simple authentication is the default and fallback in Drop. It relies on a simple username & password pair to log in. Users are created through a concept of invites.
---
## Fetch invitations {{ tag: 'GET', label: '/api/v1/admin/invitation', apilevel: "system", acl: "auth:simple:invitation:read" }}
<Row>
<Col>
This endpoint is used to fetch all invitations for an instance.
### Response
<Properties>
<Property name="id" type="string">
Invitation ID, used as a secret for the invite.
</Property>
<Property name="isAdmin" type="boolean">
Whether or not this invite will create the user as an admin.
</Property>
<Property name="username" type="string">
Optional, will enforce a username on the account.
</Property>
<Property name="email" type="string">
Optional, will enforce an email on the account.
</Property>
<Property name="expires" type="timestamp">
Timestamp for when this invite will expire. If "Never" is selected in the UI, the invitation actually expires in 100 years.
</Property>
<Property name="inviteUrl" type="timestamp">
A link to the frontend page that will allow a user to register through a web browser. Uses the EXTERNAL_URL of the instance.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/admin/invitation">
```bash {{ title: 'cURL' }}
curl -G http://localhost:3000/api/v1/admin/invitation \
-H "Authorization: Bearer {token}"
```
```js
const response = await fetch("http://localhost:3000/api/v1/admin/invitation", {
headers: {
Authorization: "Bearer {token}"
}
});
const invitations = await response.json();
```
</CodeGroup>
```json {{ title: 'Response' }}
[
{
"id": "418d2c3f-8029-409e-b47c-0c3b158be3e1",
"isAdmin": true,
"username": "testusername",
"email": "example@example.com",
"expires": "2025-09-23T06:37:19.047Z",
"inviteUrl": "http://localhost:3000/auth/register?id=418d2c3f-8029-409e-b47c-0c3b158be3e1"
}
]
```
</Col>
</Row>
---
## Create invitation {{ tag: 'POST', label: '/api/v1/admin/invitation', apilevel: "system", acl: "auth:simple:invitation:new" }}
<Row>
<Col>
This endpoint is used to create an invitation.
### Required parameters
<Properties>
<Property name="expires" type="timestamp">
Timestamp for when this invite will expire, in the format of an ISO timestamp. Typically, you can convert a JavaScript Date object.
</Property>
</Properties>
### Optional parameters
<Properties>
<Property name="username" type="string">
Username to enforce on the invitation.
</Property>
<Property name="email" type="string">
Email to enforce on the invitation.
</Property>
<Property name="isAdmin" type="boolean">
Whether or not to make this an admin invitation, defaults to false.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/admin/invitation">
```bash {{ title: 'cURL' }}
curl -X POST http://localhost:3000/api/v1/admin/invitation \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d "{ ... }"
```
```js
const response = await fetch("http://localhost:3000/api/v1/admin/invitation", {
headers: {
Authorization: "Bearer {token}"
},
body: {
expires: new Date(), // Will probably want longer than an expiry time of "now"
isAdmin: true
},
method: "POST"
});
const invitation = await response.json();
```
</CodeGroup>
```json {{ title: 'Response' }}
[
{
"id": "418d2c3f-8029-409e-b47c-0c3b158be3e1",
"isAdmin": true,
"username": "testusername",
"email": "example@example.com",
"expires": "2025-09-23T06:37:19.047Z",
"inviteUrl": "http://localhost:3000/auth/register?id=418d2c3f-8029-409e-b47c-0c3b158be3e1"
}
]
```
</Col>
</Row>
---
## Delete an invitation {{ tag: 'DELETE', label: '/api/v1/admin/invitation', apilevel: "system", acl: "auth:simple:invitation:delete" }}
<Row>
<Col>
This endpoint is used to delete an invitation.
### Required parameters
<Properties>
<Property name="id" type="string">
ID of invitation to delete.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/admin/invitation">
```bash {{ title: 'cURL' }}
curl -X DELETE http://localhost:3000/api/v1/admin/invitation \
-H "Authorization: Bearer {token}" \
-H "Content-Type: application/json" \
-d "{ ... }"
```
```js
const response = await fetch("http://localhost:3000/api/v1/admin/invitation", {
headers: {
Authorization: "Bearer {token}"
},
body: {
id: "..."
},
method: "POST"
});
```
</CodeGroup>
``` {{ title: 'Response' }}
No response.
```
</Col>
</Row>

82
src/components/Button.tsx Normal file
View File

@@ -0,0 +1,82 @@
import clsx from 'clsx'
import Link from 'next/link'
function ArrowIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
d="m11.5 6.5 3 3.5m0 0-3 3.5m3-3.5h-9"
/>
</svg>
)
}
const variantStyles = {
primary:
'rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-1 dark:ring-inset dark:ring-blue-400/20 dark:hover:bg-blue-400/10 dark:hover:text-blue-300 dark:hover:ring-blue-300',
secondary:
'rounded-full bg-zinc-100 py-1 px-3 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-zinc-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-zinc-300',
filled:
'rounded-full bg-zinc-900 py-1 px-3 text-white hover:bg-zinc-700 dark:bg-blue-500 dark:text-white dark:hover:bg-blue-400',
outline:
'rounded-full py-1 px-3 text-zinc-700 ring-1 ring-inset ring-zinc-900/10 hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:ring-white/10 dark:hover:bg-white/5 dark:hover:text-white',
text: 'text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-500',
}
type ButtonProps = {
variant?: keyof typeof variantStyles
arrow?: 'left' | 'right'
} & (
| React.ComponentPropsWithoutRef<typeof Link>
| (React.ComponentPropsWithoutRef<'button'> & { href?: undefined })
)
export function Button({
variant = 'primary',
className,
children,
arrow,
...props
}: ButtonProps) {
className = clsx(
'inline-flex gap-0.5 justify-center overflow-hidden text-sm font-medium transition',
variantStyles[variant],
className,
)
let arrowIcon = (
<ArrowIcon
className={clsx(
'mt-0.5 h-5 w-5',
variant === 'text' && 'relative top-px',
arrow === 'left' && '-ml-1 rotate-180',
arrow === 'right' && '-mr-1',
)}
/>
)
let inner = (
<>
{arrow === 'left' && arrowIcon}
{children}
{arrow === 'right' && arrowIcon}
</>
)
if (typeof props.href === 'undefined') {
return (
<button className={className} {...props}>
{inner}
</button>
)
}
return (
<Link className={className} {...props}>
{inner}
</Link>
)
}

387
src/components/Code.tsx Normal file
View File

@@ -0,0 +1,387 @@
'use client'
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
import clsx from 'clsx'
import {
Children,
createContext,
isValidElement,
useContext,
useEffect,
useRef,
useState,
} from 'react'
import { create } from 'zustand'
import { Tag } from '@/components/Tag'
const languageNames: Record<string, string> = {
js: 'JavaScript',
ts: 'TypeScript',
javascript: 'JavaScript',
typescript: 'TypeScript',
php: 'PHP',
python: 'Python',
ruby: 'Ruby',
go: 'Go',
}
function getPanelTitle({
title,
language,
}: {
title?: string
language?: string
}) {
if (title) {
return title
}
if (language && language in languageNames) {
return languageNames[language]
}
return 'Code'
}
function ClipboardIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeWidth="0"
d="M5.5 13.5v-5a2 2 0 0 1 2-2l.447-.894A2 2 0 0 1 9.737 4.5h.527a2 2 0 0 1 1.789 1.106l.447.894a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2Z"
/>
<path
fill="none"
strokeLinejoin="round"
d="M12.5 6.5a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2m5 0-.447-.894a2 2 0 0 0-1.79-1.106h-.527a2 2 0 0 0-1.789 1.106L7.5 6.5m5 0-1 1h-3l-1-1"
/>
</svg>
)
}
function CopyButton({ code }: { code: string }) {
let [copyCount, setCopyCount] = useState(0)
let copied = copyCount > 0
useEffect(() => {
if (copyCount > 0) {
let timeout = setTimeout(() => setCopyCount(0), 1000)
return () => {
clearTimeout(timeout)
}
}
}, [copyCount])
return (
<button
type="button"
className={clsx(
'group/button absolute top-3.5 right-4 overflow-hidden rounded-full py-1 pr-3 pl-2 text-2xs font-medium opacity-0 backdrop-blur-sm transition group-hover:opacity-100 focus:opacity-100',
copied
? 'bg-blue-400/10 ring-1 ring-blue-400/20 ring-inset'
: 'bg-white/5 hover:bg-white/7.5 dark:bg-white/2.5 dark:hover:bg-white/5',
)}
onClick={() => {
window.navigator.clipboard.writeText(code).then(() => {
setCopyCount((count) => count + 1)
})
}}
>
<span
aria-hidden={copied}
className={clsx(
'pointer-events-none flex items-center gap-0.5 text-zinc-400 transition duration-300',
copied && '-translate-y-1.5 opacity-0',
)}
>
<ClipboardIcon className="h-5 w-5 fill-zinc-500/20 stroke-zinc-500 transition-colors group-hover/button:stroke-zinc-400" />
Copy
</span>
<span
aria-hidden={!copied}
className={clsx(
'pointer-events-none absolute inset-0 flex items-center justify-center text-blue-400 transition duration-300',
!copied && 'translate-y-1.5 opacity-0',
)}
>
Copied!
</span>
</button>
)
}
function CodePanelHeader({ tag, label }: { tag?: string; label?: string }) {
if (!tag && !label) {
return null
}
return (
<div className="flex h-9 items-center gap-2 border-y border-t-transparent border-b-white/7.5 bg-white/2.5 bg-zinc-900 px-4 dark:border-b-white/5 dark:bg-white/1">
{tag && (
<div className="dark flex">
<Tag variant="small">{tag}</Tag>
</div>
)}
{tag && label && (
<span className="h-0.5 w-0.5 rounded-full bg-zinc-500" />
)}
{label && (
<span className="font-mono text-xs text-zinc-400">{label}</span>
)}
</div>
)
}
function CodePanel({
children,
tag,
label,
code,
}: {
children: React.ReactNode
tag?: string
label?: string
code?: string
}) {
let child = Children.only(children)
if (isValidElement(child)) {
const props = child.props as { tag?: string; label?: string; code?: string }
tag = props.tag ?? tag
label = props.label ?? label
code = props.code ?? code
}
if (!code) {
throw new Error(
'`CodePanel` requires a `code` prop, or a child with a `code` prop.',
)
}
return (
<div className="group dark:bg-white/2.5">
<CodePanelHeader tag={tag} label={label} />
<div className="relative">
<pre className="overflow-x-auto p-4 text-xs text-white">{children}</pre>
<CopyButton code={code} />
</div>
</div>
)
}
function CodeGroupHeader({
title,
children,
selectedIndex,
}: {
title: string
children: React.ReactNode
selectedIndex: number
}) {
let hasTabs = Children.count(children) > 1
if (!title && !hasTabs) {
return null
}
return (
<div className="flex min-h-[calc(--spacing(12)+1px)] flex-wrap items-start gap-x-4 border-b border-zinc-700 bg-zinc-800 px-4 dark:border-zinc-800 dark:bg-transparent">
{title && (
<h3 className="mr-auto pt-3 text-xs font-semibold text-white">
{title}
</h3>
)}
{hasTabs && (
<TabList className="-mb-px flex gap-4 text-xs font-medium">
{Children.map(children, (child, childIndex) => (
<Tab
className={clsx(
'border-b py-3 transition data-selected:not-data-focus:outline-hidden',
childIndex === selectedIndex
? 'border-blue-500 text-blue-400'
: 'border-transparent text-zinc-400 hover:text-zinc-300',
)}
>
{getPanelTitle(
isValidElement(child)
? (child.props as { title?: string })
: {},
)}
</Tab>
))}
</TabList>
)}
</div>
)
}
function CodeGroupPanels({
children,
...props
}: React.ComponentPropsWithoutRef<typeof CodePanel>) {
let hasTabs = Children.count(children) > 1
if (hasTabs) {
return (
<TabPanels>
{Children.map(children, (child) => (
<TabPanel>
<CodePanel {...props}>{child}</CodePanel>
</TabPanel>
))}
</TabPanels>
)
}
return <CodePanel {...props}>{children}</CodePanel>
}
function usePreventLayoutShift() {
let positionRef = useRef<HTMLElement>(null)
let rafRef = useRef<number | undefined>(undefined)
useEffect(() => {
return () => {
if (typeof rafRef.current !== 'undefined') {
window.cancelAnimationFrame(rafRef.current)
}
}
}, [])
return {
positionRef,
preventLayoutShift(callback: () => void) {
if (!positionRef.current) {
return
}
let initialTop = positionRef.current.getBoundingClientRect().top
callback()
rafRef.current = window.requestAnimationFrame(() => {
let newTop =
positionRef.current?.getBoundingClientRect().top ?? initialTop
window.scrollBy(0, newTop - initialTop)
})
},
}
}
const usePreferredLanguageStore = create<{
preferredLanguages: Array<string>
addPreferredLanguage: (language: string) => void
}>()((set) => ({
preferredLanguages: [],
addPreferredLanguage: (language) =>
set((state) => ({
preferredLanguages: [
...state.preferredLanguages.filter(
(preferredLanguage) => preferredLanguage !== language,
),
language,
],
})),
}))
function useTabGroupProps(availableLanguages: Array<string>) {
let { preferredLanguages, addPreferredLanguage } = usePreferredLanguageStore()
let [selectedIndex, setSelectedIndex] = useState(0)
let activeLanguage = [...availableLanguages].sort(
(a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a),
)[0]
let languageIndex = availableLanguages.indexOf(activeLanguage)
let newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex
if (newSelectedIndex !== selectedIndex) {
setSelectedIndex(newSelectedIndex)
}
let { positionRef, preventLayoutShift } = usePreventLayoutShift()
return {
as: 'div' as const,
ref: positionRef,
selectedIndex,
onChange: (newSelectedIndex: number) => {
preventLayoutShift(() =>
addPreferredLanguage(availableLanguages[newSelectedIndex]),
)
},
}
}
const CodeGroupContext = createContext(false)
export function CodeGroup({
children,
title,
...props
}: React.ComponentPropsWithoutRef<typeof CodeGroupPanels> & { title: string }) {
let languages =
Children.map(children, (child) =>
getPanelTitle(
isValidElement(child) ? (child.props as { title?: string }) : {},
),
) ?? []
let tabGroupProps = useTabGroupProps(languages)
let hasTabs = Children.count(children) > 1
let containerClassName =
'my-6 overflow-hidden rounded-2xl bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10'
let header = (
<CodeGroupHeader title={title} selectedIndex={tabGroupProps.selectedIndex}>
{children}
</CodeGroupHeader>
)
let panels = <CodeGroupPanels {...props}>{children}</CodeGroupPanels>
return (
<CodeGroupContext.Provider value={true}>
{hasTabs ? (
<TabGroup {...tabGroupProps} className={containerClassName}>
<div className="not-prose">
{header}
{panels}
</div>
</TabGroup>
) : (
<div className={containerClassName}>
<div className="not-prose">
{header}
{panels}
</div>
</div>
)}
</CodeGroupContext.Provider>
)
}
export function Code({
children,
...props
}: React.ComponentPropsWithoutRef<'code'>) {
let isGrouped = useContext(CodeGroupContext)
if (isGrouped) {
if (typeof children !== 'string') {
throw new Error(
'`Code` children must be a string when nested inside a `CodeGroup`.',
)
}
return <code {...props} dangerouslySetInnerHTML={{ __html: children }} />
}
return <code {...props}>{children}</code>
}
export function Pre({
children,
...props
}: React.ComponentPropsWithoutRef<typeof CodeGroup>) {
let isGrouped = useContext(CodeGroupContext)
if (isGrouped) {
return children
}
return <CodeGroup {...props}>{children}</CodeGroup>
}

106
src/components/Feedback.tsx Normal file
View File

@@ -0,0 +1,106 @@
'use client'
import { Transition } from '@headlessui/react'
import clsx from 'clsx'
import { forwardRef, useState } from 'react'
function CheckIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<circle cx="10" cy="10" r="10" strokeWidth="0" />
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="m6.75 10.813 2.438 2.437c1.218-4.469 4.062-6.5 4.062-6.5"
/>
</svg>
)
}
function FeedbackButton(
props: Omit<React.ComponentPropsWithoutRef<'button'>, 'type' | 'className'>,
) {
return (
<button
type="submit"
className="px-3 text-sm font-medium text-zinc-600 transition hover:bg-zinc-900/2.5 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-white/5 dark:hover:text-white"
{...props}
/>
)
}
const FeedbackForm = forwardRef<
React.ElementRef<'form'>,
React.ComponentPropsWithoutRef<'form'>
>(function FeedbackForm({ onSubmit, className, ...props }, ref) {
return (
<form
{...props}
ref={ref}
onSubmit={onSubmit}
className={clsx(
className,
'absolute inset-0 flex items-center justify-center gap-6 md:justify-start',
)}
>
<p className="text-sm text-zinc-600 dark:text-zinc-400">
Was this page helpful?
</p>
<div className="group grid h-8 grid-cols-[1fr_1px_1fr] overflow-hidden rounded-full border border-zinc-900/10 dark:border-white/10">
<FeedbackButton data-response="yes">Yes</FeedbackButton>
<div className="bg-zinc-900/10 dark:bg-white/10" />
<FeedbackButton data-response="no">No</FeedbackButton>
</div>
</form>
)
})
const FeedbackThanks = forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(function FeedbackThanks({ className, ...props }, ref) {
return (
<div
{...props}
ref={ref}
className={clsx(
className,
'absolute inset-0 flex justify-center md:justify-start',
)}
>
<div className="flex items-center gap-3 rounded-full bg-blue-50/50 py-1 pr-3 pl-1.5 text-sm text-blue-900 ring-1 ring-blue-500/20 ring-inset dark:bg-blue-500/5 dark:text-blue-200 dark:ring-blue-500/30">
<CheckIcon className="h-5 w-5 flex-none fill-blue-500 stroke-white dark:fill-blue-200/20 dark:stroke-blue-200" />
Thanks for your feedback!
</div>
</div>
)
})
export function Feedback() {
let [submitted, setSubmitted] = useState(false)
function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault()
// event.nativeEvent.submitter.dataset.response
// => "yes" or "no"
setSubmitted(true)
}
return (
<div className="relative h-8">
<Transition show={!submitted}>
<FeedbackForm
className="duration-300 data-closed:opacity-0 data-leave:pointer-events-none"
onSubmit={onSubmit}
/>
</Transition>
<Transition show={submitted}>
<FeedbackThanks className="delay-150 duration-300 data-closed:opacity-0" />
</Transition>
</div>
)
}

145
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,145 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Button } from '@/components/Button'
import { navigation } from '@/components/Navigation'
function PageLink({
label,
page,
previous = false,
}: {
label: string
page: { href: string; title: string }
previous?: boolean
}) {
return (
<>
<Button
href={page.href}
aria-label={`${label}: ${page.title}`}
variant="secondary"
arrow={previous ? 'left' : 'right'}
>
{label}
</Button>
<Link
href={page.href}
tabIndex={-1}
aria-hidden="true"
className="text-base font-semibold text-zinc-900 transition hover:text-zinc-600 dark:text-white dark:hover:text-zinc-300"
>
{page.title}
</Link>
</>
)
}
function PageNavigation() {
let pathname = usePathname()
let allPages = navigation.flatMap((group) => group.links)
let currentPageIndex = allPages.findIndex((page) => page.href === pathname)
if (currentPageIndex === -1) {
return null
}
let previousPage = allPages[currentPageIndex - 1]
let nextPage = allPages[currentPageIndex + 1]
if (!previousPage && !nextPage) {
return null
}
return (
<div className="flex">
{previousPage && (
<div className="flex flex-col items-start gap-3">
<PageLink label="Previous" page={previousPage} previous />
</div>
)}
{nextPage && (
<div className="ml-auto flex flex-col items-end gap-3">
<PageLink label="Next" page={nextPage} />
</div>
)}
</div>
)
}
function XIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path d="M11.1527 8.92804L16.2525 3H15.044L10.6159 8.14724L7.07919 3H3L8.34821 10.7835L3 17H4.20855L8.88474 11.5643L12.6198 17H16.699L11.1524 8.92804H11.1527ZM9.49748 10.8521L8.95559 10.077L4.644 3.90978H6.50026L9.97976 8.88696L10.5216 9.66202L15.0446 16.1316H13.1883L9.49748 10.8524V10.8521Z" />
</svg>
)
}
function GitHubIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 1.667c-4.605 0-8.334 3.823-8.334 8.544 0 3.78 2.385 6.974 5.698 8.106.417.075.573-.182.573-.406 0-.203-.011-.875-.011-1.592-2.093.397-2.635-.522-2.802-1.002-.094-.246-.5-1.005-.854-1.207-.291-.16-.708-.556-.01-.567.656-.01 1.124.62 1.281.876.75 1.292 1.948.93 2.427.705.073-.555.291-.93.531-1.143-1.854-.213-3.791-.95-3.791-4.218 0-.929.322-1.698.854-2.296-.083-.214-.375-1.09.083-2.265 0 0 .698-.224 2.292.876a7.576 7.576 0 0 1 2.083-.288c.709 0 1.417.096 2.084.288 1.593-1.11 2.291-.875 2.291-.875.459 1.174.167 2.05.084 2.263.53.599.854 1.357.854 2.297 0 3.278-1.948 4.005-3.802 4.219.302.266.563.78.563 1.58 0 1.143-.011 2.061-.011 2.35 0 .224.156.491.573.405a8.365 8.365 0 0 0 4.11-3.116 8.707 8.707 0 0 0 1.567-4.99c0-4.721-3.73-8.545-8.334-8.545Z"
/>
</svg>
)
}
function DiscordIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path d="M16.238 4.515a14.842 14.842 0 0 0-3.664-1.136.055.055 0 0 0-.059.027 10.35 10.35 0 0 0-.456.938 13.702 13.702 0 0 0-4.115 0 9.479 9.479 0 0 0-.464-.938.058.058 0 0 0-.058-.027c-1.266.218-2.497.6-3.664 1.136a.052.052 0 0 0-.024.02C1.4 8.023.76 11.424 1.074 14.782a.062.062 0 0 0 .024.042 14.923 14.923 0 0 0 4.494 2.272.058.058 0 0 0 .064-.02c.346-.473.654-.972.92-1.496a.057.057 0 0 0-.032-.08 9.83 9.83 0 0 1-1.404-.669.058.058 0 0 1-.029-.046.058.058 0 0 1 .023-.05c.094-.07.189-.144.279-.218a.056.056 0 0 1 .058-.008c2.946 1.345 6.135 1.345 9.046 0a.056.056 0 0 1 .059.007c.09.074.184.149.28.22a.058.058 0 0 1 .023.049.059.059 0 0 1-.028.046 9.224 9.224 0 0 1-1.405.669.058.058 0 0 0-.033.033.056.056 0 0 0 .002.047c.27.523.58 1.022.92 1.495a.056.056 0 0 0 .062.021 14.878 14.878 0 0 0 4.502-2.272.055.055 0 0 0 .016-.018.056.056 0 0 0 .008-.023c.375-3.883-.63-7.256-2.662-10.246a.046.046 0 0 0-.023-.021Zm-9.223 8.221c-.887 0-1.618-.814-1.618-1.814s.717-1.814 1.618-1.814c.908 0 1.632.821 1.618 1.814 0 1-.717 1.814-1.618 1.814Zm5.981 0c-.887 0-1.618-.814-1.618-1.814s.717-1.814 1.618-1.814c.908 0 1.632.821 1.618 1.814 0 1-.71 1.814-1.618 1.814Z" />
</svg>
)
}
function SocialLink({
href,
icon: Icon,
children,
}: {
href: string
icon: React.ComponentType<{ className?: string }>
children: React.ReactNode
}) {
return (
<Link href={href} className="group">
<span className="sr-only">{children}</span>
<Icon className="h-5 w-5 fill-zinc-700 transition group-hover:fill-zinc-900 dark:group-hover:fill-zinc-500" />
</Link>
)
}
function SmallPrint() {
return (
<div className="flex flex-col items-center justify-between gap-5 border-t border-zinc-900/5 pt-8 sm:flex-row dark:border-white/5">
<p className="text-xs text-zinc-600 dark:text-zinc-400">
&copy; Copyright {new Date().getFullYear()}. All rights reserved.
</p>
<div className="flex gap-4">
<SocialLink href="#" icon={XIcon}>
Follow us on X
</SocialLink>
<SocialLink href="#" icon={GitHubIcon}>
Follow us on GitHub
</SocialLink>
<SocialLink href="#" icon={DiscordIcon}>
Join our Discord server
</SocialLink>
</div>
</div>
)
}
export function Footer() {
return (
<footer className="mx-auto w-full max-w-2xl space-y-10 pb-16 lg:max-w-5xl">
<PageNavigation />
<SmallPrint />
</footer>
)
}

View File

@@ -0,0 +1,55 @@
import { useId } from 'react'
export function GridPattern({
width,
height,
x,
y,
squares,
...props
}: React.ComponentPropsWithoutRef<'svg'> & {
width: number
height: number
x: string | number
y: string | number
squares: Array<[x: number, y: number]>
}) {
let patternId = useId()
return (
<svg aria-hidden="true" {...props}>
<defs>
<pattern
id={patternId}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path d={`M.5 ${height}V.5H${width}`} fill="none" />
</pattern>
</defs>
<rect
width="100%"
height="100%"
strokeWidth={0}
fill={`url(#${patternId})`}
/>
{squares && (
<svg x={x} y={y} className="overflow-visible">
{squares.map(([x, y]) => (
<rect
strokeWidth="0"
key={`${x}-${y}`}
width={width + 1}
height={height + 1}
x={x * width}
y={y * height}
/>
))}
</svg>
)}
</svg>
)
}

49
src/components/Guides.tsx Normal file
View File

@@ -0,0 +1,49 @@
import { Button } from '@/components/Button'
import { Heading } from '@/components/Heading'
const guides = [
{
href: '/guides/web',
name: 'Web API',
description:
'Learn how to use the Web API to manage and interact with the Drop server.',
},
{
href: '/guides/client',
name: 'Client API',
description:
'Learn how to use the Client API to download and interact with other clients.',
},
{
href: '/guides/plugins',
name: 'Plugins',
description: 'Learn how to develop and publish plugins for Drop.',
},
]
export function Guides() {
return (
<div className="my-16 xl:max-w-none">
<Heading level={2} id="guides">
Guides
</Heading>
<div className="not-prose mt-4 grid grid-cols-1 gap-8 border-t border-zinc-900/5 pt-10 sm:grid-cols-2 xl:grid-cols-4 dark:border-white/5">
{guides.map((guide) => (
<div key={guide.href}>
<h3 className="text-sm font-semibold text-zinc-900 dark:text-white">
{guide.name}
</h3>
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
{guide.description}
</p>
<p className="mt-4">
<Button href={guide.href} variant="text" arrow="right">
Read more
</Button>
</p>
</div>
))}
</div>
</div>
)
}

97
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,97 @@
import clsx from 'clsx'
import { motion, useScroll, useTransform } from 'framer-motion'
import Link from 'next/link'
import { forwardRef } from 'react'
import { Button } from '@/components/Button'
import { Logo } from '@/components/Logo'
import {
MobileNavigation,
useIsInsideMobileNavigation,
useMobileNavigationStore,
} from '@/components/MobileNavigation'
import { MobileSearch, Search } from '@/components/Search'
import { ThemeToggle } from '@/components/ThemeToggle'
import { CloseButton } from '@headlessui/react'
function TopLevelNavItem({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
return (
<li>
<Link
href={href}
className="text-sm/5 text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white"
>
{children}
</Link>
</li>
)
}
export const Header = forwardRef<
React.ComponentRef<'div'>,
React.ComponentPropsWithoutRef<typeof motion.div>
>(function Header({ className, ...props }, ref) {
let { isOpen: mobileNavIsOpen } = useMobileNavigationStore()
let isInsideMobileNavigation = useIsInsideMobileNavigation()
let { scrollY } = useScroll()
let bgOpacityLight = useTransform(scrollY, [0, 72], ['50%', '90%'])
let bgOpacityDark = useTransform(scrollY, [0, 72], ['20%', '80%'])
return (
<motion.div
{...props}
ref={ref}
className={clsx(
className,
'fixed inset-x-0 top-0 z-50 flex h-14 items-center justify-between gap-12 px-4 transition sm:px-6 lg:left-72 lg:z-30 lg:px-8 xl:left-80',
!isInsideMobileNavigation &&
'backdrop-blur-xs lg:left-72 xl:left-80 dark:backdrop-blur-sm',
isInsideMobileNavigation
? 'bg-white dark:bg-zinc-900'
: 'bg-white/(--bg-opacity-light) dark:bg-zinc-900/(--bg-opacity-dark)',
)}
style={
{
'--bg-opacity-light': bgOpacityLight,
'--bg-opacity-dark': bgOpacityDark,
} as React.CSSProperties
}
>
<div
className={clsx(
'absolute inset-x-0 top-full h-px transition',
(isInsideMobileNavigation || !mobileNavIsOpen) &&
'bg-zinc-900/7.5 dark:bg-white/7.5',
)}
/>
<Search />
<div className="flex items-center gap-5 lg:hidden">
<MobileNavigation />
<CloseButton as={Link} href="/" aria-label="Home">
<Logo className="h-6" />
</CloseButton>
</div>
<div className="flex items-center gap-5">
<nav className="hidden md:block">
<ul role="list" className="flex items-center gap-8">
<TopLevelNavItem href="https://docs.droposs.org/">Documentation</TopLevelNavItem>
<TopLevelNavItem href="https://github.com/Drop-OSS/">GitHub</TopLevelNavItem>
<TopLevelNavItem href="https://discord.gg/ACq4qZp4a9">Discord</TopLevelNavItem>
</ul>
</nav>
<div className="hidden md:block md:h-5 md:w-px md:bg-zinc-900/10 md:dark:bg-white/15" />
<div className="flex gap-4">
<MobileSearch />
<ThemeToggle />
</div>
</div>
</motion.div>
)
})

159
src/components/Heading.tsx Normal file
View File

@@ -0,0 +1,159 @@
'use client'
import { useInView } from 'framer-motion'
import Link from 'next/link'
import { useEffect, useRef } from 'react'
import { useSectionStore } from '@/components/SectionProvider'
import { Tag } from '@/components/Tag'
import { remToPx } from '@/lib/remToPx'
function AnchorIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 20 20"
fill="none"
strokeLinecap="round"
aria-hidden="true"
{...props}
>
<path d="m6.5 11.5-.964-.964a3.535 3.535 0 1 1 5-5l.964.964m2 2 .964.964a3.536 3.536 0 0 1-5 5L8.5 13.5m0-5 3 3" />
</svg>
)
}
function Eyebrow({
tag,
label,
apilevel,
acl,
}: {
tag?: string
label?: string
apilevel?: string
acl?: string
}) {
if (!tag && !label && !apilevel) {
return null
}
const apiLevelColor =
apilevel?.toLowerCase() == 'user'
? 'text-blue-400'
: apilevel?.toLowerCase() == 'system'
? 'text-yellow-600'
: 'text-zinc-400'
return (
<div className="flex items-center gap-x-3">
{tag && <Tag>{tag}</Tag>}
{tag && label && (
<span className="h-0.5 w-0.5 rounded-full bg-zinc-300 dark:bg-zinc-600" />
)}
{label && (
<span className="font-mono text-xs text-zinc-400">{label}</span>
)}
{(tag || label) && apilevel && (
<span className="h-0.5 w-0.5 rounded-full bg-zinc-300 dark:bg-zinc-600" />
)}
{apilevel && (
<span className={`font-mono text-xs font-bold ${apiLevelColor}`}>
{apilevel.toUpperCase()}
</span>
)}
{apilevel && acl && (
<span className="h-0.5 w-0.5 rounded-full bg-zinc-300 dark:bg-zinc-600" />
)}
{apilevel && acl && (
<span className="font-mono text-xs text-green-300">
ACL: <span className="text-zinc-900 dark:text-zinc-100">{acl}</span>
</span>
)}
</div>
)
}
function Anchor({
id,
inView,
children,
}: {
id: string
inView: boolean
children: React.ReactNode
}) {
return (
<Link
href={`#${id}`}
className="group text-inherit no-underline hover:text-inherit"
>
{inView && (
<div className="absolute mt-1 ml-[calc(-1*var(--width))] hidden w-(--width) opacity-0 transition [--width:calc(2.625rem+0.5px+50%-min(50%,calc(var(--container-lg)+(--spacing(8)))))] group-hover:opacity-100 group-focus:opacity-100 md:block lg:z-50 2xl:[--width:--spacing(10)]">
<div className="group/anchor block h-5 w-5 rounded-lg bg-zinc-50 ring-1 ring-zinc-300 transition ring-inset hover:ring-zinc-500 dark:bg-zinc-800 dark:ring-zinc-700 dark:hover:bg-zinc-700 dark:hover:ring-zinc-600">
<AnchorIcon className="h-5 w-5 stroke-zinc-500 transition dark:stroke-zinc-400 dark:group-hover/anchor:stroke-white" />
</div>
</div>
)}
{children}
</Link>
)
}
export function Heading<Level extends 2 | 3>({
children,
tag,
label,
level,
anchor = true,
apilevel,
acl,
...props
}: React.ComponentPropsWithoutRef<`h${Level}`> & {
id: string
tag?: string
label?: string
level?: Level
anchor?: boolean
apilevel?: string
acl?: string
}) {
level = level ?? (2 as Level)
let Component = `h${level}` as 'h2' | 'h3'
let ref = useRef<HTMLHeadingElement>(null)
let registerHeading = useSectionStore((s) => s.registerHeading)
let inView = useInView(ref, {
margin: `${remToPx(-3.5)}px 0px 0px 0px`,
amount: 'all',
})
useEffect(() => {
if (level === 2) {
registerHeading({
id: props.id,
ref,
offsetRem: tag || label ? 8 : 6,
})
}
})
return (
<>
<Eyebrow tag={tag} label={label} apilevel={apilevel} acl={acl} />
<Component
ref={ref}
className={tag || label ? 'mt-2 scroll-mt-32' : 'scroll-mt-24'}
{...props}
>
{anchor ? (
<Anchor id={props.id} inView={inView}>
{children}
</Anchor>
) : (
children
)}
</Component>
</>
)
}

View File

@@ -0,0 +1,32 @@
import { GridPattern } from '@/components/GridPattern'
export function HeroPattern() {
return (
<div className="absolute inset-0 -z-10 mx-0 max-w-none overflow-hidden">
<div className="absolute top-0 left-1/2 -ml-152 h-100 w-325 dark:mask-[linear-gradient(white,transparent)]">
<div className="absolute inset-0 bg-linear-to-r from-blue-100 to-blue-900 mask-[radial-gradient(farthest-side_at_top,white,transparent)] opacity-40 dark:from-blue-600/30 dark:to-blue-300/30 dark:opacity-100">
<GridPattern
width={72}
height={56}
x={-12}
y={4}
squares={[
[4, 3],
[2, 1],
[7, 3],
[10, 6],
]}
className="absolute inset-x-0 inset-y-[-50%] h-[200%] w-full skew-y-[-18deg] fill-black/40 stroke-black/50 mix-blend-overlay dark:fill-white/2.5 dark:stroke-white/5"
/>
</div>
<svg
viewBox="0 0 1113 440"
aria-hidden="true"
className="absolute top-0 left-1/2 -ml-76 w-278.25 fill-white blur-[26px] dark:hidden"
>
<path d="M.016 439.5s-9.5-300 434-300S882.516 20 882.516 20V0h230.004v439.5H.016Z" />
</svg>
</div>
</div>
)
}

46
src/components/Layout.tsx Normal file
View File

@@ -0,0 +1,46 @@
'use client'
import { motion } from 'framer-motion'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Footer } from '@/components/Footer'
import { Header } from '@/components/Header'
import { Logo } from '@/components/Logo'
import { Navigation } from '@/components/Navigation'
import { SectionProvider, type Section } from '@/components/SectionProvider'
export function Layout({
children,
allSections,
}: {
children: React.ReactNode
allSections: Record<string, Array<Section>>
}) {
let pathname = usePathname()
return (
<SectionProvider sections={allSections[pathname] ?? []}>
<div className="h-full lg:ml-72 xl:ml-80">
<motion.header
layoutScroll
className="contents lg:pointer-events-none lg:fixed lg:inset-0 lg:z-40 lg:flex"
>
<div className="contents lg:pointer-events-auto lg:block lg:w-72 lg:overflow-y-auto lg:border-r lg:border-zinc-900/10 lg:px-6 lg:pt-4 lg:pb-8 xl:w-80 lg:dark:border-white/10">
<div className="hidden lg:flex">
<Link href="/" aria-label="Home">
<Logo className="h-6" />
</Link>
</div>
<Header />
<Navigation className="hidden lg:mt-10 lg:block" />
</div>
</motion.header>
<div className="relative flex h-full flex-col px-4 pt-14 sm:px-6 lg:px-8">
<main className="flex-auto">{children}</main>
<Footer />
</div>
</div>
</SectionProvider>
)
}

View File

@@ -0,0 +1,82 @@
import Image from 'next/image'
import { Button } from '@/components/Button'
import { Heading } from '@/components/Heading'
import logoGo from '@/images/logos/go.svg'
import logoNode from '@/images/logos/node.svg'
import logoPhp from '@/images/logos/php.svg'
import logoPython from '@/images/logos/python.svg'
import logoRuby from '@/images/logos/ruby.svg'
const libraries = [
{
href: '#',
name: 'PHP',
description:
'A popular general-purpose scripting language that is especially suited to web development.',
logo: logoPhp,
},
{
href: '#',
name: 'Ruby',
description:
'A dynamic, open source programming language with a focus on simplicity and productivity.',
logo: logoRuby,
},
{
href: '#',
name: 'Node.js',
description:
'Node.js® is an open-source, cross-platform JavaScript runtime environment.',
logo: logoNode,
},
{
href: '#',
name: 'Python',
description:
'Python is a programming language that lets you work quickly and integrate systems more effectively.',
logo: logoPython,
},
{
href: '#',
name: 'Go',
description:
'An open-source programming language supported by Google with built-in concurrency.',
logo: logoGo,
},
]
export function Libraries() {
return (
<div className="my-16 xl:max-w-none">
<Heading level={2} id="official-libraries">
Official libraries
</Heading>
<div className="not-prose mt-4 grid grid-cols-1 gap-x-6 gap-y-10 border-t border-zinc-900/5 pt-10 sm:grid-cols-2 xl:max-w-none xl:grid-cols-3 dark:border-white/5">
{libraries.map((library) => (
<div key={library.name} className="flex flex-row-reverse gap-6">
<div className="flex-auto">
<h3 className="text-sm font-semibold text-zinc-900 dark:text-white">
{library.name}
</h3>
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
{library.description}
</p>
<p className="mt-4">
<Button href={library.href} variant="text" arrow="right">
Read more
</Button>
</p>
</div>
<Image
src={library.logo}
alt=""
className="h-12 w-12"
unoptimized
/>
</div>
))}
</div>
</div>
)
}

20
src/components/Logo.tsx Normal file
View File

@@ -0,0 +1,20 @@
export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<div className="flex flex-row items-center gap-x-2 font-semibold">
<svg
aria-label="Drop Logo"
className="mt-[1px] h-6 text-blue-400"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 13.5C4 11.0008 5.38798 8.76189 7.00766 7C8.43926 5.44272 10.0519 4.25811 11.0471 3.5959C11.6287 3.20893 12.3713 3.20893 12.9529 3.5959C13.9481 4.25811 15.5607 5.44272 16.9923 7C18.612 8.76189 20 11.0008 20 13.5C20 17.9183 16.4183 21.5 12 21.5C7.58172 21.5 4 17.9183 4 13.5Z"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
Developer API
</div>
)
}

View File

@@ -0,0 +1,122 @@
'use client'
import {
Dialog,
DialogBackdrop,
DialogPanel,
TransitionChild,
} from '@headlessui/react'
import { motion } from 'framer-motion'
import { Suspense, createContext, useContext } from 'react'
import { create } from 'zustand'
import { Header } from '@/components/Header'
import { Navigation } from '@/components/Navigation'
function MenuIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 10 9"
fill="none"
strokeLinecap="round"
aria-hidden="true"
{...props}
>
<path d="M.5 1h9M.5 8h9M.5 4.5h9" />
</svg>
)
}
function XIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg
viewBox="0 0 10 9"
fill="none"
strokeLinecap="round"
aria-hidden="true"
{...props}
>
<path d="m1.5 1 7 7M8.5 1l-7 7" />
</svg>
)
}
const IsInsideMobileNavigationContext = createContext(false)
function MobileNavigationDialog({
isOpen,
close,
}: {
isOpen: boolean
close: () => void
}) {
return (
<Dialog
transition
open={isOpen}
onClose={close}
className="fixed inset-0 z-50 lg:hidden"
>
<DialogBackdrop
transition
className="fixed inset-0 top-14 bg-zinc-400/20 backdrop-blur-xs data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in dark:bg-black/40"
/>
<DialogPanel>
<TransitionChild>
<Header className="data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in" />
</TransitionChild>
<TransitionChild>
<motion.div
layoutScroll
className="fixed top-14 bottom-0 left-0 w-full overflow-y-auto bg-white px-4 pt-6 pb-4 shadow-lg ring-1 shadow-zinc-900/10 ring-zinc-900/7.5 duration-500 ease-in-out data-closed:-translate-x-full min-[416px]:max-w-sm sm:px-6 sm:pb-10 dark:bg-zinc-900 dark:ring-zinc-800"
>
<Navigation />
</motion.div>
</TransitionChild>
</DialogPanel>
</Dialog>
)
}
export function useIsInsideMobileNavigation() {
return useContext(IsInsideMobileNavigationContext)
}
export const useMobileNavigationStore = create<{
isOpen: boolean
open: () => void
close: () => void
toggle: () => void
}>()((set) => ({
isOpen: false,
open: () => set({ isOpen: true }),
close: () => set({ isOpen: false }),
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
}))
export function MobileNavigation() {
let isInsideMobileNavigation = useIsInsideMobileNavigation()
let { isOpen, toggle, close } = useMobileNavigationStore()
let ToggleIcon = isOpen ? XIcon : MenuIcon
return (
<IsInsideMobileNavigationContext.Provider value={true}>
<button
type="button"
className="relative flex size-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 dark:hover:bg-white/5"
aria-label="Toggle navigation"
onClick={toggle}
>
<span className="absolute size-12 pointer-fine:hidden" />
<ToggleIcon className="w-2.5 stroke-zinc-900 dark:stroke-white" />
</button>
{!isInsideMobileNavigation && (
<Suspense fallback={null}>
<MobileNavigationDialog isOpen={isOpen} close={close} />
</Suspense>
)}
</IsInsideMobileNavigationContext.Provider>
)
}

View File

@@ -0,0 +1,294 @@
'use client'
import clsx from 'clsx'
import { AnimatePresence, motion, useIsPresent } from 'framer-motion'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useRef } from 'react'
import { useIsInsideMobileNavigation } from '@/components/MobileNavigation'
import { useSectionStore } from '@/components/SectionProvider'
import { Tag } from '@/components/Tag'
import { remToPx } from '@/lib/remToPx'
import { CloseButton } from '@headlessui/react'
interface NavGroup {
title: string
links: Array<{
title: string
href: string
}>
}
function useInitialValue<T>(value: T, condition = true) {
let initialValue = useRef(value).current
return condition ? initialValue : value
}
function TopLevelNavItem({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
return (
<li className="md:hidden">
<CloseButton
as={Link}
href={href}
className="block py-1 text-sm text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white"
>
{children}
</CloseButton>
</li>
)
}
function NavLink({
href,
children,
tag,
apilevel,
active = false,
isAnchorLink = false,
}: {
href: string
children: React.ReactNode
tag?: string
active?: boolean
isAnchorLink?: boolean
apilevel?: string
}) {
const apiLevelColor =
apilevel?.toLowerCase() == 'user'
? 'text-blue-400/90 dark:text-blue-400/70'
: apilevel?.toLowerCase() == 'system'
? 'text-yellow-600/90 dark:text-yellow-400/70'
: 'text-zinc-400 dark:text-zinc-500'
return (
<CloseButton
as={Link}
href={href}
aria-current={active ? 'page' : undefined}
className={clsx(
'flex justify-between gap-2 py-1 pr-3 text-sm transition',
isAnchorLink ? 'pl-7' : 'pl-4',
active
? 'text-zinc-900 dark:text-white'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white',
)}
>
<span className="truncate">{children}</span>
<div className="inline-flex items-center gap-x-1">
{apilevel && (
<span
className={`font-mono text-[0.625rem]/6 font-bold ${apiLevelColor}`}
>
{apilevel.toUpperCase()}
</span>
)}
{tag && apilevel && (
<span className="h-0.5 w-0.5 rounded-full bg-zinc-300 dark:bg-zinc-600" />
)}
{tag && (
<Tag variant="small" color="zinc">
{tag}
</Tag>
)}
</div>
</CloseButton>
)
}
function VisibleSectionHighlight({
group,
pathname,
}: {
group: NavGroup
pathname: string
}) {
let [sections, visibleSections] = useInitialValue(
[
useSectionStore((s) => s.sections),
useSectionStore((s) => s.visibleSections),
],
useIsInsideMobileNavigation(),
)
let isPresent = useIsPresent()
let firstVisibleSectionIndex = Math.max(
0,
[{ id: '_top' }, ...sections].findIndex(
(section) => section.id === visibleSections[0],
),
)
let itemHeight = remToPx(2)
let height = isPresent
? Math.max(1, visibleSections.length) * itemHeight
: itemHeight
let top =
group.links.findIndex((link) => link.href === pathname) * itemHeight +
firstVisibleSectionIndex * itemHeight
return (
<motion.div
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.2 } }}
exit={{ opacity: 0 }}
className="absolute inset-x-0 top-0 bg-zinc-800/2.5 will-change-transform dark:bg-white/2.5"
style={{ borderRadius: 8, height, top }}
/>
)
}
function ActivePageMarker({
group,
pathname,
}: {
group: NavGroup
pathname: string
}) {
let itemHeight = remToPx(2)
let offset = remToPx(0.25)
let activePageIndex = group.links.findIndex((link) => link.href === pathname)
let top = offset + activePageIndex * itemHeight
return (
<motion.div
layout
className="absolute left-2 h-6 w-px bg-blue-500"
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.2 } }}
exit={{ opacity: 0 }}
style={{ top }}
/>
)
}
function NavigationGroup({
group,
className,
}: {
group: NavGroup
className?: string
}) {
// If this is the mobile navigation then we always render the initial
// state, so that the state does not change during the close animation.
// The state will still update when we re-open (re-render) the navigation.
let isInsideMobileNavigation = useIsInsideMobileNavigation()
let [pathname, sections] = useInitialValue(
[usePathname(), useSectionStore((s) => s.sections)],
isInsideMobileNavigation,
)
let isActiveGroup =
group.links.findIndex((link) => link.href === pathname) !== -1
return (
<li className={clsx('relative mt-6', className)}>
<motion.h2
layout="position"
className="text-xs font-semibold text-zinc-900 dark:text-white"
>
{group.title}
</motion.h2>
<div className="relative mt-3 pl-2">
<AnimatePresence initial={!isInsideMobileNavigation}>
{isActiveGroup && (
<VisibleSectionHighlight group={group} pathname={pathname} />
)}
</AnimatePresence>
<motion.div
layout
className="absolute inset-y-0 left-2 w-px bg-zinc-900/10 dark:bg-white/5"
/>
<AnimatePresence initial={false}>
{isActiveGroup && (
<ActivePageMarker group={group} pathname={pathname} />
)}
</AnimatePresence>
<ul role="list" className="border-l border-transparent">
{group.links.map((link) => (
<motion.li key={link.href} layout="position" className="relative">
<NavLink href={link.href} active={link.href === pathname}>
{link.title}
</NavLink>
<AnimatePresence mode="popLayout" initial={false}>
{link.href === pathname && sections.length > 0 && (
<motion.ul
role="list"
initial={{ opacity: 0 }}
animate={{
opacity: 1,
transition: { delay: 0.1 },
}}
exit={{
opacity: 0,
transition: { duration: 0.15 },
}}
>
{sections.map((section) => (
<li key={section.id}>
<NavLink
href={`${link.href}#${section.id}`}
tag={section.tag}
apilevel={section.apilevel}
isAnchorLink
>
{section.title}
</NavLink>
</li>
))}
</motion.ul>
)}
</AnimatePresence>
</motion.li>
))}
</ul>
</div>
</li>
)
}
export const navigation: Array<NavGroup> = [
{
title: 'Guides',
links: [
{ title: 'Introduction', href: '/' },
{ title: 'Quickstart', href: '/guides/quickstart' },
{ title: 'Web API', href: '/guides/web' },
{ title: 'Client API', href: '/guides/client' },
{ title: 'Plugins', href: '/guides/plugins' },
],
},
{
title: 'Web API',
links: [
{ title: 'Users', href: '/web/users' },
{ title: 'Objects', href: '/web/objects' },
],
},
]
export function Navigation(props: React.ComponentPropsWithoutRef<'nav'>) {
return (
<nav {...props}>
<ul role="list">
<TopLevelNavItem href="/">API</TopLevelNavItem>
<TopLevelNavItem href="#">Documentation</TopLevelNavItem>
<TopLevelNavItem href="#">Support</TopLevelNavItem>
{navigation.map((group, groupIndex) => (
<NavigationGroup
key={group.title}
group={group}
className={groupIndex === 0 ? 'md:mt-0' : ''}
/>
))}
</ul>
</nav>
)
}

24
src/components/Prose.tsx Normal file
View File

@@ -0,0 +1,24 @@
import clsx from 'clsx'
export function Prose<T extends React.ElementType = 'div'>({
as,
className,
...props
}: Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'className'> & {
as?: T
className?: string
}) {
let Component = as ?? 'div'
return (
<Component
className={clsx(
className,
'prose dark:prose-invert',
// `html :where(& > *)` is used to select all direct children without an increase in specificity like you'd get from just `& > *`
'[html_:where(&>*)]:mx-auto [html_:where(&>*)]:max-w-2xl lg:[html_:where(&>*)]:mx-[calc(50%-min(50%,var(--container-lg)))] lg:[html_:where(&>*)]:max-w-3xl',
)}
{...props}
/>
)
}

View File

@@ -0,0 +1,186 @@
'use client'
import {
motion,
useMotionTemplate,
useMotionValue,
type MotionValue,
} from 'framer-motion'
import Link from 'next/link'
import { GridPattern } from '@/components/GridPattern'
import { Heading } from '@/components/Heading'
import { EnvelopeIcon } from '@/components/icons/EnvelopeIcon'
import { UserIcon } from '@/components/icons/UserIcon'
import { UsersIcon } from '@/components/icons/UsersIcon'
import { DocumentIcon } from '@/components/icons/DocumentIcon'
interface Resource {
href: string
name: string
description: string
icon: React.ComponentType<{ className?: string }>
pattern: Omit<
React.ComponentPropsWithoutRef<typeof GridPattern>,
'width' | 'height' | 'x'
>
}
const resources: Array<Resource> = [
{
href: '/web/users',
name: 'Users',
description:
'Learn about the user model, authentication methods, and how to interact with them.',
icon: UserIcon,
pattern: {
y: 16,
squares: [
[0, 1],
[1, 3],
],
},
},
{
href: '/web/objects',
name: 'Objects',
description:
'Learn how to access object data, and perform CRUD operations on it.',
icon: DocumentIcon,
pattern: {
y: -6,
squares: [
[-1, 2],
[1, 3],
],
},
},
{
href: '/messages',
name: 'Messages',
description:
'Learn about the message model and how to create, retrieve, update, delete, and list messages.',
icon: EnvelopeIcon,
pattern: {
y: 32,
squares: [
[0, 2],
[1, 4],
],
},
},
{
href: '/groups',
name: 'Groups',
description:
'Learn about the group model and how to create, retrieve, update, delete, and list groups.',
icon: UsersIcon,
pattern: {
y: 22,
squares: [[0, 1]],
},
},
]
function ResourceIcon({ icon: Icon }: { icon: Resource['icon'] }) {
return (
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-zinc-900/5 ring-1 ring-zinc-900/25 backdrop-blur-[2px] transition duration-300 group-hover:bg-white/50 group-hover:ring-zinc-900/25 dark:bg-white/7.5 dark:ring-white/15 dark:group-hover:bg-blue-300/10 dark:group-hover:ring-blue-400">
<Icon className="h-5 w-5 fill-zinc-700/10 stroke-zinc-700 transition-colors duration-300 group-hover:stroke-zinc-900 dark:fill-white/10 dark:stroke-zinc-400 dark:group-hover:fill-blue-300/10 dark:group-hover:stroke-blue-400" />
</div>
)
}
function ResourcePattern({
mouseX,
mouseY,
...gridProps
}: Resource['pattern'] & {
mouseX: MotionValue<number>
mouseY: MotionValue<number>
}) {
let maskImage = useMotionTemplate`radial-gradient(180px at ${mouseX}px ${mouseY}px, white, transparent)`
let style = { maskImage, WebkitMaskImage: maskImage }
return (
<div className="pointer-events-none">
<div className="absolute inset-0 rounded-2xl mask-[linear-gradient(white,transparent)] transition duration-300 group-hover:opacity-50">
<GridPattern
width={72}
height={56}
x="50%"
className="absolute inset-x-0 inset-y-[-30%] h-[160%] w-full skew-y-[-18deg] fill-black/2 stroke-black/5 dark:fill-white/1 dark:stroke-white/2.5"
{...gridProps}
/>
</div>
<motion.div
className="absolute inset-0 rounded-2xl bg-linear-to-r from-blue-600/10 to-blue-400/10 opacity-0 transition duration-300 group-hover:opacity-100 dark:from-[#202D2E] dark:to-[#303428]"
style={style}
/>
<motion.div
className="absolute inset-0 rounded-2xl opacity-0 mix-blend-overlay transition duration-300 group-hover:opacity-100"
style={style}
>
<GridPattern
width={72}
height={56}
x="50%"
className="absolute inset-x-0 inset-y-[-30%] h-[160%] w-full skew-y-[-18deg] fill-black/50 stroke-black/70 dark:fill-white/2.5 dark:stroke-white/10"
{...gridProps}
/>
</motion.div>
</div>
)
}
function Resource({ resource }: { resource: Resource }) {
let mouseX = useMotionValue(0)
let mouseY = useMotionValue(0)
function onMouseMove({
currentTarget,
clientX,
clientY,
}: React.MouseEvent<HTMLDivElement>) {
let { left, top } = currentTarget.getBoundingClientRect()
mouseX.set(clientX - left)
mouseY.set(clientY - top)
}
return (
<div
key={resource.href}
onMouseMove={onMouseMove}
className="group relative flex rounded-2xl bg-zinc-50 transition-shadow hover:shadow-md hover:shadow-zinc-900/5 dark:bg-white/2.5 dark:hover:shadow-black/5"
>
<ResourcePattern {...resource.pattern} mouseX={mouseX} mouseY={mouseY} />
<div className="absolute inset-0 rounded-2xl ring-1 ring-zinc-900/7.5 ring-inset group-hover:ring-zinc-900/10 dark:ring-white/10 dark:group-hover:ring-white/20" />
<div className="relative rounded-2xl px-4 pt-16 pb-4">
<ResourceIcon icon={resource.icon} />
<h3 className="mt-4 text-sm/7 font-semibold text-zinc-900 dark:text-white">
<Link href={resource.href}>
<span className="absolute inset-0 rounded-2xl" />
{resource.name}
</Link>
</h3>
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
{resource.description}
</p>
</div>
</div>
)
}
export function Resources() {
return (
<div className="my-16 xl:max-w-none">
<Heading level={2} id="resources">
Web API
</Heading>
<div className="not-prose mt-4 grid grid-cols-1 gap-8 border-t border-zinc-900/5 pt-10 sm:grid-cols-2 xl:grid-cols-4 dark:border-white/5">
{resources.map((resource) => (
<Resource key={resource.href} resource={resource} />
))}
</div>
</div>
)
}

493
src/components/Search.tsx Normal file
View File

@@ -0,0 +1,493 @@
'use client'
import {
createAutocomplete,
type AutocompleteApi,
type AutocompleteCollection,
type AutocompleteState,
} from '@algolia/autocomplete-core'
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
import clsx from 'clsx'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import {
Fragment,
Suspense,
forwardRef,
useCallback,
useEffect,
useId,
useRef,
useState,
} from 'react'
import Highlighter from 'react-highlight-words'
import { navigation } from '@/components/Navigation'
import { type Result } from '@/mdx/search.mjs'
import { useMobileNavigationStore } from './MobileNavigation'
type EmptyObject = Record<string, never>
type Autocomplete = AutocompleteApi<
Result,
React.SyntheticEvent,
React.MouseEvent,
React.KeyboardEvent
>
function useAutocomplete({ onNavigate }: { onNavigate: () => void }) {
let id = useId()
let router = useRouter()
let [autocompleteState, setAutocompleteState] = useState<
AutocompleteState<Result> | EmptyObject
>({})
function navigate({ itemUrl }: { itemUrl?: string }) {
if (itemUrl) {
router.push(itemUrl)
}
onNavigate()
}
let [autocomplete] = useState<Autocomplete>(() =>
createAutocomplete<
Result,
React.SyntheticEvent,
React.MouseEvent,
React.KeyboardEvent
>({
id,
placeholder: 'Find something...',
defaultActiveItemId: 0,
onStateChange({ state }) {
setAutocompleteState(state)
},
shouldPanelOpen({ state }) {
return state.query !== ''
},
navigator: {
navigate,
},
getSources({ query }) {
return import('@/mdx/search.mjs').then(({ search }) => {
return [
{
sourceId: 'documentation',
getItems() {
return search(query, { limit: 5 })
},
getItemUrl({ item }) {
return item.url
},
onSelect: navigate,
},
]
})
},
}),
)
return { autocomplete, autocompleteState }
}
function SearchIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12.01 12a4.25 4.25 0 1 0-6.02-6 4.25 4.25 0 0 0 6.02 6Zm0 0 3.24 3.25"
/>
</svg>
)
}
function NoResultsIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12.01 12a4.237 4.237 0 0 0 1.24-3c0-.62-.132-1.207-.37-1.738M12.01 12A4.237 4.237 0 0 1 9 13.25c-.635 0-1.237-.14-1.777-.388M12.01 12l3.24 3.25m-3.715-9.661a4.25 4.25 0 0 0-5.975 5.908M4.5 15.5l11-11"
/>
</svg>
)
}
function LoadingIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
let id = useId()
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<circle cx="10" cy="10" r="5.5" strokeLinejoin="round" />
<path
stroke={`url(#${id})`}
strokeLinecap="round"
strokeLinejoin="round"
d="M15.5 10a5.5 5.5 0 1 0-5.5 5.5"
/>
<defs>
<linearGradient
id={id}
x1="13"
x2="9.5"
y1="9"
y2="15"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="currentColor" />
<stop offset="1" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
)
}
function HighlightQuery({ text, query }: { text: string; query: string }) {
return (
<Highlighter
highlightClassName="underline bg-transparent text-blue-500"
searchWords={[query]}
autoEscape={true}
textToHighlight={text}
/>
)
}
function SearchResult({
result,
resultIndex,
autocomplete,
collection,
query,
}: {
result: Result
resultIndex: number
autocomplete: Autocomplete
collection: AutocompleteCollection<Result>
query: string
}) {
let id = useId()
let sectionTitle = navigation.find((section) =>
section.links.find((link) => link.href === result.url.split('#')[0]),
)?.title
let hierarchy = [sectionTitle, result.pageTitle].filter(
(x): x is string => typeof x === 'string',
)
return (
<li
className={clsx(
'group block cursor-default px-4 py-3 aria-selected:bg-zinc-50 dark:aria-selected:bg-zinc-800/50',
resultIndex > 0 && 'border-t border-zinc-100 dark:border-zinc-800',
)}
aria-labelledby={`${id}-hierarchy ${id}-title`}
{...autocomplete.getItemProps({
item: result,
source: collection.source,
})}
>
<div
id={`${id}-title`}
aria-hidden="true"
className="text-sm font-medium text-zinc-900 group-aria-selected:text-blue-500 dark:text-white"
>
<HighlightQuery text={result.title} query={query} />
</div>
{hierarchy.length > 0 && (
<div
id={`${id}-hierarchy`}
aria-hidden="true"
className="mt-1 truncate text-2xs whitespace-nowrap text-zinc-500"
>
{hierarchy.map((item, itemIndex, items) => (
<Fragment key={itemIndex}>
<HighlightQuery text={item} query={query} />
<span
className={
itemIndex === items.length - 1
? 'sr-only'
: 'mx-2 text-zinc-300 dark:text-zinc-700'
}
>
/
</span>
</Fragment>
))}
</div>
)}
</li>
)
}
function SearchResults({
autocomplete,
query,
collection,
}: {
autocomplete: Autocomplete
query: string
collection: AutocompleteCollection<Result>
}) {
if (collection.items.length === 0) {
return (
<div className="p-6 text-center">
<NoResultsIcon className="mx-auto h-5 w-5 stroke-zinc-900 dark:stroke-zinc-600" />
<p className="mt-2 text-xs text-zinc-700 dark:text-zinc-400">
Nothing found for{' '}
<strong className="font-semibold break-words text-zinc-900 dark:text-white">
&lsquo;{query}&rsquo;
</strong>
. Please try again.
</p>
</div>
)
}
return (
<ul {...autocomplete.getListProps()}>
{collection.items.map((result, resultIndex) => (
<SearchResult
key={result.url}
result={result}
resultIndex={resultIndex}
autocomplete={autocomplete}
collection={collection}
query={query}
/>
))}
</ul>
)
}
const SearchInput = forwardRef<
React.ElementRef<'input'>,
{
autocomplete: Autocomplete
autocompleteState: AutocompleteState<Result> | EmptyObject
onClose: () => void
}
>(function SearchInput({ autocomplete, autocompleteState, onClose }, inputRef) {
let inputProps = autocomplete.getInputProps({ inputElement: null })
return (
<div className="group relative flex h-12">
<SearchIcon className="pointer-events-none absolute top-0 left-3 h-full w-5 stroke-zinc-500" />
<input
ref={inputRef}
data-autofocus
className={clsx(
'flex-auto appearance-none bg-transparent pl-10 text-zinc-900 outline-hidden placeholder:text-zinc-500 focus:w-full focus:flex-none sm:text-sm dark:text-white [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden [&::-webkit-search-results-button]:hidden [&::-webkit-search-results-decoration]:hidden',
autocompleteState.status === 'stalled' ? 'pr-11' : 'pr-4',
)}
{...inputProps}
onKeyDown={(event) => {
if (
event.key === 'Escape' &&
!autocompleteState.isOpen &&
autocompleteState.query === ''
) {
// In Safari, closing the dialog with the escape key can sometimes cause the scroll position to jump to the
// bottom of the page. This is a workaround for that until we can figure out a proper fix in Headless UI.
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur()
}
onClose()
} else {
inputProps.onKeyDown(event)
}
}}
/>
{autocompleteState.status === 'stalled' && (
<div className="absolute inset-y-0 right-3 flex items-center">
<LoadingIcon className="h-5 w-5 animate-spin stroke-zinc-200 text-zinc-900 dark:stroke-zinc-800 dark:text-blue-400" />
</div>
)}
</div>
)
})
function SearchDialog({
open,
setOpen,
className,
onNavigate = () => {},
}: {
open: boolean
setOpen: (open: boolean) => void
className?: string
onNavigate?: () => void
}) {
let formRef = useRef<React.ElementRef<'form'>>(null)
let panelRef = useRef<React.ElementRef<'div'>>(null)
let inputRef = useRef<React.ElementRef<typeof SearchInput>>(null)
let { autocomplete, autocompleteState } = useAutocomplete({
onNavigate() {
onNavigate()
setOpen(false)
},
})
let pathname = usePathname()
let searchParams = useSearchParams()
useEffect(() => {
setOpen(false)
}, [pathname, searchParams, setOpen])
useEffect(() => {
if (open) {
return
}
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault()
setOpen(true)
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
}, [open, setOpen])
return (
<Dialog
open={open}
onClose={() => {
setOpen(false)
autocomplete.setQuery('')
}}
className={clsx('fixed inset-0 z-50', className)}
>
<DialogBackdrop
transition
className="fixed inset-0 bg-zinc-400/25 backdrop-blur-xs data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in dark:bg-black/40"
/>
<div className="fixed inset-0 overflow-y-auto px-4 py-4 sm:px-6 sm:py-20 md:py-32 lg:px-8 lg:py-[15vh]">
<DialogPanel
transition
className="mx-auto transform-gpu overflow-hidden rounded-lg bg-zinc-50 shadow-xl ring-1 ring-zinc-900/7.5 data-closed:scale-95 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:max-w-xl dark:bg-zinc-900 dark:ring-zinc-800"
>
<div {...autocomplete.getRootProps({})}>
<form
ref={formRef}
{...autocomplete.getFormProps({
inputElement: inputRef.current,
})}
>
<SearchInput
ref={inputRef}
autocomplete={autocomplete}
autocompleteState={autocompleteState}
onClose={() => setOpen(false)}
/>
<div
ref={panelRef}
className="border-t border-zinc-200 bg-white empty:hidden dark:border-zinc-100/5 dark:bg-white/2.5"
{...autocomplete.getPanelProps({})}
>
{autocompleteState.isOpen && (
<SearchResults
autocomplete={autocomplete}
query={autocompleteState.query}
collection={autocompleteState.collections[0]}
/>
)}
</div>
</form>
</div>
</DialogPanel>
</div>
</Dialog>
)
}
function useSearchProps() {
let buttonRef = useRef<React.ElementRef<'button'>>(null)
let [open, setOpen] = useState(false)
return {
buttonProps: {
ref: buttonRef,
onClick() {
setOpen(true)
},
},
dialogProps: {
open,
setOpen: useCallback(
(open: boolean) => {
let { width = 0, height = 0 } =
buttonRef.current?.getBoundingClientRect() ?? {}
if (!open || (width !== 0 && height !== 0)) {
setOpen(open)
}
},
[setOpen],
),
},
}
}
export function Search() {
let [modifierKey, setModifierKey] = useState<string>()
let { buttonProps, dialogProps } = useSearchProps()
useEffect(() => {
setModifierKey(
/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl ',
)
}, [])
return (
<div className="hidden lg:block lg:max-w-md lg:flex-auto">
<button
type="button"
className="hidden h-8 w-full items-center gap-2 rounded-full bg-white pr-3 pl-2 text-sm text-zinc-500 ring-1 ring-zinc-900/10 transition hover:ring-zinc-900/20 lg:flex dark:bg-white/5 dark:text-zinc-400 dark:ring-white/10 dark:ring-inset dark:hover:ring-white/20"
{...buttonProps}
>
<SearchIcon className="h-5 w-5 stroke-current" />
Find something...
<kbd className="ml-auto text-2xs text-zinc-400 dark:text-zinc-500">
<kbd className="font-sans">{modifierKey}</kbd>
<kbd className="font-sans">K</kbd>
</kbd>
</button>
<Suspense fallback={null}>
<SearchDialog className="hidden lg:block" {...dialogProps} />
</Suspense>
</div>
)
}
export function MobileSearch() {
let { close } = useMobileNavigationStore()
let { buttonProps, dialogProps } = useSearchProps()
return (
<div className="contents lg:hidden">
<button
type="button"
className="relative flex size-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 lg:hidden dark:hover:bg-white/5"
aria-label="Find something..."
{...buttonProps}
>
<span className="absolute size-12 pointer-fine:hidden" />
<SearchIcon className="h-5 w-5 stroke-zinc-900 dark:stroke-white" />
</button>
<Suspense fallback={null}>
<SearchDialog
className="lg:hidden"
onNavigate={close}
{...dialogProps}
/>
</Suspense>
</div>
)
}

View File

@@ -0,0 +1,154 @@
'use client'
import {
createContext,
useContext,
useEffect,
useLayoutEffect,
useState,
} from 'react'
import { createStore, useStore, type StoreApi } from 'zustand'
import { remToPx } from '@/lib/remToPx'
export interface Section {
id: string
title: string
offsetRem?: number
tag?: string
apilevel?: string
headingRef?: React.RefObject<HTMLHeadingElement | null>
}
interface SectionState {
sections: Array<Section>
visibleSections: Array<string>
setVisibleSections: (visibleSections: Array<string>) => void
registerHeading: ({
id,
ref,
offsetRem,
}: {
id: string
ref: React.RefObject<HTMLHeadingElement | null>
offsetRem: number
}) => void
}
function createSectionStore(sections: Array<Section>) {
return createStore<SectionState>()((set) => ({
sections,
visibleSections: [],
setVisibleSections: (visibleSections) =>
set((state) =>
state.visibleSections.join() === visibleSections.join()
? {}
: { visibleSections },
),
registerHeading: ({ id, ref, offsetRem }) =>
set((state) => {
return {
sections: state.sections.map((section) => {
if (section.id === id) {
return {
...section,
headingRef: ref,
offsetRem,
}
}
return section
}),
}
}),
}))
}
function useVisibleSections(sectionStore: StoreApi<SectionState>) {
let setVisibleSections = useStore(sectionStore, (s) => s.setVisibleSections)
let sections = useStore(sectionStore, (s) => s.sections)
useEffect(() => {
function checkVisibleSections() {
let { innerHeight, scrollY } = window
let newVisibleSections = []
for (
let sectionIndex = 0;
sectionIndex < sections.length;
sectionIndex++
) {
let { id, headingRef, offsetRem = 0 } = sections[sectionIndex]
if (!headingRef?.current) {
continue
}
let offset = remToPx(offsetRem)
let top = headingRef.current.getBoundingClientRect().top + scrollY
if (sectionIndex === 0 && top - offset > scrollY) {
newVisibleSections.push('_top')
}
let nextSection = sections[sectionIndex + 1]
let bottom =
(nextSection?.headingRef?.current?.getBoundingClientRect().top ??
Infinity) +
scrollY -
remToPx(nextSection?.offsetRem ?? 0)
if (
(top > scrollY && top < scrollY + innerHeight) ||
(bottom > scrollY && bottom < scrollY + innerHeight) ||
(top <= scrollY && bottom >= scrollY + innerHeight)
) {
newVisibleSections.push(id)
}
}
setVisibleSections(newVisibleSections)
}
let raf = window.requestAnimationFrame(() => checkVisibleSections())
window.addEventListener('scroll', checkVisibleSections, { passive: true })
window.addEventListener('resize', checkVisibleSections)
return () => {
window.cancelAnimationFrame(raf)
window.removeEventListener('scroll', checkVisibleSections)
window.removeEventListener('resize', checkVisibleSections)
}
}, [setVisibleSections, sections])
}
const SectionStoreContext = createContext<StoreApi<SectionState> | null>(null)
const useIsomorphicLayoutEffect =
typeof window === 'undefined' ? useEffect : useLayoutEffect
export function SectionProvider({
sections,
children,
}: {
sections: Array<Section>
children: React.ReactNode
}) {
let [sectionStore] = useState(() => createSectionStore(sections))
useVisibleSections(sectionStore)
useIsomorphicLayoutEffect(() => {
sectionStore.setState({ sections })
}, [sectionStore, sections])
return (
<SectionStoreContext.Provider value={sectionStore}>
{children}
</SectionStoreContext.Provider>
)
}
export function useSectionStore<T>(selector: (state: SectionState) => T) {
let store = useContext(SectionStoreContext)
return useStore(store!, selector)
}

63
src/components/Tag.tsx Normal file
View File

@@ -0,0 +1,63 @@
import clsx from 'clsx'
const variantStyles = {
small: '',
medium: 'rounded-lg px-1.5 ring-1 ring-inset',
}
const colorStyles = {
blue: {
small: 'text-blue-500 dark:text-blue-400',
medium:
'ring-blue-300 dark:ring-blue-400/30 bg-blue-400/10 text-blue-500 dark:text-blue-400',
},
emerald: {
small: 'text-emerald-500',
medium:
'ring-emerald-300 bg-emerald-400/10 text-emerald-500 dark:ring-emerald-400/30 dark:bg-emerald-400/10 dark:text-emerald-400',
},
amber: {
small: 'text-amber-500',
medium:
'ring-amber-300 bg-amber-400/10 text-amber-500 dark:ring-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400',
},
rose: {
small: 'text-red-500 dark:text-rose-500',
medium:
'ring-rose-200 bg-rose-50 text-red-500 dark:ring-rose-500/20 dark:bg-rose-400/10 dark:text-rose-400',
},
zinc: {
small: 'text-zinc-400 dark:text-zinc-500',
medium:
'ring-zinc-200 bg-zinc-50 text-zinc-500 dark:ring-zinc-500/20 dark:bg-zinc-400/10 dark:text-zinc-400',
},
}
const valueColorMap = {
GET: 'blue',
POST: 'emerald',
PUT: 'amber',
DELETE: 'rose',
} as Record<string, keyof typeof colorStyles>
export function Tag({
children,
variant = 'medium',
color = valueColorMap[children] ?? 'blue',
}: {
children: keyof typeof valueColorMap & (string | {})
variant?: keyof typeof variantStyles
color?: keyof typeof colorStyles
}) {
return (
<span
className={clsx(
'font-mono text-[0.625rem]/6 font-semibold',
variantStyles[variant],
colorStyles[color][variant],
)}
>
{children}
</span>
)
}

View File

@@ -0,0 +1,45 @@
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
function SunIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<path d="M12.5 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Z" />
<path
strokeLinecap="round"
d="M10 5.5v-1M13.182 6.818l.707-.707M14.5 10h1M13.182 13.182l.707.707M10 15.5v-1M6.11 13.889l.708-.707M4.5 10h1M6.11 6.111l.708.707"
/>
</svg>
)
}
function MoonIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<path d="M15.224 11.724a5.5 5.5 0 0 1-6.949-6.949 5.5 5.5 0 1 0 6.949 6.949Z" />
</svg>
)
}
export function ThemeToggle() {
let { resolvedTheme, setTheme } = useTheme()
let otherTheme = resolvedTheme === 'dark' ? 'light' : 'dark'
let [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
return (
<button
type="button"
className="flex size-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 dark:hover:bg-white/5"
aria-label={mounted ? `Switch to ${otherTheme} theme` : 'Toggle theme'}
onClick={() => setTheme(otherTheme)}
>
<span className="absolute size-12 pointer-fine:hidden" />
<SunIcon className="h-5 w-5 stroke-zinc-900 dark:hidden" />
<MoonIcon className="hidden h-5 w-5 stroke-white dark:block" />
</button>
)
}

View File

@@ -0,0 +1,17 @@
export function BellIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.438 8.063a5.563 5.563 0 0 1 11.125 0v2.626c0 1.182.34 2.34.982 3.332L17.5 15.5h-15l.955-1.479c.641-.993.982-2.15.982-3.332V8.062Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 15.5v0a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v0"
/>
</svg>
)
}

View File

@@ -0,0 +1,11 @@
export function BoltIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 11.5 10 2v5.5a1 1 0 0 0 1 1h4.5L10 18v-5.5a1 1 0 0 0-1-1H4.5Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,17 @@
export function BookIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="m10 5.5-7.5-3v12l7.5 3m0-12 7.5-3v12l-7.5 3m0-12v12"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m17.5 2.5-7.5 3v12l7.5-3v-12Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,23 @@
export function CalendarIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M2.5 6.5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-11a2 2 0 0 1-2-2v-9Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.5 6.5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v2h-15v-2Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M5.5 5.5v-3M14.5 5.5v-3"
/>
</svg>
)
}

View File

@@ -0,0 +1,15 @@
export function CartIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeWidth="0"
d="M5.98 11.288 3.5 5.5h14l-2.48 5.788A2 2 0 0 1 13.18 12.5H7.82a2 2 0 0 1-1.838-1.212Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m3.5 5.5 2.48 5.788A2 2 0 0 0 7.82 12.5h5.362a2 2 0 0 0 1.839-1.212L17.5 5.5h-14Zm0 0-1-2M6.5 14.5a1 1 0 1 1 0 2 1 1 0 0 1 0-2ZM14.5 14.5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,17 @@
export function ChatBubbleIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10 16.5c4.142 0 7.5-3.134 7.5-7s-3.358-7-7.5-7c-4.142 0-7.5 3.134-7.5 7 0 1.941.846 3.698 2.214 4.966L3.5 17.5c2.231 0 3.633-.553 4.513-1.248A8.014 8.014 0 0 0 10 16.5Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 8.5h5M8.5 11.5h3"
/>
</svg>
)
}

View File

@@ -0,0 +1,17 @@
export function CheckIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10 1.5a8.5 8.5 0 1 1 0 17 8.5 8.5 0 0 1 0-17Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="m7.5 10.5 2 2c1-3.5 3-5 3-5"
/>
</svg>
)
}

View File

@@ -0,0 +1,19 @@
export function ChevronRightLeftIcon(
props: React.ComponentPropsWithoutRef<'svg'>,
) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M1.5 10A6.5 6.5 0 0 1 8 3.5h4a6.5 6.5 0 1 1 0 13H8A6.5 6.5 0 0 1 1.5 10Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="m7.5 7.5-3 2.5 3 2.5M12.5 7.5l3 2.5-3 2.5"
/>
</svg>
)
}

View File

@@ -0,0 +1,17 @@
export function ClipboardIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M3.5 6v10a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1l-.447.894A2 2 0 0 1 11.263 6H8.737a2 2 0 0 1-1.789-1.106L6.5 4h-1a2 2 0 0 0-2 2Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m13.5 4-.447.894A2 2 0 0 1 11.263 6H8.737a2 2 0 0 1-1.789-1.106L6.5 4l.724-1.447A1 1 0 0 1 8.118 2h3.764a1 1 0 0 1 .894.553L13.5 4Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,19 @@
export function CogIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeWidth="0"
fillRule="evenodd"
d="M11.063 1.5H8.937l-.14 1.128c-.086.682-.61 1.22-1.246 1.484-.634.264-1.37.247-1.912-.175l-.898-.699-1.503 1.503.699.898c.422.543.44 1.278.175 1.912-.264.635-.802 1.16-1.484 1.245L1.5 8.938v2.124l1.128.142c.682.085 1.22.61 1.484 1.244.264.635.247 1.37-.175 1.913l-.699.898 1.503 1.503.898-.699c.543-.422 1.278-.44 1.912-.175.635.264 1.16.801 1.245 1.484l.142 1.128h2.124l.142-1.128c.085-.683.61-1.22 1.244-1.484.635-.264 1.37-.247 1.913.175l.898.699 1.503-1.503-.699-.898c-.422-.543-.44-1.278-.175-1.913.264-.634.801-1.16 1.484-1.245l1.128-.14V8.937l-1.128-.14c-.683-.086-1.22-.611-1.484-1.246-.264-.634-.247-1.37.175-1.912l.699-.898-1.503-1.503-.898.699c-.543.422-1.278.44-1.913.175-.634-.264-1.16-.802-1.244-1.484L11.062 1.5ZM10 12.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z"
clipRule="evenodd"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M8.938 1.5h2.124l.142 1.128c.085.682.61 1.22 1.244 1.484v0c.635.264 1.37.247 1.913-.175l.898-.699 1.503 1.503-.699.898c-.422.543-.44 1.278-.175 1.912v0c.264.635.801 1.16 1.484 1.245l1.128.142v2.124l-1.128.142c-.683.085-1.22.61-1.484 1.244v0c-.264.635-.247 1.37.175 1.913l.699.898-1.503 1.503-.898-.699c-.543-.422-1.278-.44-1.913-.175v0c-.634.264-1.16.801-1.245 1.484l-.14 1.128H8.937l-.14-1.128c-.086-.683-.611-1.22-1.246-1.484v0c-.634-.264-1.37-.247-1.912.175l-.898.699-1.503-1.503.699-.898c.422-.543.44-1.278.175-1.913v0c-.264-.634-.802-1.16-1.484-1.245l-1.128-.14V8.937l1.128-.14c.682-.086 1.22-.61 1.484-1.246v0c.264-.634.247-1.37-.175-1.912l-.699-.898 1.503-1.503.898.699c.543.422 1.278.44 1.912.175v0c.635-.264 1.16-.802 1.245-1.484L8.938 1.5Z"
/>
<circle cx="10" cy="10" r="2.5" fill="none" />
</svg>
)
}

View File

@@ -0,0 +1,17 @@
export function CopyIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M14.5 5.5v-1a2 2 0 0 0-2-2h-8a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h1"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5.5 7.5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-8a2 2 0 0 1-2-2v-8Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,17 @@
export function DocumentIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.5 4.5v11a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-8h-5v-5h-6a2 2 0 0 0-2 2Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="m11.5 2.5 5 5"
/>
</svg>
)
}

View File

@@ -0,0 +1,17 @@
export function EnvelopeIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M2.5 5.5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v8a3 3 0 0 1-3 3h-9a3 3 0 0 1-3-3v-8Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10 10 4.526 5.256c-.7-.607-.271-1.756.655-1.756h9.638c.926 0 1.355 1.15.655 1.756L10 10Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,17 @@
export function FaceSmileIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10 1.5a8.5 8.5 0 1 1 0 17 8.5 8.5 0 0 1 0-17Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 6.5v2M12.5 6.5v2M5.5 11.5s1 3 4.5 3 4.5-3 4.5-3"
/>
</svg>
)
}

View File

@@ -0,0 +1,22 @@
export function FolderIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M17.5 15.5v-8a2 2 0 0 0-2-2h-2.93a2 2 0 0 1-1.664-.89l-.812-1.22A2 2 0 0 0 8.43 2.5H4.5a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2Z"
/>
<path
strokeWidth="0"
d="M8.43 2.5H4.5a2 2 0 0 0-2 2v1h9l-1.406-2.11A2 2 0 0 0 8.43 2.5Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="m11.5 5.5-1.406-2.11A2 2 0 0 0 8.43 2.5H4.5a2 2 0 0 0-2 2v1h9Zm0 0h2"
/>
</svg>
)
}

View File

@@ -0,0 +1,12 @@
export function LinkIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="m5.056 11.5-1.221-1.222a4.556 4.556 0 0 1 6.443-6.443L11.5 5.056M7.5 7.5l5 5m2.444-4 1.222 1.222a4.556 4.556 0 0 1-6.444 6.444L8.5 14.944"
/>
</svg>
)
}

View File

@@ -0,0 +1,17 @@
export function ListIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.5 4.5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2h-11a2 2 0 0 1-2-2v-11Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M6.5 6.5h7M6.5 13.5h7M6.5 10h7"
/>
</svg>
)
}

View File

@@ -0,0 +1,15 @@
export function MagnifyingGlassIcon(
props: React.ComponentPropsWithoutRef<'svg'>,
) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path strokeWidth="0" d="M2.5 8.5a6 6 0 1 1 12 0 6 6 0 0 1-12 0Z" />
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="m13 13 4.5 4.5m-9-3a6 6 0 1 1 0-12 6 6 0 0 1 0 12Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,19 @@
export function MapPinIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeWidth="0"
fillRule="evenodd"
clipRule="evenodd"
d="M10 2.5A5.5 5.5 0 0 0 4.5 8c0 3.038 5.5 9.5 5.5 9.5s5.5-6.462 5.5-9.5A5.5 5.5 0 0 0 10 2.5Zm0 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 8a5.5 5.5 0 1 1 11 0c0 3.038-5.5 9.5-5.5 9.5S4.5 11.038 4.5 8Z"
/>
<circle cx="10" cy="8" r="1.5" fill="none" />
</svg>
)
}

View File

@@ -0,0 +1,16 @@
export function PackageIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeWidth="0"
d="m10 9.5-7.5-4v9l7.5 4v-9ZM10 9.5l7.5-4v9l-7.5 4v-9Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="m2.5 5.5 7.5 4m-7.5-4v9l7.5 4m-7.5-13 7.5-4 7.5 4m-7.5 4v9m0-9 7.5-4m-7.5 13 7.5-4v-9m-11 6 .028-3.852L13.5 3.5"
/>
</svg>
)
}

View File

@@ -0,0 +1,19 @@
export function PaperAirplaneIcon(
props: React.ComponentPropsWithoutRef<'svg'>,
) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M17 3L1 9L8 12M17 3L11 19L8 12M17 3L8 12"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11 19L8 12L17 3L11 19Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,12 @@
export function PaperClipIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="m15.56 7.375-3.678-3.447c-2.032-1.904-5.326-1.904-7.358 0s-2.032 4.99 0 6.895l6.017 5.639c1.477 1.384 3.873 1.384 5.35 0 1.478-1.385 1.478-3.63 0-5.015L10.21 6.122a1.983 1.983 0 0 0-2.676 0 1.695 1.695 0 0 0 0 2.507l4.013 3.76"
/>
</svg>
)
}

View File

@@ -0,0 +1,17 @@
export function ShapesIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M2.5 7.5v-4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1ZM11.5 16.5v-4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m2.5 17.5 3-6 3 6h-6ZM14.5 2.5a3 3 0 1 1 0 6 3 3 0 0 1 0-6Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,11 @@
export function ShirtIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12.5 1.5s0 2-2.5 2-2.5-2-2.5-2h-2L2.207 4.793a1 1 0 0 0 0 1.414L4.5 8.5v10h11v-10l2.293-2.293a1 1 0 0 0 0-1.414L14.5 1.5h-2Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,17 @@
export function SquaresPlusIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8.5 4.5v2a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2ZM8.5 13.5v2a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2ZM17.5 4.5v2a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M14.5 11.5v6M17.5 14.5h-6"
/>
</svg>
)
}

View File

@@ -0,0 +1,19 @@
export function TagIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeWidth="0"
fillRule="evenodd"
clipRule="evenodd"
d="M3 8.69499V3H8.69499C9.18447 3 9.65389 3.19444 10 3.54055L16.4594 10C17.1802 10.7207 17.1802 11.8893 16.4594 12.61L12.61 16.4594C11.8893 17.1802 10.7207 17.1802 10 16.4594L3.54055 10C3.19444 9.65389 3 9.18447 3 8.69499ZM7 8.5C7.82843 8.5 8.5 7.82843 8.5 7C8.5 6.17157 7.82843 5.5 7 5.5C6.17157 5.5 5.5 6.17157 5.5 7C5.5 7.82843 6.17157 8.5 7 8.5Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M3 3V8.69499C3 9.18447 3.19444 9.65389 3.54055 10L10 16.4594C10.7207 17.1802 11.8893 17.1802 12.61 16.4594L16.4594 12.61C17.1802 11.8893 17.1802 10.7207 16.4594 10L10 3.54055C9.65389 3.19444 9.18447 3 8.69499 3H3Z"
/>
<circle cx="7" cy="7" r="1.5" fill="none" />
</svg>
)
}

View File

@@ -0,0 +1,24 @@
export function UserIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
strokeWidth="0"
fillRule="evenodd"
clipRule="evenodd"
d="M10 .5a9.5 9.5 0 0 1 5.598 17.177C14.466 15.177 12.383 13.5 10 13.5s-4.466 1.677-5.598 4.177A9.5 9.5 0 0 1 10 .5ZM12.5 8a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M10 .5a9.5 9.5 0 0 1 5.598 17.177A9.458 9.458 0 0 1 10 19.5a9.458 9.458 0 0 1-5.598-1.823A9.5 9.5 0 0 1 10 .5Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M4.402 17.677C5.534 15.177 7.617 13.5 10 13.5s4.466 1.677 5.598 4.177M10 5.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z"
/>
</svg>
)
}

View File

@@ -0,0 +1,28 @@
export function UsersIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M10.046 16H1.955a.458.458 0 0 1-.455-.459C1.5 13.056 3.515 11 6 11h.5"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7.5 15.454C7.5 12.442 9.988 10 13 10s5.5 2.442 5.5 5.454a.545.545 0 0 1-.546.546H8.045a.545.545 0 0 1-.545-.546Z"
/>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
d="M6.5 4a2 2 0 1 1 0 4 2 2 0 0 1 0-4Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13 2a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z"
/>
</svg>
)
}

127
src/components/mdx.tsx Normal file
View File

@@ -0,0 +1,127 @@
import clsx from 'clsx'
import Link from 'next/link'
import { Feedback } from '@/components/Feedback'
import { Heading } from '@/components/Heading'
import { Prose } from '@/components/Prose'
export const a = Link
export { Button } from '@/components/Button'
export { CodeGroup, Code as code, Pre as pre } from '@/components/Code'
export function wrapper({ children }: { children: React.ReactNode }) {
return (
<article className="flex h-full flex-col pt-16 pb-10">
<Prose className="flex-auto">{children}</Prose>
<footer className="mx-auto mt-16 w-full max-w-2xl lg:max-w-5xl">
<Feedback />
</footer>
</article>
)
}
export const h2 = function H2(
props: Omit<React.ComponentPropsWithoutRef<typeof Heading>, 'level'>,
) {
return <Heading level={2} {...props} />
}
function InfoIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" {...props}>
<circle cx="8" cy="8" r="8" strokeWidth="0" />
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="1.5"
d="M6.75 7.75h1.5v3.5"
/>
<circle cx="8" cy="4" r=".5" fill="none" />
</svg>
)
}
export function Note({ children }: { children: React.ReactNode }) {
return (
<div className="my-6 flex gap-2.5 rounded-2xl border border-blue-500/20 bg-blue-50/50 p-4 text-sm/6 text-blue-900 dark:border-blue-500/30 dark:bg-blue-500/5 dark:text-blue-200 dark:[--tw-prose-links-hover:var(--color-blue-300)] dark:[--tw-prose-links:var(--color-white)]">
<InfoIcon className="mt-1 h-4 w-4 flex-none fill-blue-500 stroke-white dark:fill-blue-200/20 dark:stroke-blue-200" />
<div className="[&>:first-child]:mt-0 [&>:last-child]:mb-0">
{children}
</div>
</div>
)
}
export function Row({ children }: { children: React.ReactNode }) {
return (
<div className="grid grid-cols-1 items-start gap-x-16 gap-y-10 xl:max-w-none xl:grid-cols-2">
{children}
</div>
)
}
export function Col({
children,
sticky = false,
}: {
children: React.ReactNode
sticky?: boolean
}) {
return (
<div
className={clsx(
'[&>:first-child]:mt-0 [&>:last-child]:mb-0',
sticky && 'xl:sticky xl:top-24',
)}
>
{children}
</div>
)
}
export function Properties({ children }: { children: React.ReactNode }) {
return (
<div className="my-6">
<ul
role="list"
className="m-0 max-w-[calc(var(--container-lg)-(--spacing(8)))] list-none divide-y divide-zinc-900/5 p-0 dark:divide-white/5"
>
{children}
</ul>
</div>
)
}
export function Property({
name,
children,
type,
}: {
name: string
children: React.ReactNode
type?: string
}) {
return (
<li className="m-0 px-0 py-4 first:pt-0 last:pb-0">
<dl className="m-0 flex flex-wrap items-center gap-x-3 gap-y-2">
<dt className="sr-only">Name</dt>
<dd>
<code>{name}</code>
</dd>
{type && (
<>
<dt className="sr-only">Type</dt>
<dd className="font-mono text-xs text-zinc-400 dark:text-zinc-500">
{type}
</dd>
</>
)}
<dt className="sr-only">Description</dt>
<dd className="w-full flex-none [&>:first-child]:mt-0 [&>:last-child]:mb-0">
{children}
</dd>
</dl>
</li>
)
}

14
src/images/logos/go.svg Normal file
View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none">
<g fill="#00ACD7" clip-path="url(#a)">
<path fill-rule="evenodd"
d="M5.8 19.334c-.08 0-.093-.054-.067-.107l.4-.533a.421.421 0 0 1 .227-.08h6.893c.08 0 .094.053.067.106l-.334.507c-.04.053-.133.12-.2.12L5.8 19.32v.014Zm-2.92 1.773c-.08 0-.093-.04-.053-.107l.4-.52c.04-.053.133-.093.213-.093h8.8c.093 0 .133.053.107.12l-.16.453c-.014.08-.094.134-.174.134H2.88v.013Zm4.68 1.773c-.08 0-.107-.053-.067-.12l.267-.48c.053-.053.133-.12.2-.12h3.866c.08 0 .12.067.12.134l-.04.466c0 .08-.08.134-.133.134L7.56 22.88Zm20.053-3.906-3.24.853c-.293.08-.32.093-.56-.2-.293-.32-.506-.533-.92-.733a3.36 3.36 0 0 0-3.493.293 4.107 4.107 0 0 0-1.973 3.667 3.027 3.027 0 0 0 2.613 3.04c1.306.173 2.413-.294 3.28-1.28l.533-.707H20.12c-.4 0-.507-.267-.373-.587.253-.6.72-1.6.986-2.106a.533.533 0 0 1 .48-.307h7.04c-.04.533-.04 1.04-.12 1.573-.213 1.387-.733 2.667-1.586 3.787a8.053 8.053 0 0 1-5.507 3.28 6.839 6.839 0 0 1-5.2-1.28A6.065 6.065 0 0 1 13.386 24c-.24-2.106.374-4 1.654-5.666A8.573 8.573 0 0 1 20.44 15a6.667 6.667 0 0 1 5.12.934c1.027.666 1.76 1.6 2.253 2.733.107.173.027.267-.2.32v-.013Z"
clip-rule="evenodd" />
<path
d="M34 29.667a7.253 7.253 0 0 1-4.707-1.707 6.066 6.066 0 0 1-2.08-3.733 7.373 7.373 0 0 1 1.56-5.827 8.107 8.107 0 0 1 5.413-3.226 7.173 7.173 0 0 1 5.507.986 6.015 6.015 0 0 1 2.72 4.307 7.467 7.467 0 0 1-2.227 6.547 8.854 8.854 0 0 1-4.626 2.48c-.534.093-1.054.106-1.547.173H34Zm4.613-7.813c-.027-.254-.027-.44-.067-.64a3.186 3.186 0 0 0-3.933-2.547 4.227 4.227 0 0 0-3.387 3.36A3.187 3.187 0 0 0 33 25.68c1.066.454 2.133.4 3.16-.133a4.227 4.227 0 0 0 2.453-3.68v-.013Z" />
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M4 4h40v40H4z" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none">
<path fill="#89D42C"
d="M23.675 39.82a2.48 2.48 0 0 1-1.19-.313l-3.764-2.236c-.568-.31-.285-.425-.114-.48.765-.256.906-.313 1.698-.765.086-.057.198-.029.284.028l2.888 1.727c.113.057.254.057.34 0l11.296-6.54c.113-.057.17-.17.17-.312v-13.05c0-.143-.057-.256-.17-.313l-11.296-6.51c-.114-.057-.256-.057-.34 0L12.18 17.567c-.114.057-.17.198-.17.311V30.93c0 .114.056.255.17.312l3.087 1.784c1.67.849 2.717-.143 2.717-1.133V19.01a.344.344 0 0 1 .34-.34h1.443a.344.344 0 0 1 .341.34v12.882c0 2.237-1.218 3.539-3.342 3.539-.65 0-1.16 0-2.604-.708l-2.975-1.698A2.39 2.39 0 0 1 10 30.959V17.904c0-.849.452-1.642 1.189-2.066l11.296-6.54a2.527 2.527 0 0 1 2.379 0l11.297 6.54a2.39 2.39 0 0 1 1.188 2.066V30.96c0 .85-.452 1.642-1.188 2.066l-11.297 6.54a2.896 2.896 0 0 1-1.189.256v-.001Zm3.482-8.976c-4.954 0-5.973-2.264-5.973-4.19a.344.344 0 0 1 .34-.34h1.472c.169 0 .311.114.311.284.226 1.5.878 2.236 3.879 2.236 2.378 0 3.397-.538 3.397-1.812 0-.736-.283-1.274-3.992-1.642-3.086-.311-5.012-.99-5.012-3.454 0-2.293 1.926-3.652 5.154-3.652 3.623 0 5.407 1.246 5.634 3.963a.459.459 0 0 1-.086.256c-.056.056-.141.113-.226.113h-1.472a.332.332 0 0 1-.311-.256c-.34-1.555-1.217-2.066-3.539-2.066-2.605 0-2.916.906-2.916 1.585 0 .821.368 1.076 3.878 1.53 3.483.452 5.124 1.104 5.124 3.539-.027 2.491-2.066 3.907-5.662 3.907Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

10
src/images/logos/php.svg Normal file
View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none">
<path fill="#6181B6" fill-rule="evenodd"
d="M14.643 21.762h-1.77l-.964 4.965h1.57c1.043 0 1.82-.198 2.33-.59.51-.393.853-1.047 1.03-1.966.173-.882.095-1.503-.232-1.866-.328-.362-.98-.543-1.962-.543h-.002Z"
clip-rule="evenodd" />
<path fill="#6181B6"
d="M24 13.29c-12.426 0-22.5 5.3-22.5 11.835 0 6.535 10.074 11.837 22.5 11.837s22.5-5.3 22.5-11.837S36.426 13.29 24 13.29Zm-6.113 13.971a4.55 4.55 0 0 1-1.718 1.032c-.63.203-1.434.308-2.41.308h-2.215l-.612 3.152H8.346l2.307-11.861h4.968c1.494 0 2.585.391 3.27 1.177.687.785.893 1.88.618 3.285a5.34 5.34 0 0 1-.57 1.588c-.28.493-.634.938-1.053 1.319h.002Zm7.546 1.34 1.018-5.247c.119-.598.073-1.005-.128-1.221-.2-.218-.63-.328-1.288-.328h-2.047l-1.32 6.799h-2.566L21.41 16.74h2.561l-.611 3.155h2.282c1.439 0 2.429.25 2.975.75.546.499.708 1.314.492 2.437l-1.073 5.52h-2.604V28.6Zm14.243-4.245a5.215 5.215 0 0 1-.571 1.586 5.356 5.356 0 0 1-1.051 1.319c-.49.467-1.078.82-1.721 1.032-.63.203-1.434.308-2.41.308H31.71l-.614 3.154h-2.581l2.305-11.862h4.968c1.495 0 2.584.393 3.27 1.177.686.784.895 1.878.62 3.285h-.002Z" />
<path fill="#6181B6" fill-rule="evenodd"
d="M34.81 21.762h-1.765l-.968 4.965h1.571c1.044 0 1.821-.198 2.33-.59.51-.393.852-1.047 1.032-1.966.172-.882.093-1.503-.234-1.866-.326-.362-.983-.543-1.964-.543h-.002Z"
clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none">
<g clip-path="url(#a)">
<path fill="#3372A7"
d="M23.429 9.008c-7.882 0-7.39 3.418-7.39 3.418l.01 3.541h7.52v1.063H13.062s-5.043-.572-5.043 7.38c0 7.954 4.402 7.671 4.402 7.671h2.627v-3.69s-.142-4.402 4.331-4.402h7.46s4.191.068 4.191-4.05v-6.81s.637-4.12-7.6-4.12Zm-4.147 2.382a1.353 1.353 0 1 1 .001 2.706 1.353 1.353 0 0 1-.001-2.706Z" />
<path fill="#FFD235"
d="M23.653 39.894c7.881 0 7.39-3.418 7.39-3.418l-.01-3.541h-7.52v-1.063H34.02s5.043.572 5.043-7.381-4.402-7.67-4.402-7.67h-2.627v3.69s.142 4.402-4.332 4.402h-7.46s-4.19-.068-4.19 4.05v6.81s-.637 4.12 7.6 4.12Zm4.147-2.381a1.353 1.353 0 1 1-.002-2.707 1.353 1.353 0 0 1 .002 2.706Z" />
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M8 9h31.122v31H8z" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 864 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none">
<path fill="#D91505"
d="M33.735 10.41c3.376.585 4.334 2.893 4.262 5.311l.017-.035-1.519 19.912-19.752 1.352h.017c-1.639-.069-5.294-.218-5.46-5.328l1.83-3.34 3.139 7.331.56 1.306L19.95 26.74l-.032.007.017-.034 10.302 3.29-1.555-6.044-1.101-4.341 9.817-.634-.684-.567-7.048-5.746 4.073-2.272-.004.012v-.001ZM17.01 15.966c3.963-3.932 9.079-6.256 11.044-4.274 1.96 1.98-.118 6.796-4.089 10.726-3.966 3.931-9.02 6.382-10.98 4.405-1.967-1.98.05-6.921 4.02-10.853l.005-.004Z" />
</svg>

After

Width:  |  Height:  |  Size: 561 B

8
src/lib/remToPx.ts Normal file
View File

@@ -0,0 +1,8 @@
export function remToPx(remValue: number) {
let rootFontSize =
typeof window === 'undefined'
? 16
: parseFloat(window.getComputedStyle(document.documentElement).fontSize)
return remValue * rootFontSize
}

3
src/mdx/recma.mjs Normal file
View File

@@ -0,0 +1,3 @@
import { mdxAnnotations } from 'mdx-annotations'
export const recmaPlugins = [mdxAnnotations.recma]

123
src/mdx/rehype.mjs Normal file
View File

@@ -0,0 +1,123 @@
import { slugifyWithCounter } from '@sindresorhus/slugify'
import * as acorn from 'acorn'
import { toString } from 'mdast-util-to-string'
import { mdxAnnotations } from 'mdx-annotations'
import shiki from 'shiki'
import { visit } from 'unist-util-visit'
function rehypeParseCodeBlocks() {
return (tree) => {
visit(tree, 'element', (node, _nodeIndex, parentNode) => {
if (node.tagName === 'code') {
parentNode.properties.language = node.properties.className
? node.properties?.className[0]?.replace(/^language-/, '')
: 'txt'
}
})
}
}
let highlighter
function rehypeShiki() {
return async (tree) => {
highlighter =
highlighter ?? (await shiki.getHighlighter({ theme: 'css-variables' }))
visit(tree, 'element', (node) => {
if (node.tagName === 'pre' && node.children[0]?.tagName === 'code') {
let codeNode = node.children[0]
let textNode = codeNode.children[0]
node.properties.code = textNode.value
if (node.properties.language) {
let tokens = highlighter.codeToThemedTokens(
textNode.value,
node.properties.language,
)
textNode.value = shiki.renderToHtml(tokens, {
elements: {
pre: ({ children }) => children,
code: ({ children }) => children,
line: ({ children }) => `<span>${children}</span>`,
},
})
}
}
})
}
}
function rehypeSlugify() {
return (tree) => {
let slugify = slugifyWithCounter()
visit(tree, 'element', (node) => {
if (node.tagName === 'h2' && !node.properties.id) {
node.properties.id = slugify(toString(node))
}
})
}
}
function rehypeAddMDXExports(getExports) {
return (tree) => {
let exports = Object.entries(getExports(tree))
for (let [name, value] of exports) {
for (let node of tree.children) {
if (
node.type === 'mdxjsEsm' &&
new RegExp(`export\\s+const\\s+${name}\\s*=`).test(node.value)
) {
return
}
}
let exportStr = `export const ${name} = ${value}`
tree.children.push({
type: 'mdxjsEsm',
value: exportStr,
data: {
estree: acorn.parse(exportStr, {
sourceType: 'module',
ecmaVersion: 'latest',
}),
},
})
}
}
}
function getSections(node) {
let sections = []
for (let child of node.children ?? []) {
if (child.type === 'element' && child.tagName === 'h2') {
sections.push(`{
title: ${JSON.stringify(toString(child))},
id: ${JSON.stringify(child.properties.id)},
...${child.properties.annotation}
}`)
} else if (child.children) {
sections.push(...getSections(child))
}
}
return sections
}
export const rehypePlugins = [
mdxAnnotations.rehype,
rehypeParseCodeBlocks,
rehypeShiki,
rehypeSlugify,
[
rehypeAddMDXExports,
(tree) => ({
sections: `[${getSections(tree).join()}]`,
}),
],
]

4
src/mdx/remark.mjs Normal file
View File

@@ -0,0 +1,4 @@
import { mdxAnnotations } from 'mdx-annotations'
import remarkGfm from 'remark-gfm'
export const remarkPlugins = [mdxAnnotations.remark, remarkGfm]

135
src/mdx/search.mjs Normal file
View File

@@ -0,0 +1,135 @@
import { slugifyWithCounter } from '@sindresorhus/slugify'
import glob from 'fast-glob'
import * as fs from 'fs'
import { toString } from 'mdast-util-to-string'
import * as path from 'path'
import { remark } from 'remark'
import remarkMdx from 'remark-mdx'
import { createLoader } from 'simple-functional-loader'
import { filter } from 'unist-util-filter'
import { SKIP, visit } from 'unist-util-visit'
import * as url from 'url'
const __filename = url.fileURLToPath(import.meta.url)
const processor = remark().use(remarkMdx).use(extractSections)
const slugify = slugifyWithCounter()
function isObjectExpression(node) {
return (
node.type === 'mdxTextExpression' &&
node.data?.estree?.body?.[0]?.expression?.type === 'ObjectExpression'
)
}
function excludeObjectExpressions(tree) {
return filter(tree, (node) => !isObjectExpression(node))
}
function extractSections() {
return (tree, { sections }) => {
slugify.reset()
visit(tree, (node) => {
if (node.type === 'heading' || node.type === 'paragraph') {
let content = toString(excludeObjectExpressions(node))
if (node.type === 'heading' && node.depth <= 2) {
let hash = node.depth === 1 ? null : slugify(content)
sections.push([content, hash, []])
} else {
sections.at(-1)?.[2].push(content)
}
return SKIP
}
})
}
}
export default function Search(nextConfig = {}) {
let cache = new Map()
return Object.assign({}, nextConfig, {
webpack(config, options) {
config.module.rules.push({
test: __filename,
use: [
createLoader(function () {
let appDir = path.resolve('./src/app')
this.addContextDependency(appDir)
let files = glob.sync('**/*.mdx', { cwd: appDir })
let data = files.map((file) => {
let url = '/' + file.replace(/(^|\/)page\.mdx$/, '')
let mdx = fs.readFileSync(path.join(appDir, file), 'utf8')
let sections = []
if (cache.get(file)?.[0] === mdx) {
sections = cache.get(file)[1]
} else {
let vfile = { value: mdx, sections }
processor.runSync(processor.parse(vfile), vfile)
cache.set(file, [mdx, sections])
}
return { url, sections }
})
// When this file is imported within the application
// the following module is loaded:
return `
import FlexSearch from 'flexsearch'
let sectionIndex = new FlexSearch.Document({
tokenize: 'full',
document: {
id: 'url',
index: 'content',
store: ['title', 'pageTitle'],
},
context: {
resolution: 9,
depth: 2,
bidirectional: true
}
})
let data = ${JSON.stringify(data)}
for (let { url, sections } of data) {
for (let [title, hash, content] of sections) {
sectionIndex.add({
url: url + (hash ? ('#' + hash) : ''),
title,
content: [title, ...content].join('\\n'),
pageTitle: hash ? sections[0][0] : undefined,
})
}
}
export function search(query, options = {}) {
let result = sectionIndex.search(query, {
...options,
enrich: true,
})
if (result.length === 0) {
return []
}
return result[0].result.map((item) => ({
url: item.id,
title: item.doc.title,
pageTitle: item.doc.pageTitle,
}))
}
`
}),
],
})
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options)
}
return config
},
})
}

60
src/styles/tailwind.css Normal file
View File

@@ -0,0 +1,60 @@
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
@config '../../typography.ts';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--text-2xs: 0.75rem;
--text-2xs--line-height: 1.25rem;
--text-xs: 0.8125rem;
--text-xs--line-height: 1.5rem;
--text-sm: 0.875rem;
--text-sm--line-height: 1.5rem;
--text-base: 1rem;
--text-base--line-height: 1.75rem;
--text-lg: 1.125rem;
--text-lg--line-height: 1.75rem;
--text-xl: 1.25rem;
--text-xl--line-height: 1.75rem;
--text-2xl: 1.5rem;
--text-2xl--line-height: 2rem;
--text-3xl: 1.875rem;
--text-3xl--line-height: 2.25rem;
--text-4xl: 2.25rem;
--text-4xl--line-height: 2.5rem;
--text-5xl: 3rem;
--text-5xl--line-height: 1;
--text-6xl: 3.75rem;
--text-6xl--line-height: 1;
--text-7xl: 4.5rem;
--text-7xl--line-height: 1;
--text-8xl: 6rem;
--text-8xl--line-height: 1;
--text-9xl: 8rem;
--text-9xl--line-height: 1;
--shadow-glow: 0 0 4px rgb(0 0 0 / 0.1);
--container-lg: 33rem;
--container-2xl: 40rem;
--container-3xl: 50rem;
--container-5xl: 66rem;
}
@layer base {
:root {
--shiki-color-text: var(--color-white);
--shiki-token-constant: var(--color-emerald-300);
--shiki-token-string: var(--color-emerald-300);
--shiki-token-comment: var(--color-zinc-500);
--shiki-token-keyword: var(--color-sky-300);
--shiki-token-parameter: var(--color-pink-300);
--shiki-token-function: var(--color-violet-300);
--shiki-token-string-expression: var(--color-emerald-300);
--shiki-token-punctuation: var(--color-zinc-200);
}
[inert] ::-webkit-scrollbar {
display: none;
}
}

28
tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

11
types.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
import { type SearchOptions } from 'flexsearch'
declare module '@/mdx/search.mjs' {
export type Result = {
url: string
title: string
pageTitle?: string
}
export function search(query: string, options?: SearchOptions): Array<Result>
}

354
typography.ts Normal file
View File

@@ -0,0 +1,354 @@
import { type Config } from 'tailwindcss'
export default {
theme: {
typography: ({ theme }) => ({
DEFAULT: {
css: {
'--tw-prose-body': theme('colors.zinc.700'),
'--tw-prose-headings': theme('colors.zinc.900'),
'--tw-prose-links': theme('colors.blue.500'),
'--tw-prose-links-hover': theme('colors.blue.600'),
'--tw-prose-links-underline': theme('colors.blue.500 / 0.3'),
'--tw-prose-bold': theme('colors.zinc.900'),
'--tw-prose-counters': theme('colors.zinc.500'),
'--tw-prose-bullets': theme('colors.zinc.300'),
'--tw-prose-hr': theme('colors.zinc.900 / 0.05'),
'--tw-prose-quotes': theme('colors.zinc.900'),
'--tw-prose-quote-borders': theme('colors.zinc.200'),
'--tw-prose-captions': theme('colors.zinc.500'),
'--tw-prose-code': theme('colors.zinc.900'),
'--tw-prose-code-bg': theme('colors.zinc.100'),
'--tw-prose-code-ring': theme('colors.zinc.300'),
'--tw-prose-th-borders': theme('colors.zinc.300'),
'--tw-prose-td-borders': theme('colors.zinc.200'),
'--tw-prose-invert-body': theme('colors.zinc.400'),
'--tw-prose-invert-headings': theme('colors.white'),
'--tw-prose-invert-links': theme('colors.blue.400'),
'--tw-prose-invert-links-hover': theme('colors.blue.500'),
'--tw-prose-invert-links-underline': theme(
'colors.blue.500 / 0.3',
),
'--tw-prose-invert-bold': theme('colors.white'),
'--tw-prose-invert-counters': theme('colors.zinc.400'),
'--tw-prose-invert-bullets': theme('colors.zinc.600'),
'--tw-prose-invert-hr': theme('colors.white / 0.05'),
'--tw-prose-invert-quotes': theme('colors.zinc.100'),
'--tw-prose-invert-quote-borders': theme('colors.zinc.700'),
'--tw-prose-invert-captions': theme('colors.zinc.400'),
'--tw-prose-invert-code': theme('colors.white'),
'--tw-prose-invert-code-bg': theme('colors.zinc.700 / 0.15'),
'--tw-prose-invert-code-ring': theme('colors.white / 0.1'),
'--tw-prose-invert-th-borders': theme('colors.zinc.600'),
'--tw-prose-invert-td-borders': theme('colors.zinc.700'),
// Base
color: 'var(--tw-prose-body)',
fontSize: theme('fontSize.sm')[0],
lineHeight: theme('lineHeight.7'),
// Text
p: {
marginTop: theme('spacing.6'),
marginBottom: theme('spacing.6'),
},
'[class~="lead"]': {
fontSize: theme('fontSize.base')[0],
...theme('fontSize.base')[1],
},
// Lists
ol: {
listStyleType: 'decimal',
marginTop: theme('spacing.5'),
marginBottom: theme('spacing.5'),
paddingLeft: '1.625rem',
},
'ol[type="A"]': {
listStyleType: 'upper-alpha',
},
'ol[type="a"]': {
listStyleType: 'lower-alpha',
},
'ol[type="A" s]': {
listStyleType: 'upper-alpha',
},
'ol[type="a" s]': {
listStyleType: 'lower-alpha',
},
'ol[type="I"]': {
listStyleType: 'upper-roman',
},
'ol[type="i"]': {
listStyleType: 'lower-roman',
},
'ol[type="I" s]': {
listStyleType: 'upper-roman',
},
'ol[type="i" s]': {
listStyleType: 'lower-roman',
},
'ol[type="1"]': {
listStyleType: 'decimal',
},
ul: {
listStyleType: 'disc',
marginTop: theme('spacing.5'),
marginBottom: theme('spacing.5'),
paddingLeft: '1.625rem',
},
li: {
marginTop: theme('spacing.2'),
marginBottom: theme('spacing.2'),
},
':is(ol, ul) > li': {
paddingLeft: theme('spacing[1.5]'),
},
'ol > li::marker': {
fontWeight: '400',
color: 'var(--tw-prose-counters)',
},
'ul > li::marker': {
color: 'var(--tw-prose-bullets)',
},
'> ul > li p': {
marginTop: theme('spacing.3'),
marginBottom: theme('spacing.3'),
},
'> ul > li > *:first-child': {
marginTop: theme('spacing.5'),
},
'> ul > li > *:last-child': {
marginBottom: theme('spacing.5'),
},
'> ol > li > *:first-child': {
marginTop: theme('spacing.5'),
},
'> ol > li > *:last-child': {
marginBottom: theme('spacing.5'),
},
'ul ul, ul ol, ol ul, ol ol': {
marginTop: theme('spacing.3'),
marginBottom: theme('spacing.3'),
},
// Horizontal rules
hr: {
borderColor: 'var(--tw-prose-hr)',
borderTopWidth: 1,
marginTop: theme('spacing.16'),
marginBottom: theme('spacing.16'),
maxWidth: 'none',
marginLeft: `calc(-1 * ${theme('spacing.4')})`,
marginRight: `calc(-1 * ${theme('spacing.4')})`,
'@screen sm': {
marginLeft: `calc(-1 * ${theme('spacing.6')})`,
marginRight: `calc(-1 * ${theme('spacing.6')})`,
},
'@screen lg': {
marginLeft: `calc(-1 * ${theme('spacing.8')})`,
marginRight: `calc(-1 * ${theme('spacing.8')})`,
},
},
// Quotes
blockquote: {
fontWeight: '500',
fontStyle: 'italic',
color: 'var(--tw-prose-quotes)',
borderLeftWidth: '0.25rem',
borderLeftColor: 'var(--tw-prose-quote-borders)',
quotes: '"\\201C""\\201D""\\2018""\\2019"',
marginTop: theme('spacing.8'),
marginBottom: theme('spacing.8'),
paddingLeft: theme('spacing.5'),
},
'blockquote p:first-of-type::before': {
content: 'open-quote',
},
'blockquote p:last-of-type::after': {
content: 'close-quote',
},
// Headings
h1: {
color: 'var(--tw-prose-headings)',
fontWeight: '700',
fontSize: theme('fontSize.2xl')[0],
...theme('fontSize.2xl')[1],
marginBottom: theme('spacing.2'),
},
h2: {
color: 'var(--tw-prose-headings)',
fontWeight: '600',
fontSize: theme('fontSize.lg')[0],
...theme('fontSize.lg')[1],
marginTop: theme('spacing.16'),
marginBottom: theme('spacing.2'),
},
h3: {
color: 'var(--tw-prose-headings)',
fontSize: theme('fontSize.base')[0],
...theme('fontSize.base')[1],
fontWeight: '600',
marginTop: theme('spacing.10'),
marginBottom: theme('spacing.2'),
},
// Media
'img, video, figure': {
marginTop: theme('spacing.8'),
marginBottom: theme('spacing.8'),
},
'figure > *': {
marginTop: '0',
marginBottom: '0',
},
figcaption: {
color: 'var(--tw-prose-captions)',
fontSize: theme('fontSize.xs')[0],
...theme('fontSize.xs')[1],
marginTop: theme('spacing.2'),
},
// Tables
table: {
width: '100%',
tableLayout: 'auto',
textAlign: 'left',
marginTop: theme('spacing.8'),
marginBottom: theme('spacing.8'),
lineHeight: theme('lineHeight.6'),
},
thead: {
borderBottomWidth: '1px',
borderBottomColor: 'var(--tw-prose-th-borders)',
},
'thead th': {
color: 'var(--tw-prose-headings)',
fontWeight: '600',
verticalAlign: 'bottom',
paddingRight: theme('spacing.2'),
paddingBottom: theme('spacing.2'),
paddingLeft: theme('spacing.2'),
},
'thead th:first-child': {
paddingLeft: '0',
},
'thead th:last-child': {
paddingRight: '0',
},
'tbody tr': {
borderBottomWidth: '1px',
borderBottomColor: 'var(--tw-prose-td-borders)',
},
'tbody tr:last-child': {
borderBottomWidth: '0',
},
'tbody td': {
verticalAlign: 'baseline',
},
tfoot: {
borderTopWidth: '1px',
borderTopColor: 'var(--tw-prose-th-borders)',
},
'tfoot td': {
verticalAlign: 'top',
},
':is(tbody, tfoot) td': {
paddingTop: theme('spacing.2'),
paddingRight: theme('spacing.2'),
paddingBottom: theme('spacing.2'),
paddingLeft: theme('spacing.2'),
},
':is(tbody, tfoot) td:first-child': {
paddingLeft: '0',
},
':is(tbody, tfoot) td:last-child': {
paddingRight: '0',
},
// Inline elements
a: {
color: 'var(--tw-prose-links)',
textDecoration: 'underline transparent',
fontWeight: '500',
transitionProperty: 'color, text-decoration-color',
transitionDuration: theme('transitionDuration.DEFAULT'),
transitionTimingFunction: theme('transitionTimingFunction.DEFAULT'),
'&:hover': {
color: 'var(--tw-prose-links-hover)',
textDecorationColor: 'var(--tw-prose-links-underline)',
},
},
':is(h1, h2, h3) a': {
fontWeight: 'inherit',
},
strong: {
color: 'var(--tw-prose-bold)',
fontWeight: '600',
},
':is(a, blockquote, thead th) strong': {
color: 'inherit',
},
code: {
color: 'var(--tw-prose-code)',
borderRadius: theme('borderRadius.lg'),
paddingTop: theme('padding.1'),
paddingRight: theme('padding[1.5]'),
paddingBottom: theme('padding.1'),
paddingLeft: theme('padding[1.5]'),
boxShadow: 'inset 0 0 0 1px var(--tw-prose-code-ring)',
backgroundColor: 'var(--tw-prose-code-bg)',
fontSize: theme('fontSize.2xs')[0],
},
':is(a, h1, h2, h3, blockquote, thead th) code': {
color: 'inherit',
},
'h2 code': {
fontSize: theme('fontSize.base')[0],
fontWeight: 'inherit',
},
'h3 code': {
fontSize: theme('fontSize.sm')[0],
fontWeight: 'inherit',
},
// Overrides
':is(h1, h2, h3) + *': {
marginTop: '0',
},
'> :first-child': {
marginTop: '0 !important',
},
'> :last-child': {
marginBottom: '0 !important',
},
},
},
invert: {
css: {
'--tw-prose-body': 'var(--tw-prose-invert-body)',
'--tw-prose-headings': 'var(--tw-prose-invert-headings)',
'--tw-prose-links': 'var(--tw-prose-invert-links)',
'--tw-prose-links-hover': 'var(--tw-prose-invert-links-hover)',
'--tw-prose-links-underline':
'var(--tw-prose-invert-links-underline)',
'--tw-prose-bold': 'var(--tw-prose-invert-bold)',
'--tw-prose-counters': 'var(--tw-prose-invert-counters)',
'--tw-prose-bullets': 'var(--tw-prose-invert-bullets)',
'--tw-prose-hr': 'var(--tw-prose-invert-hr)',
'--tw-prose-quotes': 'var(--tw-prose-invert-quotes)',
'--tw-prose-quote-borders': 'var(--tw-prose-invert-quote-borders)',
'--tw-prose-captions': 'var(--tw-prose-invert-captions)',
'--tw-prose-code': 'var(--tw-prose-invert-code)',
'--tw-prose-code-bg': 'var(--tw-prose-invert-code-bg)',
'--tw-prose-code-ring': 'var(--tw-prose-invert-code-ring)',
'--tw-prose-th-borders': 'var(--tw-prose-invert-th-borders)',
'--tw-prose-td-borders': 'var(--tw-prose-invert-td-borders)',
},
},
}),
},
} satisfies Config

4570
yarn.lock Normal file

File diff suppressed because it is too large Load Diff