forked from github/plane
chore: remove docs (#553)
* feat: block sync * chore: remove docs from repo
This commit is contained in:
parent
d9f31a1eb7
commit
fbbf97f3a6
@ -1,4 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
extends: ['custom'],
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
# Plane Docs
|
|
||||||
|
|
||||||
Source code that powers plane.so/docs
|
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
import { mdxAnnotations } from 'mdx-annotations'
|
|
||||||
import recmaNextjsStaticProps from 'recma-nextjs-static-props'
|
|
||||||
|
|
||||||
function recmaRemoveNamedExports() {
|
|
||||||
return (tree) => {
|
|
||||||
tree.body = tree.body.map((node) => {
|
|
||||||
if (node.type === 'ExportNamedDeclaration') {
|
|
||||||
return node.declaration
|
|
||||||
}
|
|
||||||
return node
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const recmaPlugins = [
|
|
||||||
mdxAnnotations.recma,
|
|
||||||
recmaRemoveNamedExports,
|
|
||||||
recmaNextjsStaticProps,
|
|
||||||
]
|
|
@ -1,126 +0,0 @@
|
|||||||
import { mdxAnnotations } from 'mdx-annotations'
|
|
||||||
import { visit } from 'unist-util-visit'
|
|
||||||
import rehypeMdxTitle from 'rehype-mdx-title'
|
|
||||||
import shiki from 'shiki'
|
|
||||||
import { toString } from 'mdast-util-to-string'
|
|
||||||
import * as acorn from 'acorn'
|
|
||||||
import { slugifyWithCounter } from '@sindresorhus/slugify'
|
|
||||||
|
|
||||||
function rehypeParseCodeBlocks() {
|
|
||||||
return (tree) => {
|
|
||||||
visit(tree, 'element', (node, _nodeIndex, parentNode) => {
|
|
||||||
if (node.tagName === 'code' && node.properties.className) {
|
|
||||||
parentNode.properties.language = node.properties.className[0]?.replace(
|
|
||||||
/^language-/,
|
|
||||||
''
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
rehypeMdxTitle,
|
|
||||||
[
|
|
||||||
rehypeAddMDXExports,
|
|
||||||
(tree) => ({
|
|
||||||
sections: `[${getSections(tree).join()}]`,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
]
|
|
@ -1,3 +0,0 @@
|
|||||||
import { mdxAnnotations } from 'mdx-annotations'
|
|
||||||
|
|
||||||
export const remarkPlugins = [mdxAnnotations.remark]
|
|
@ -1,23 +0,0 @@
|
|||||||
import nextMDX from '@next/mdx'
|
|
||||||
import { remarkPlugins } from './mdx/remark.mjs'
|
|
||||||
import { rehypePlugins } from './mdx/rehype.mjs'
|
|
||||||
import { recmaPlugins } from './mdx/recma.mjs'
|
|
||||||
|
|
||||||
const withMDX = nextMDX({
|
|
||||||
options: {
|
|
||||||
remarkPlugins,
|
|
||||||
rehypePlugins,
|
|
||||||
recmaPlugins,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
reactStrictMode: true,
|
|
||||||
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'mdx'],
|
|
||||||
experimental: {
|
|
||||||
scrollRestoration: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withMDX(nextConfig)
|
|
@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "docs",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev --port 3002",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint"
|
|
||||||
},
|
|
||||||
"browserslist": "defaults, not ie <= 11",
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/autocomplete-core": "^1.7.3",
|
|
||||||
"@algolia/autocomplete-preset-algolia": "^1.7.3",
|
|
||||||
"@headlessui/react": "^1.7.7",
|
|
||||||
"@mdx-js/loader": "^2.1.5",
|
|
||||||
"@mdx-js/react": "^2.1.5",
|
|
||||||
"@next/mdx": "^13.0.3",
|
|
||||||
"@sindresorhus/slugify": "^2.1.1",
|
|
||||||
"@tailwindcss/typography": "^0.5.8",
|
|
||||||
"acorn": "^8.8.1",
|
|
||||||
"algoliasearch": "^4.14.2",
|
|
||||||
"autoprefixer": "^10.4.7",
|
|
||||||
"clsx": "^1.2.0",
|
|
||||||
"focus-visible": "^5.2.0",
|
|
||||||
"framer-motion": "7.8.1",
|
|
||||||
"mdast-util-to-string": "^3.1.0",
|
|
||||||
"mdx-annotations": "^0.1.1",
|
|
||||||
"next": "13.0.2",
|
|
||||||
"postcss-focus-visible": "^6.0.4",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"recma-nextjs-static-props": "^1.0.0",
|
|
||||||
"rehype-mdx-title": "^2.0.0",
|
|
||||||
"shiki": "^0.11.1",
|
|
||||||
"tailwindcss": "^3.2.4",
|
|
||||||
"unist-util-visit": "^4.1.1",
|
|
||||||
"zustand": "^4.1.4"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.51.0",
|
|
||||||
"eslint": "8.26.0",
|
|
||||||
"eslint-config-next": "13.0.2",
|
|
||||||
"prettier": "^2.7.1",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.1.13"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
'postcss-focus-visible': {
|
|
||||||
replaceWith: '[data-focus-visible-added]',
|
|
||||||
},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
singleQuote: true,
|
|
||||||
semi: false,
|
|
||||||
plugins: [require('prettier-plugin-tailwindcss')],
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 15 KiB |
@ -1,62 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
|
|
||||||
function ArrowIcon(props) {
|
|
||||||
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',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Button({
|
|
||||||
variant = 'primary',
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
arrow,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const Component = props.href ? Link : 'button'
|
|
||||||
|
|
||||||
className = clsx(
|
|
||||||
'inline-flex gap-0.5 justify-center overflow-hidden text-sm font-medium transition',
|
|
||||||
variantStyles[variant],
|
|
||||||
className
|
|
||||||
)
|
|
||||||
|
|
||||||
const 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'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Component className={className} {...props}>
|
|
||||||
{arrow === 'left' && arrowIcon}
|
|
||||||
{children}
|
|
||||||
{arrow === 'right' && arrowIcon}
|
|
||||||
</Component>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,301 +0,0 @@
|
|||||||
import {
|
|
||||||
Children,
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { Tab } from '@headlessui/react'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import create from 'zustand'
|
|
||||||
|
|
||||||
import { Tag } from '@/components/Tag'
|
|
||||||
|
|
||||||
const languageNames = {
|
|
||||||
js: 'JavaScript',
|
|
||||||
ts: 'TypeScript',
|
|
||||||
javascript: 'JavaScript',
|
|
||||||
typescript: 'TypeScript',
|
|
||||||
php: 'PHP',
|
|
||||||
python: 'Python',
|
|
||||||
ruby: 'Ruby',
|
|
||||||
go: 'Go',
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPanelTitle({ title, language }) {
|
|
||||||
return title ?? languageNames[language] ?? 'Code'
|
|
||||||
}
|
|
||||||
|
|
||||||
function ClipboardIcon(props) {
|
|
||||||
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 }) {
|
|
||||||
const [copyCount, setCopyCount] = useState(0)
|
|
||||||
const copied = copyCount > 0
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (copyCount > 0) {
|
|
||||||
const 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 pl-2 pr-3 text-2xs font-medium opacity-0 backdrop-blur transition focus:opacity-100 group-hover:opacity-100',
|
|
||||||
copied
|
|
||||||
? 'bg-blue-400/10 ring-1 ring-inset ring-blue-400/20'
|
|
||||||
: '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 }) {
|
|
||||||
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-zinc-900 bg-white/2.5 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({ tag, label, code, children }) {
|
|
||||||
const child = Children.only(children)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="group dark:bg-white/2.5">
|
|
||||||
<CodePanelHeader
|
|
||||||
tag={child.props.tag ?? tag}
|
|
||||||
label={child.props.label ?? label}
|
|
||||||
/>
|
|
||||||
<div className="relative">
|
|
||||||
<pre className="overflow-x-auto p-4 text-xs text-white">{children}</pre>
|
|
||||||
<CopyButton code={child.props.code ?? code} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeGroupHeader({ title, children, selectedIndex }) {
|
|
||||||
const hasTabs = Children.count(children) > 1
|
|
||||||
|
|
||||||
if (!title && !hasTabs) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[calc(theme(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 && (
|
|
||||||
<Tab.List className="-mb-px flex gap-4 text-xs font-medium">
|
|
||||||
{Children.map(children, (child, childIndex) => (
|
|
||||||
<Tab
|
|
||||||
className={clsx(
|
|
||||||
'border-b py-3 transition focus:[&:not(:focus-visible)]:outline-none',
|
|
||||||
childIndex === selectedIndex
|
|
||||||
? 'border-blue-500 text-blue-400'
|
|
||||||
: 'border-transparent text-zinc-400 hover:text-zinc-300'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{getPanelTitle(child.props)}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tab.List>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeGroupPanels({ children, ...props }) {
|
|
||||||
const hasTabs = Children.count(children) > 1
|
|
||||||
|
|
||||||
if (hasTabs) {
|
|
||||||
return (
|
|
||||||
<Tab.Panels>
|
|
||||||
{Children.map(children, (child) => (
|
|
||||||
<Tab.Panel>
|
|
||||||
<CodePanel {...props}>{child}</CodePanel>
|
|
||||||
</Tab.Panel>
|
|
||||||
))}
|
|
||||||
</Tab.Panels>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <CodePanel {...props}>{children}</CodePanel>
|
|
||||||
}
|
|
||||||
|
|
||||||
function usePreventLayoutShift() {
|
|
||||||
const positionRef = useRef()
|
|
||||||
const rafRef = useRef()
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => () => {
|
|
||||||
window.cancelAnimationFrame(rafRef.current)
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
positionRef,
|
|
||||||
preventLayoutShift(callback) {
|
|
||||||
const initialTop = positionRef.current.getBoundingClientRect().top
|
|
||||||
|
|
||||||
callback()
|
|
||||||
|
|
||||||
rafRef.current = window.requestAnimationFrame(() => {
|
|
||||||
const newTop = positionRef.current.getBoundingClientRect().top
|
|
||||||
window.scrollBy(0, newTop - initialTop)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const usePreferredLanguageStore = create((set) => ({
|
|
||||||
preferredLanguages: [],
|
|
||||||
addPreferredLanguage: (language) =>
|
|
||||||
set((state) => ({
|
|
||||||
preferredLanguages: [
|
|
||||||
...state.preferredLanguages.filter(
|
|
||||||
(preferredLanguage) => preferredLanguage !== language
|
|
||||||
),
|
|
||||||
language,
|
|
||||||
],
|
|
||||||
})),
|
|
||||||
}))
|
|
||||||
|
|
||||||
function useTabGroupProps(availableLanguages) {
|
|
||||||
const { preferredLanguages, addPreferredLanguage } =
|
|
||||||
usePreferredLanguageStore()
|
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
||||||
const activeLanguage = [...availableLanguages].sort(
|
|
||||||
(a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a)
|
|
||||||
)[0]
|
|
||||||
const languageIndex = availableLanguages.indexOf(activeLanguage)
|
|
||||||
const newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex
|
|
||||||
if (newSelectedIndex !== selectedIndex) {
|
|
||||||
setSelectedIndex(newSelectedIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { positionRef, preventLayoutShift } = usePreventLayoutShift()
|
|
||||||
|
|
||||||
return {
|
|
||||||
as: 'div',
|
|
||||||
ref: positionRef,
|
|
||||||
selectedIndex,
|
|
||||||
onChange: (newSelectedIndex) => {
|
|
||||||
preventLayoutShift(() =>
|
|
||||||
addPreferredLanguage(availableLanguages[newSelectedIndex])
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const CodeGroupContext = createContext(false)
|
|
||||||
|
|
||||||
export function CodeGroup({ children, title, ...props }) {
|
|
||||||
const languages = Children.map(children, (child) =>
|
|
||||||
getPanelTitle(child.props)
|
|
||||||
)
|
|
||||||
const tabGroupProps = useTabGroupProps(languages)
|
|
||||||
const hasTabs = Children.count(children) > 1
|
|
||||||
const Container = hasTabs ? Tab.Group : 'div'
|
|
||||||
const containerProps = hasTabs ? tabGroupProps : {}
|
|
||||||
const headerProps = hasTabs
|
|
||||||
? { selectedIndex: tabGroupProps.selectedIndex }
|
|
||||||
: {}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CodeGroupContext.Provider value={true}>
|
|
||||||
<Container
|
|
||||||
{...containerProps}
|
|
||||||
className="not-prose my-6 overflow-hidden rounded-2xl bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10"
|
|
||||||
>
|
|
||||||
<CodeGroupHeader title={title} {...headerProps}>
|
|
||||||
{children}
|
|
||||||
</CodeGroupHeader>
|
|
||||||
<CodeGroupPanels {...props}>{children}</CodeGroupPanels>
|
|
||||||
</Container>
|
|
||||||
</CodeGroupContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Code({ children, ...props }) {
|
|
||||||
const isGrouped = useContext(CodeGroupContext)
|
|
||||||
|
|
||||||
if (isGrouped) {
|
|
||||||
return <code {...props} dangerouslySetInnerHTML={{ __html: children }} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <code {...props}>{children}</code>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Pre({ children, ...props }) {
|
|
||||||
const isGrouped = useContext(CodeGroupContext)
|
|
||||||
|
|
||||||
if (isGrouped) {
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
|
|
||||||
return <CodeGroup {...props}>{children}</CodeGroup>
|
|
||||||
}
|
|
@ -1,231 +0,0 @@
|
|||||||
import { forwardRef, Fragment, useState } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { Transition } from '@headlessui/react'
|
|
||||||
|
|
||||||
import { Button } from '@/components/Button'
|
|
||||||
import { navigation } from '@/components/Navigation'
|
|
||||||
|
|
||||||
function CheckIcon(props) {
|
|
||||||
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) {
|
|
||||||
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(function FeedbackForm({ onSubmit }, ref) {
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
ref={ref}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
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(function FeedbackThanks(_props, ref) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
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-inset ring-blue-500/20 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>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
function Feedback() {
|
|
||||||
const [submitted, setSubmitted] = useState(false)
|
|
||||||
|
|
||||||
function onSubmit(event) {
|
|
||||||
event.preventDefault()
|
|
||||||
|
|
||||||
// event.nativeEvent.submitter.dataset.response
|
|
||||||
// => "yes" or "no"
|
|
||||||
|
|
||||||
setSubmitted(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative h-8">
|
|
||||||
<Transition
|
|
||||||
show={!submitted}
|
|
||||||
as={Fragment}
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
leave="pointer-events-none duration-300"
|
|
||||||
>
|
|
||||||
<FeedbackForm onSubmit={onSubmit} />
|
|
||||||
</Transition>
|
|
||||||
<Transition
|
|
||||||
show={submitted}
|
|
||||||
as={Fragment}
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
enter="delay-150 duration-300"
|
|
||||||
>
|
|
||||||
<FeedbackThanks />
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function PageLink({ label, page, previous = false }) {
|
|
||||||
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() {
|
|
||||||
const router = useRouter()
|
|
||||||
const allPages = navigation.flatMap((group) => group.links)
|
|
||||||
const currentPageIndex = allPages.findIndex(
|
|
||||||
(page) => page.href === router.pathname
|
|
||||||
)
|
|
||||||
|
|
||||||
if (currentPageIndex === -1) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousPage = allPages[currentPageIndex - 1]
|
|
||||||
const 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 TwitterIcon(props) {
|
|
||||||
return (
|
|
||||||
<svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
|
|
||||||
<path d="M16.712 6.652c.01.146.01.29.01.436 0 4.449-3.267 9.579-9.242 9.579v-.003a8.963 8.963 0 0 1-4.98-1.509 6.379 6.379 0 0 0 4.807-1.396c-1.39-.027-2.608-.966-3.035-2.337.487.097.99.077 1.467-.059-1.514-.316-2.606-1.696-2.606-3.3v-.041c.45.26.956.404 1.475.42C3.18 7.454 2.74 5.486 3.602 3.947c1.65 2.104 4.083 3.382 6.695 3.517a3.446 3.446 0 0 1 .94-3.217 3.172 3.172 0 0 1 4.596.148 6.38 6.38 0 0 0 2.063-.817 3.357 3.357 0 0 1-1.428 1.861 6.283 6.283 0 0 0 1.865-.53 6.735 6.735 0 0 1-1.62 1.744Z" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function GitHubIcon(props) {
|
|
||||||
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) {
|
|
||||||
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 }) {
|
|
||||||
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 dark:border-white/5 sm:flex-row">
|
|
||||||
<p className="text-xs text-zinc-600 dark:text-zinc-400">
|
|
||||||
© Copyrights Plane {new Date().getFullYear()}. All rights reserved.
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-4">
|
|
||||||
<SocialLink href="https://twitter.com/planepowers" icon={TwitterIcon}>
|
|
||||||
Follow us on Twitter
|
|
||||||
</SocialLink>
|
|
||||||
<SocialLink href="https://github.com/makeplane" icon={GitHubIcon}>
|
|
||||||
Follow us on GitHub
|
|
||||||
</SocialLink>
|
|
||||||
<SocialLink
|
|
||||||
href="https://discord.com/invite/A92xrEGCge"
|
|
||||||
icon={DiscordIcon}
|
|
||||||
>
|
|
||||||
Join our Discord server
|
|
||||||
</SocialLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Footer() {
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<footer className="mx-auto max-w-2xl space-y-10 pb-16 lg:max-w-5xl">
|
|
||||||
<Feedback key={router.pathname} />
|
|
||||||
<PageNavigation />
|
|
||||||
<SmallPrint />
|
|
||||||
</footer>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
import { useId } from 'react'
|
|
||||||
|
|
||||||
export function GridPattern({ width, height, x, y, squares, ...props }) {
|
|
||||||
const 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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
import { Button } from '@/components/Button'
|
|
||||||
import { Heading } from '@/components/Heading'
|
|
||||||
|
|
||||||
const guides = [
|
|
||||||
{
|
|
||||||
href: '/authentication',
|
|
||||||
name: 'Authentication',
|
|
||||||
description: 'Learn how to authenticate your API requests.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/pagination',
|
|
||||||
name: 'Pagination',
|
|
||||||
description: 'Understand how to work with paginated responses.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/errors',
|
|
||||||
name: 'Errors',
|
|
||||||
description:
|
|
||||||
'Read about the different types of errors returned by the API.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/webhooks',
|
|
||||||
name: 'Webhooks',
|
|
||||||
description:
|
|
||||||
'Learn how to programmatically configure webhooks for your app.',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
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 dark:border-white/5 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
{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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
import { forwardRef } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { motion, useScroll, useTransform } from 'framer-motion'
|
|
||||||
|
|
||||||
import { Button } from '@/components/Button'
|
|
||||||
import { Logo } from '@/components/Logo'
|
|
||||||
import {
|
|
||||||
MobileNavigation,
|
|
||||||
useIsInsideMobileNavigation,
|
|
||||||
useMobileNavigationStore,
|
|
||||||
} from '@/components/MobileNavigation'
|
|
||||||
import { ModeToggle } from '@/components/ModeToggle'
|
|
||||||
import { MobileSearch, Search } from '@/components/Search'
|
|
||||||
|
|
||||||
function TopLevelNavItem({ href, children }) {
|
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
href={href}
|
|
||||||
className="text-sm leading-5 text-zinc-600 transition hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Header = forwardRef(function Header({ className }, ref) {
|
|
||||||
const { isOpen: mobileNavIsOpen } = useMobileNavigationStore()
|
|
||||||
const isInsideMobileNavigation = useIsInsideMobileNavigation()
|
|
||||||
|
|
||||||
const { scrollY } = useScroll()
|
|
||||||
const bgOpacityLight = useTransform(scrollY, [0, 72], [0.5, 0.9])
|
|
||||||
const bgOpacityDark = useTransform(scrollY, [0, 72], [0.2, 0.8])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
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:z-30 lg:px-8',
|
|
||||||
!isInsideMobileNavigation &&
|
|
||||||
'backdrop-blur-sm dark:backdrop-blur lg:left-72 xl:left-80',
|
|
||||||
isInsideMobileNavigation
|
|
||||||
? 'bg-white dark:bg-zinc-900'
|
|
||||||
: 'bg-white/[var(--bg-opacity-light)] dark:bg-zinc-900/[var(--bg-opacity-dark)]'
|
|
||||||
)}
|
|
||||||
style={{
|
|
||||||
'--bg-opacity-light': bgOpacityLight,
|
|
||||||
'--bg-opacity-dark': bgOpacityDark,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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 />
|
|
||||||
<Link href="/" aria-label="Home">
|
|
||||||
<Logo />
|
|
||||||
</Link>
|
|
||||||
</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://plane.so/">
|
|
||||||
Plane Cloud
|
|
||||||
</TopLevelNavItem>
|
|
||||||
<TopLevelNavItem href="https://github.com/makeplane/plane">
|
|
||||||
GitHub
|
|
||||||
</TopLevelNavItem>
|
|
||||||
<TopLevelNavItem href="https://discord.com/invite/A92xrEGCge">
|
|
||||||
Discord | Support
|
|
||||||
</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 />
|
|
||||||
<ModeToggle />
|
|
||||||
</div>
|
|
||||||
<div className="hidden min-[416px]:contents">
|
|
||||||
<Button href="https://app.plane.so/signin">Sign in</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
})
|
|
@ -1,102 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useInView } from 'framer-motion'
|
|
||||||
|
|
||||||
import { useSectionStore } from '@/components/SectionProvider'
|
|
||||||
import { Tag } from '@/components/Tag'
|
|
||||||
import { remToPx } from '@/lib/remToPx'
|
|
||||||
|
|
||||||
function AnchorIcon(props) {
|
|
||||||
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 }) {
|
|
||||||
if (!tag && !label) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Anchor({ id, inView, children }) {
|
|
||||||
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-[var(--width)] opacity-0 transition [--width:calc(2.625rem+0.5px+50%-min(50%,calc(theme(maxWidth.lg)+theme(spacing.8))))] group-hover:opacity-100 group-focus:opacity-100 md:block lg:z-50 2xl:[--width:theme(spacing.10)]">
|
|
||||||
<div className="group/anchor block h-5 w-5 rounded-lg bg-zinc-50 ring-1 ring-inset ring-zinc-300 transition 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 = 2,
|
|
||||||
children,
|
|
||||||
id,
|
|
||||||
tag,
|
|
||||||
label,
|
|
||||||
anchor = true,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const Component = `h${level}`
|
|
||||||
const ref = useRef()
|
|
||||||
const registerHeading = useSectionStore((s) => s.registerHeading)
|
|
||||||
|
|
||||||
const inView = useInView(ref, {
|
|
||||||
margin: `${remToPx(-3.5)}px 0px 0px 0px`,
|
|
||||||
amount: 'all',
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (level === 2) {
|
|
||||||
registerHeading({ id, ref, offsetRem: tag || label ? 8 : 6 })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Eyebrow tag={tag} label={label} />
|
|
||||||
<Component
|
|
||||||
ref={ref}
|
|
||||||
id={anchor ? id : undefined}
|
|
||||||
className={tag || label ? 'mt-2 scroll-mt-32' : 'scroll-mt-24'}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{anchor ? (
|
|
||||||
<Anchor id={id} inView={inView}>
|
|
||||||
{children}
|
|
||||||
</Anchor>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</Component>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
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 left-1/2 top-0 ml-[-38rem] h-[25rem] w-[81.25rem] dark:[mask-image:linear-gradient(white,transparent)]">
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-[#36b49f] to-[#DBFF75] opacity-40 [mask-image:radial-gradient(farthest-side_at_top,white,transparent)] dark:from-[#36b49f]/30 dark:to-[#DBFF75]/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-[-19rem] w-[69.5625rem] 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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
|
|
||||||
import { Footer } from '@/components/Footer'
|
|
||||||
import { Header } from '@/components/Header'
|
|
||||||
import { Logo } from '@/components/Logo'
|
|
||||||
import { Navigation } from '@/components/Navigation'
|
|
||||||
import { Prose } from '@/components/Prose'
|
|
||||||
import { SectionProvider } from '@/components/SectionProvider'
|
|
||||||
|
|
||||||
export function Layout({ children, sections = [] }) {
|
|
||||||
return (
|
|
||||||
<SectionProvider sections={sections}>
|
|
||||||
<div className="lg:ml-72 xl:ml-80">
|
|
||||||
<motion.header
|
|
||||||
layoutScroll
|
|
||||||
className="fixed inset-y-0 left-0 z-40 contents w-72 overflow-y-auto border-r border-zinc-900/10 px-6 pt-4 pb-8 dark:border-white/10 lg:block xl:w-80"
|
|
||||||
>
|
|
||||||
<div className="hidden lg:flex">
|
|
||||||
<Link href="/" aria-label="Home">
|
|
||||||
<Logo />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<Header />
|
|
||||||
<Navigation className="hidden lg:mt-5 lg:block" />
|
|
||||||
</motion.header>
|
|
||||||
<div className="relative px-4 pt-14 sm:px-6 lg:px-8">
|
|
||||||
<main className="py-16">
|
|
||||||
<Prose as="article">{children}</Prose>
|
|
||||||
</main>
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SectionProvider>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
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 dark:border-white/5 sm:grid-cols-2 xl:max-w-none xl:grid-cols-3">
|
|
||||||
{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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
export function Logo() {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
src="https://camo.githubusercontent.com/ef32512ae486a8cef8e000e74b2ff11c92c89c2cadb2d79674c6bd1599b99a56/68747470733a2f2f696b2e696d6167656b69742e696f2f77326f6b77627475322f706c616e652d6c6f676f5f306d383378756537522e706e673f696b2d73646b2d76657273696f6e3d6a6176617363726970742d312e342e33267570646174656441743d31363638383632373137303834"
|
|
||||||
height={100}
|
|
||||||
width={100}
|
|
||||||
alt="Plane"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
import { createContext, Fragment, useContext } from 'react'
|
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import create from 'zustand'
|
|
||||||
|
|
||||||
import { Header } from '@/components/Header'
|
|
||||||
import { Navigation } from '@/components/Navigation'
|
|
||||||
|
|
||||||
function MenuIcon(props) {
|
|
||||||
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) {
|
|
||||||
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)
|
|
||||||
|
|
||||||
export function useIsInsideMobileNavigation() {
|
|
||||||
return useContext(IsInsideMobileNavigationContext)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useMobileNavigationStore = create((set) => ({
|
|
||||||
isOpen: false,
|
|
||||||
open: () => set({ isOpen: true }),
|
|
||||||
close: () => set({ isOpen: false }),
|
|
||||||
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
|
|
||||||
}))
|
|
||||||
|
|
||||||
export function MobileNavigation() {
|
|
||||||
const isInsideMobileNavigation = useIsInsideMobileNavigation()
|
|
||||||
const { isOpen, toggle, close } = useMobileNavigationStore()
|
|
||||||
const ToggleIcon = isOpen ? XIcon : MenuIcon
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IsInsideMobileNavigationContext.Provider value={true}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 dark:hover:bg-white/5"
|
|
||||||
aria-label="Toggle navigation"
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<ToggleIcon className="w-2.5 stroke-zinc-900 dark:stroke-white" />
|
|
||||||
</button>
|
|
||||||
{!isInsideMobileNavigation && (
|
|
||||||
<Transition.Root show={isOpen} as={Fragment}>
|
|
||||||
<Dialog onClose={close} className="fixed inset-0 z-50 lg:hidden">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="duration-300 ease-out"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="duration-200 ease-in"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 top-14 bg-zinc-400/20 backdrop-blur-sm dark:bg-black/40" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<Dialog.Panel>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="duration-300 ease-out"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="duration-200 ease-in"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<Header />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="duration-500 ease-in-out"
|
|
||||||
enterFrom="-translate-x-full"
|
|
||||||
enterTo="translate-x-0"
|
|
||||||
leave="duration-500 ease-in-out"
|
|
||||||
leaveFrom="translate-x-0"
|
|
||||||
leaveTo="-translate-x-full"
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
layoutScroll
|
|
||||||
className="fixed left-0 top-14 bottom-0 w-full overflow-y-auto bg-white px-4 pt-6 pb-4 shadow-lg shadow-zinc-900/10 ring-1 ring-zinc-900/7.5 dark:bg-zinc-900 dark:ring-zinc-800 min-[416px]:max-w-sm sm:px-6 sm:pb-10"
|
|
||||||
>
|
|
||||||
<Navigation />
|
|
||||||
</motion.div>
|
|
||||||
</Transition.Child>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)}
|
|
||||||
</IsInsideMobileNavigationContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,54 +0,0 @@
|
|||||||
function SunIcon(props) {
|
|
||||||
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) {
|
|
||||||
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 ModeToggle() {
|
|
||||||
function disableTransitionsTemporarily() {
|
|
||||||
document.documentElement.classList.add('[&_*]:!transition-none')
|
|
||||||
window.setTimeout(() => {
|
|
||||||
document.documentElement.classList.remove('[&_*]:!transition-none')
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleMode() {
|
|
||||||
disableTransitionsTemporarily()
|
|
||||||
|
|
||||||
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
const isSystemDarkMode = darkModeMediaQuery.matches
|
|
||||||
const isDarkMode = document.documentElement.classList.toggle('dark')
|
|
||||||
|
|
||||||
if (isDarkMode === isSystemDarkMode) {
|
|
||||||
delete window.localStorage.isDarkMode
|
|
||||||
} else {
|
|
||||||
window.localStorage.isDarkMode = isDarkMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 dark:hover:bg-white/5"
|
|
||||||
aria-label="Toggle dark mode"
|
|
||||||
onClick={toggleMode}
|
|
||||||
>
|
|
||||||
<SunIcon className="h-5 w-5 stroke-zinc-900 dark:hidden" />
|
|
||||||
<MoonIcon className="hidden h-5 w-5 stroke-white dark:block" />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,233 +0,0 @@
|
|||||||
import { useRef } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import { AnimatePresence, motion, useIsPresent } from 'framer-motion'
|
|
||||||
|
|
||||||
import { Button } from '@/components/Button'
|
|
||||||
import { useIsInsideMobileNavigation } from '@/components/MobileNavigation'
|
|
||||||
import { useSectionStore } from '@/components/SectionProvider'
|
|
||||||
import { Tag } from '@/components/Tag'
|
|
||||||
import { remToPx } from '@/lib/remToPx'
|
|
||||||
|
|
||||||
function useInitialValue(value, condition = true) {
|
|
||||||
const initialValue = useRef(value).current
|
|
||||||
return condition ? initialValue : value
|
|
||||||
}
|
|
||||||
|
|
||||||
function TopLevelNavItem({ href, children }) {
|
|
||||||
return (
|
|
||||||
<li className="md:hidden">
|
|
||||||
<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}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavLink({ href, tag, active, isAnchorLink = false, children }) {
|
|
||||||
return (
|
|
||||||
<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>
|
|
||||||
{tag && (
|
|
||||||
<Tag variant="small" color="zinc">
|
|
||||||
{tag}
|
|
||||||
</Tag>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function VisibleSectionHighlight({ group, pathname }) {
|
|
||||||
const [sections, visibleSections] = useInitialValue(
|
|
||||||
[
|
|
||||||
useSectionStore((s) => s.sections),
|
|
||||||
useSectionStore((s) => s.visibleSections),
|
|
||||||
],
|
|
||||||
useIsInsideMobileNavigation()
|
|
||||||
)
|
|
||||||
|
|
||||||
const isPresent = useIsPresent()
|
|
||||||
const firstVisibleSectionIndex = Math.max(
|
|
||||||
0,
|
|
||||||
[{ id: '_top' }, ...sections].findIndex(
|
|
||||||
(section) => section.id === visibleSections[0]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
const itemHeight = remToPx(2)
|
|
||||||
const height = isPresent
|
|
||||||
? Math.max(1, visibleSections.length) * itemHeight
|
|
||||||
: itemHeight
|
|
||||||
const 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 }) {
|
|
||||||
const itemHeight = remToPx(2)
|
|
||||||
const offset = remToPx(0.25)
|
|
||||||
const activePageIndex = group.links.findIndex(
|
|
||||||
(link) => link.href === pathname
|
|
||||||
)
|
|
||||||
const 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 }) {
|
|
||||||
// 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.
|
|
||||||
const isInsideMobileNavigation = useIsInsideMobileNavigation()
|
|
||||||
const [router, sections] = useInitialValue(
|
|
||||||
[useRouter(), useSectionStore((s) => s.sections)],
|
|
||||||
isInsideMobileNavigation
|
|
||||||
)
|
|
||||||
|
|
||||||
const isActiveGroup =
|
|
||||||
group.links.findIndex((link) => link.href === router.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={router.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={router.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 === router.pathname}>
|
|
||||||
{link.title}
|
|
||||||
</NavLink>
|
|
||||||
<AnimatePresence mode="popLayout" initial={false}>
|
|
||||||
{link.href === router.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}
|
|
||||||
isAnchorLink
|
|
||||||
>
|
|
||||||
{section.title}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</motion.ul>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</motion.li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const navigation = [
|
|
||||||
{
|
|
||||||
title: 'Guides',
|
|
||||||
links: [
|
|
||||||
{ title: 'Introduction', href: '/' },
|
|
||||||
{ title: 'Quick Start', href: '/quick-start' },
|
|
||||||
{ title: 'Self Hosting', href: '/self-hosting' },
|
|
||||||
{ title: 'Architecture', href: '/architecture' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Plane App',
|
|
||||||
links: [
|
|
||||||
{ title: 'Workspace', href: '/workspace' },
|
|
||||||
{ title: 'Project', href: '/projects' },
|
|
||||||
{ title: 'Issues', href: '/issues' },
|
|
||||||
{ title: 'Cycles', href: '/cycles' },
|
|
||||||
{ title: 'Modules', href: '/modules' },
|
|
||||||
{ title: 'Integrations', href: '/integrations' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function Navigation(props) {
|
|
||||||
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'}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<li className="sticky bottom-0 z-10 mt-6 min-[416px]:hidden">
|
|
||||||
<Button href="#" variant="filled" className="w-full">
|
|
||||||
Sign in
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import clsx from 'clsx'
|
|
||||||
|
|
||||||
export function Prose({ as: Component = 'div', className, ...props }) {
|
|
||||||
return (
|
|
||||||
<Component
|
|
||||||
className={clsx(className, 'prose dark:prose-invert')}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,155 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion'
|
|
||||||
|
|
||||||
import { GridPattern } from '@/components/GridPattern'
|
|
||||||
import { Heading } from '@/components/Heading'
|
|
||||||
import { ChatBubbleIcon } from '@/components/icons/ChatBubbleIcon'
|
|
||||||
import { EnvelopeIcon } from '@/components/icons/EnvelopeIcon'
|
|
||||||
import { UserIcon } from '@/components/icons/UserIcon'
|
|
||||||
import { UsersIcon } from '@/components/icons/UsersIcon'
|
|
||||||
|
|
||||||
const resources = [
|
|
||||||
{
|
|
||||||
href: '/quick-start',
|
|
||||||
name: 'Quick Start',
|
|
||||||
description:
|
|
||||||
'Learn how to use Plane and follow the best practices of taking-off.',
|
|
||||||
icon: UserIcon,
|
|
||||||
pattern: {
|
|
||||||
y: 16,
|
|
||||||
squares: [
|
|
||||||
[0, 1],
|
|
||||||
[1, 3],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/self-hosting',
|
|
||||||
name: 'Self-host Plane',
|
|
||||||
description: 'Run Plane on your computer or development machine.',
|
|
||||||
icon: ChatBubbleIcon,
|
|
||||||
pattern: {
|
|
||||||
y: -6,
|
|
||||||
squares: [
|
|
||||||
[-1, 2],
|
|
||||||
[1, 3],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: '/plane-basics',
|
|
||||||
name: 'Plane Basics',
|
|
||||||
description:
|
|
||||||
'Learn about Plane basic features and kickstart your workspace',
|
|
||||||
icon: EnvelopeIcon,
|
|
||||||
pattern: {
|
|
||||||
y: 32,
|
|
||||||
squares: [
|
|
||||||
[0, 2],
|
|
||||||
[1, 4],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
href: 'https://discord.com/invite/A92xrEGCge',
|
|
||||||
name: 'Community',
|
|
||||||
description: 'Hang out with truly exceptional devs & designers on Discord.',
|
|
||||||
icon: UsersIcon,
|
|
||||||
pattern: {
|
|
||||||
y: 22,
|
|
||||||
squares: [[0, 1]],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
function ResourceIcon({ icon: 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 }) {
|
|
||||||
const maskImage = useMotionTemplate`radial-gradient(180px at ${mouseX}px ${mouseY}px, white, transparent)`
|
|
||||||
const style = { maskImage, WebkitMaskImage: maskImage }
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="pointer-events-none">
|
|
||||||
<div className="absolute inset-0 rounded-2xl transition duration-300 [mask-image:linear-gradient(white,transparent)] 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/[0.02] stroke-black/5 dark:fill-white/1 dark:stroke-white/2.5"
|
|
||||||
{...gridProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 rounded-2xl bg-gradient-to-r from-[#D7EDEA] to-[#F4FBDF] 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 }) {
|
|
||||||
const mouseX = useMotionValue(0)
|
|
||||||
const mouseY = useMotionValue(0)
|
|
||||||
|
|
||||||
function onMouseMove({ currentTarget, clientX, clientY }) {
|
|
||||||
const { 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-inset ring-zinc-900/7.5 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 font-semibold leading-7 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">
|
|
||||||
Resources
|
|
||||||
</Heading>
|
|
||||||
<div className="not-prose mt-4 grid grid-cols-1 gap-8 border-t border-zinc-900/5 pt-10 dark:border-white/5 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
{resources.map((resource) => (
|
|
||||||
<Resource key={resource.href} resource={resource} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,515 +0,0 @@
|
|||||||
import { forwardRef, Fragment, useEffect, useId, useRef, useState } from 'react'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import { createAutocomplete } from '@algolia/autocomplete-core'
|
|
||||||
import { getAlgoliaResults } from '@algolia/autocomplete-preset-algolia'
|
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
|
||||||
import algoliasearch from 'algoliasearch/lite'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
|
|
||||||
const searchClient = algoliasearch(
|
|
||||||
process.env.NEXT_PUBLIC_DOCSEARCH_APP_ID,
|
|
||||||
process.env.NEXT_PUBLIC_DOCSEARCH_API_KEY
|
|
||||||
)
|
|
||||||
|
|
||||||
function useAutocomplete() {
|
|
||||||
const id = useId()
|
|
||||||
const router = useRouter()
|
|
||||||
const [autocompleteState, setAutocompleteState] = useState({})
|
|
||||||
|
|
||||||
const [autocomplete] = useState(() =>
|
|
||||||
createAutocomplete({
|
|
||||||
id,
|
|
||||||
placeholder: 'Find something...',
|
|
||||||
defaultActiveItemId: 0,
|
|
||||||
onStateChange({ state }) {
|
|
||||||
setAutocompleteState(state)
|
|
||||||
},
|
|
||||||
shouldPanelOpen({ state }) {
|
|
||||||
return state.query !== ''
|
|
||||||
},
|
|
||||||
navigator: {
|
|
||||||
navigate({ itemUrl }) {
|
|
||||||
autocomplete.setIsOpen(true)
|
|
||||||
router.push(itemUrl)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
getSources() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
sourceId: 'documentation',
|
|
||||||
getItemInputValue({ item }) {
|
|
||||||
return item.query
|
|
||||||
},
|
|
||||||
getItemUrl({ item }) {
|
|
||||||
const url = new URL(item.url)
|
|
||||||
return `${url.pathname}${url.hash}`
|
|
||||||
},
|
|
||||||
onSelect({ itemUrl }) {
|
|
||||||
router.push(itemUrl)
|
|
||||||
},
|
|
||||||
getItems({ query }) {
|
|
||||||
return getAlgoliaResults({
|
|
||||||
searchClient,
|
|
||||||
queries: [
|
|
||||||
{
|
|
||||||
query,
|
|
||||||
indexName: process.env.NEXT_PUBLIC_DOCSEARCH_INDEX_NAME,
|
|
||||||
params: {
|
|
||||||
hitsPerPage: 5,
|
|
||||||
highlightPreTag:
|
|
||||||
'<mark class="underline bg-transparent text-blue-500">',
|
|
||||||
highlightPostTag: '</mark>',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
return { autocomplete, autocompleteState }
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveResult(result) {
|
|
||||||
const allLevels = Object.keys(result.hierarchy)
|
|
||||||
const hierarchy = Object.entries(result._highlightResult.hierarchy).filter(
|
|
||||||
([, { value }]) => Boolean(value)
|
|
||||||
)
|
|
||||||
const levels = hierarchy.map(([level]) => level)
|
|
||||||
|
|
||||||
const level =
|
|
||||||
result.type === 'content'
|
|
||||||
? levels.pop()
|
|
||||||
: levels
|
|
||||||
.filter(
|
|
||||||
(level) =>
|
|
||||||
allLevels.indexOf(level) <= allLevels.indexOf(result.type)
|
|
||||||
)
|
|
||||||
.pop()
|
|
||||||
|
|
||||||
return {
|
|
||||||
titleHtml: result._highlightResult.hierarchy[level].value,
|
|
||||||
hierarchyHtml: hierarchy
|
|
||||||
.slice(0, levels.indexOf(level))
|
|
||||||
.map(([, { value }]) => value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchIcon(props) {
|
|
||||||
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) {
|
|
||||||
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) {
|
|
||||||
const 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 SearchResult({ result, resultIndex, autocomplete, collection }) {
|
|
||||||
const id = useId()
|
|
||||||
const { titleHtml, hierarchyHtml } = resolveResult(result)
|
|
||||||
|
|
||||||
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"
|
|
||||||
dangerouslySetInnerHTML={{ __html: titleHtml }}
|
|
||||||
/>
|
|
||||||
{hierarchyHtml.length > 0 && (
|
|
||||||
<div
|
|
||||||
id={`${id}-hierarchy`}
|
|
||||||
aria-hidden="true"
|
|
||||||
className="mt-1 truncate whitespace-nowrap text-2xs text-zinc-500"
|
|
||||||
>
|
|
||||||
{hierarchyHtml.map((item, itemIndex, items) => (
|
|
||||||
<Fragment key={itemIndex}>
|
|
||||||
<span dangerouslySetInnerHTML={{ __html: item }} />
|
|
||||||
<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 }) {
|
|
||||||
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="break-words font-semibold text-zinc-900 dark:text-white">
|
|
||||||
‘{query}’
|
|
||||||
</strong>
|
|
||||||
. Please try again.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul role="list" {...autocomplete.getListProps()}>
|
|
||||||
{collection.items.map((result, resultIndex) => (
|
|
||||||
<SearchResult
|
|
||||||
key={result.objectID}
|
|
||||||
result={result}
|
|
||||||
resultIndex={resultIndex}
|
|
||||||
autocomplete={autocomplete}
|
|
||||||
collection={collection}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const SearchInput = forwardRef(function SearchInput(
|
|
||||||
{ autocomplete, autocompleteState, onClose },
|
|
||||||
inputRef
|
|
||||||
) {
|
|
||||||
const inputProps = autocomplete.getInputProps({})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="group relative flex h-12">
|
|
||||||
<SearchIcon className="pointer-events-none absolute left-3 top-0 h-full w-5 stroke-zinc-500" />
|
|
||||||
<input
|
|
||||||
ref={inputRef}
|
|
||||||
className={clsx(
|
|
||||||
'flex-auto appearance-none bg-transparent pl-10 text-zinc-900 outline-none placeholder:text-zinc-500 focus:w-full focus:flex-none dark:text-white sm:text-sm [&::-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.
|
|
||||||
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 AlgoliaLogo(props) {
|
|
||||||
return (
|
|
||||||
<svg viewBox="0 0 71 16" role="img" aria-label="Algolia" {...props}>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M34.98 8.81V.19a.189.189 0 0 0-.218-.186l-1.615.254a.19.19 0 0 0-.16.187l.006 8.741c0 .414 0 2.966 3.07 3.056a.19.19 0 0 0 .195-.19v-1.304a.187.187 0 0 0-.164-.187c-1.115-.128-1.115-1.522-1.115-1.75v-.002Z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
<path d="M61.605 3.352H59.98a.189.189 0 0 0-.189.189v8.514c0 .104.085.189.189.189h1.625a.189.189 0 0 0 .188-.19V3.542a.189.189 0 0 0-.188-.189Z" />
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M59.98 2.285h1.625a.189.189 0 0 0 .188-.189V.19a.189.189 0 0 0-.218-.187l-1.624.255a.189.189 0 0 0-.16.186v1.652c0 .104.085.189.189.189ZM57.172 8.81V.19a.189.189 0 0 0-.218-.186l-1.615.254a.19.19 0 0 0-.16.187l.006 8.741c0 .414 0 2.966 3.07 3.056a.19.19 0 0 0 .196-.19v-1.304a.187.187 0 0 0-.164-.187c-1.115-.128-1.115-1.522-1.115-1.75v-.002ZM52.946 4.568a3.628 3.628 0 0 0-1.304-.906 4.347 4.347 0 0 0-1.666-.315c-.601 0-1.157.101-1.662.315a3.822 3.822 0 0 0-1.304.906c-.367.39-.652.86-.856 1.408-.204.55-.296 1.196-.296 1.868 0 .671.103 1.18.306 1.734.204.554.484 1.027.846 1.42.361.39.795.691 1.3.91.504.218 1.283.33 1.676.335.392 0 1.177-.122 1.686-.335.51-.214.943-.52 1.305-.91.361-.393.641-.866.84-1.42.199-.555.295-1.063.295-1.734 0-.672-.107-1.318-.32-1.868a4.203 4.203 0 0 0-.846-1.408Zm-1.421 5.239c-.367.504-.882.758-1.539.758-.657 0-1.172-.25-1.539-.758-.367-.504-.55-1.088-.55-1.958 0-.86.178-1.573.545-2.076.367-.504.882-.752 1.538-.752.658 0 1.172.248 1.539.752.367.498.556 1.215.556 2.076 0 .87-.184 1.449-.55 1.958ZM29.35 3.352H27.77c-1.547 0-2.909.815-3.703 2.051a4.643 4.643 0 0 0-.736 2.519 4.611 4.611 0 0 0 1.949 3.783 2.574 2.574 0 0 0 1.542.428l.034-.002.084-.006.032-.004.088-.011.02-.003c1.052-.163 1.97-.986 2.268-2.01v1.85c0 .105.085.19.19.19h1.612a.189.189 0 0 0 .19-.19V3.541a.189.189 0 0 0-.19-.189H29.35Zm0 6.62c-.39.326-.896.448-1.435.484l-.016.002a1.68 1.68 0 0 1-.107.003c-1.352 0-2.468-1.149-2.468-2.54 0-.328.063-.64.173-.927.36-.932 1.241-1.591 2.274-1.591h1.578v4.57ZM69.009 3.352H67.43c-1.547 0-2.908.815-3.703 2.051a4.643 4.643 0 0 0-.736 2.519 4.611 4.611 0 0 0 1.949 3.783 2.575 2.575 0 0 0 1.542.428l.034-.002.084-.006.033-.004.087-.011.02-.003c1.053-.163 1.97-.986 2.269-2.01v1.85c0 .105.084.19.188.19h1.614a.189.189 0 0 0 .188-.19V3.541a.189.189 0 0 0-.188-.189h-1.802Zm0 6.62c-.39.326-.895.448-1.435.484l-.016.002a1.675 1.675 0 0 1-.107.003c-1.352 0-2.468-1.149-2.468-2.54 0-.328.063-.64.174-.927.359-.932 1.24-1.591 2.273-1.591h1.579v4.57ZM42.775 3.352h-1.578c-1.547 0-2.909.815-3.704 2.051a4.63 4.63 0 0 0-.735 2.519 4.6 4.6 0 0 0 1.65 3.555c.094.083.194.16.298.228a2.575 2.575 0 0 0 2.966-.08c.52-.37.924-.913 1.103-1.527v1.608h-.004v.354c0 .7-.182 1.225-.554 1.58-.372.354-.994.532-1.864.532-.356 0-.921-.02-1.491-.078a.19.19 0 0 0-.2.136l-.41 1.379a.19.19 0 0 0 .155.24c.688.1 1.36.15 1.748.15 1.565 0 2.725-.343 3.484-1.03.688-.621 1.061-1.564 1.127-2.832V3.54a.189.189 0 0 0-.19-.189h-1.801Zm0 2.051s.021 4.452 0 4.587c-.386.312-.867.435-1.391.47l-.016.001a1.751 1.751 0 0 1-.233 0c-1.293-.067-2.385-1.192-2.385-2.54 0-.327.063-.64.174-.927.359-.931 1.24-1.591 2.273-1.591h1.578Z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
<path d="M8.725.001C4.356.001.795 3.523.732 7.877c-.064 4.422 3.524 8.085 7.946 8.111a7.94 7.94 0 0 0 3.849-.96.187.187 0 0 0 .034-.305l-.748-.663a.528.528 0 0 0-.555-.094 6.461 6.461 0 0 1-2.614.513c-3.574-.043-6.46-3.016-6.404-6.59a6.493 6.493 0 0 1 6.485-6.38h6.485v11.527l-3.68-3.269a.271.271 0 0 0-.397.042 3.014 3.014 0 0 1-5.416-1.583 3.02 3.02 0 0 1 3.008-3.248 3.02 3.02 0 0 1 3.005 2.75.537.537 0 0 0 .176.356l.958.85a.187.187 0 0 0 .308-.106c.07-.37.094-.755.067-1.15a4.536 4.536 0 0 0-4.23-4.2A4.53 4.53 0 0 0 4.203 7.87c-.067 2.467 1.954 4.593 4.421 4.648a4.498 4.498 0 0 0 2.756-.863l4.808 4.262a.32.32 0 0 0 .531-.239V.304a.304.304 0 0 0-.303-.303h-7.69Z" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchButton(props) {
|
|
||||||
const [modifierKey, setModifierKey] = useState()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setModifierKey(
|
|
||||||
/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl '
|
|
||||||
)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="hidden h-8 w-full items-center gap-2 rounded-full bg-white pl-2 pr-3 text-sm text-zinc-500 ring-1 ring-zinc-900/10 transition hover:ring-zinc-900/20 dark:bg-white/5 dark:text-zinc-400 dark:ring-inset dark:ring-white/10 dark:hover:ring-white/20 lg:flex focus:[&:not(:focus-visible)]:outline-none"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 dark:hover:bg-white/5 lg:hidden focus:[&:not(:focus-visible)]:outline-none"
|
|
||||||
aria-label="Find something..."
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SearchIcon className="h-5 w-5 stroke-zinc-900 dark:stroke-white" />
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchDialog({ open, setOpen, className }) {
|
|
||||||
const router = useRouter()
|
|
||||||
const formRef = useRef()
|
|
||||||
const panelRef = useRef()
|
|
||||||
const inputRef = useRef()
|
|
||||||
const { autocomplete, autocompleteState } = useAutocomplete()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRouteChange() {
|
|
||||||
setOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
router.events.on('routeChangeStart', onRouteChange)
|
|
||||||
router.events.on('hashChangeStart', onRouteChange)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
router.events.off('routeChangeStart', onRouteChange)
|
|
||||||
router.events.off('hashChangeStart', onRouteChange)
|
|
||||||
}
|
|
||||||
}, [open, setOpen, router])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKeyDown(event) {
|
|
||||||
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
|
|
||||||
event.preventDefault()
|
|
||||||
setOpen(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('keydown', onKeyDown)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', onKeyDown)
|
|
||||||
}
|
|
||||||
}, [open, setOpen])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Transition.Root
|
|
||||||
show={open}
|
|
||||||
as={Fragment}
|
|
||||||
afterLeave={() => autocomplete.setQuery('')}
|
|
||||||
>
|
|
||||||
<Dialog
|
|
||||||
onClose={setOpen}
|
|
||||||
className={clsx('fixed inset-0 z-50', className)}
|
|
||||||
>
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0"
|
|
||||||
enterTo="opacity-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 bg-zinc-400/25 backdrop-blur-sm dark:bg-black/40" />
|
|
||||||
</Transition.Child>
|
|
||||||
|
|
||||||
<div className="fixed inset-0 overflow-y-auto px-4 py-4 sm:py-20 sm:px-6 md:py-32 lg:px-8 lg:py-[15vh]">
|
|
||||||
<Transition.Child
|
|
||||||
as={Fragment}
|
|
||||||
enter="ease-out duration-300"
|
|
||||||
enterFrom="opacity-0 scale-95"
|
|
||||||
enterTo="opacity-100 scale-100"
|
|
||||||
leave="ease-in duration-200"
|
|
||||||
leaveFrom="opacity-100 scale-100"
|
|
||||||
leaveTo="opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Dialog.Panel className="mx-auto overflow-hidden rounded-lg bg-zinc-50 shadow-xl ring-1 ring-zinc-900/7.5 dark:bg-zinc-900 dark:ring-zinc-800 sm:max-w-xl">
|
|
||||||
<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]}
|
|
||||||
/>
|
|
||||||
<p className="flex items-center justify-end gap-2 border-t border-zinc-100 px-4 py-2 text-xs text-zinc-400 dark:border-zinc-800 dark:text-zinc-500">
|
|
||||||
Search by{' '}
|
|
||||||
<AlgoliaLogo className="h-4 fill-[#003DFF] dark:fill-zinc-400" />
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Dialog.Panel>
|
|
||||||
</Transition.Child>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
</Transition.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function useSearchProps() {
|
|
||||||
const buttonRef = useRef()
|
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
|
|
||||||
return {
|
|
||||||
buttonProps: {
|
|
||||||
ref: buttonRef,
|
|
||||||
onClick() {
|
|
||||||
setOpen(true)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dialogProps: {
|
|
||||||
open,
|
|
||||||
setOpen(open) {
|
|
||||||
const { width, height } = buttonRef.current.getBoundingClientRect()
|
|
||||||
if (!open || (width !== 0 && height !== 0)) {
|
|
||||||
setOpen(open)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Search() {
|
|
||||||
const [modifierKey, setModifierKey] = useState()
|
|
||||||
const { 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 pl-2 pr-3 text-sm text-zinc-500 ring-1 ring-zinc-900/10 transition hover:ring-zinc-900/20 dark:bg-white/5 dark:text-zinc-400 dark:ring-inset dark:ring-white/10 dark:hover:ring-white/20 lg:flex focus:[&:not(:focus-visible)]:outline-none"
|
|
||||||
{...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>
|
|
||||||
<SearchDialog className="hidden lg:block" {...dialogProps} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MobileSearch() {
|
|
||||||
const { buttonProps, dialogProps } = useSearchProps()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="contents lg:hidden">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex h-6 w-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 dark:hover:bg-white/5 lg:hidden focus:[&:not(:focus-visible)]:outline-none"
|
|
||||||
aria-label="Find something..."
|
|
||||||
{...buttonProps}
|
|
||||||
>
|
|
||||||
<SearchIcon className="h-5 w-5 stroke-zinc-900 dark:stroke-white" />
|
|
||||||
</button>
|
|
||||||
<SearchDialog className="lg:hidden" {...dialogProps} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,115 +0,0 @@
|
|||||||
import {
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useState,
|
|
||||||
} from 'react'
|
|
||||||
import { createStore, useStore } from 'zustand'
|
|
||||||
|
|
||||||
import { remToPx } from '@/lib/remToPx'
|
|
||||||
|
|
||||||
function createSectionStore(sections) {
|
|
||||||
return createStore((set) => ({
|
|
||||||
sections,
|
|
||||||
visibleSections: [],
|
|
||||||
setVisibleSections: (visibleSections) =>
|
|
||||||
set((state) =>
|
|
||||||
state.visibleSections.join() === visibleSections.join()
|
|
||||||
? {}
|
|
||||||
: { visibleSections }
|
|
||||||
),
|
|
||||||
registerHeading: ({ id, ref, offsetRem }) =>
|
|
||||||
set((state) => ({
|
|
||||||
sections: state.sections.map((section) => {
|
|
||||||
if (section.id === id) {
|
|
||||||
return {
|
|
||||||
...section,
|
|
||||||
headingRef: ref,
|
|
||||||
offsetRem,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return section
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function useVisibleSections(sectionStore) {
|
|
||||||
const setVisibleSections = useStore(sectionStore, (s) => s.setVisibleSections)
|
|
||||||
const sections = useStore(sectionStore, (s) => s.sections)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function checkVisibleSections() {
|
|
||||||
const { innerHeight, scrollY } = window
|
|
||||||
const newVisibleSections = []
|
|
||||||
|
|
||||||
for (
|
|
||||||
let sectionIndex = 0;
|
|
||||||
sectionIndex < sections.length;
|
|
||||||
sectionIndex++
|
|
||||||
) {
|
|
||||||
const { id, headingRef, offsetRem } = sections[sectionIndex]
|
|
||||||
const offset = remToPx(offsetRem)
|
|
||||||
const top = headingRef.current.getBoundingClientRect().top + scrollY
|
|
||||||
|
|
||||||
if (sectionIndex === 0 && top - offset > scrollY) {
|
|
||||||
newVisibleSections.push('_top')
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextSection = sections[sectionIndex + 1]
|
|
||||||
const 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
const 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()
|
|
||||||
|
|
||||||
const useIsomorphicLayoutEffect =
|
|
||||||
typeof window === 'undefined' ? useEffect : useLayoutEffect
|
|
||||||
|
|
||||||
export function SectionProvider({ sections, children }) {
|
|
||||||
const [sectionStore] = useState(() => createSectionStore(sections))
|
|
||||||
|
|
||||||
useVisibleSections(sectionStore)
|
|
||||||
|
|
||||||
useIsomorphicLayoutEffect(() => {
|
|
||||||
sectionStore.setState({ sections })
|
|
||||||
}, [sectionStore, sections])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SectionStoreContext.Provider value={sectionStore}>
|
|
||||||
{children}
|
|
||||||
</SectionStoreContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSectionStore(selector) {
|
|
||||||
const store = useContext(SectionStoreContext)
|
|
||||||
return useStore(store, selector)
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
import clsx from 'clsx'
|
|
||||||
|
|
||||||
const variantStyles = {
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
sky: {
|
|
||||||
small: 'text-sky-500',
|
|
||||||
medium:
|
|
||||||
'ring-sky-300 bg-sky-400/10 text-sky-500 dark:ring-sky-400/30 dark:bg-sky-400/10 dark:text-sky-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: 'sky',
|
|
||||||
put: 'amber',
|
|
||||||
delete: 'rose',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Tag({
|
|
||||||
children,
|
|
||||||
variant = 'medium',
|
|
||||||
color = valueColorMap[children.toLowerCase()] ?? 'blue',
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
'font-mono text-[0.625rem] font-semibold leading-6',
|
|
||||||
variantStyles[variant],
|
|
||||||
colorStyles[color][variant]
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function BellIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
export function BoltIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function BookIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
export function CalendarIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
export function CartIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function ChatBubbleIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function CheckIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function ChevronRightLeftIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function ClipboardIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
export function CogIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function CopyIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function DocumentIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function EnvelopeIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function FaceSmileIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
export function FolderIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
export function LinkIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function ListIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
export function MagnifyingGlassIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
export function MapPinIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
export function PackageIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function PaperAirplaneIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
export function PaperClipIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function ShapesIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
export function ShirtIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
export function SquaresPlusIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
export function TagIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
export function UserIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
export function UsersIcon(props) {
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
|
|
||||||
import { Heading } from '@/components/Heading'
|
|
||||||
|
|
||||||
export const a = Link
|
|
||||||
export { Button } from '@/components/Button'
|
|
||||||
export { CodeGroup, Code as code, Pre as pre } from '@/components/Code'
|
|
||||||
|
|
||||||
export const h2 = function H2(props) {
|
|
||||||
return <Heading level={2} {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function InfoIcon(props) {
|
|
||||||
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 }) {
|
|
||||||
return (
|
|
||||||
<div className="my-6 flex gap-2.5 rounded-2xl border border-blue-500/20 bg-blue-50/50 p-4 leading-6 text-blue-900 dark:border-blue-500/30 dark:bg-blue-500/5 dark:text-blue-200 dark:[--tw-prose-links:theme(colors.white)] dark:[--tw-prose-links-hover:theme(colors.blue.300)]">
|
|
||||||
<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 }) {
|
|
||||||
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 }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={clsx(
|
|
||||||
'[&>:first-child]:mt-0 [&>:last-child]:mb-0',
|
|
||||||
sticky && 'xl:sticky xl:top-24'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Properties({ children }) {
|
|
||||||
return (
|
|
||||||
<div className="my-6">
|
|
||||||
<ul
|
|
||||||
role="list"
|
|
||||||
className="m-0 max-w-[calc(theme(maxWidth.lg)-theme(spacing.8))] list-none divide-y divide-zinc-900/5 p-0 dark:divide-white/5"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Property({ name, type, children }) {
|
|
||||||
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>
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 1.8 KiB |
@ -1,4 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1,10 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1,13 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 864 B |
@ -1,4 +0,0 @@
|
|||||||
<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>
|
|
Before Width: | Height: | Size: 561 B |
@ -1,8 +0,0 @@
|
|||||||
export function remToPx(remValue) {
|
|
||||||
const rootFontSize =
|
|
||||||
typeof window === 'undefined'
|
|
||||||
? 16
|
|
||||||
: parseFloat(window.getComputedStyle(document.documentElement).fontSize)
|
|
||||||
|
|
||||||
return parseFloat(remValue) * rootFontSize
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import Head from 'next/head'
|
|
||||||
import { Router, useRouter } from 'next/router'
|
|
||||||
import { MDXProvider } from '@mdx-js/react'
|
|
||||||
|
|
||||||
import { Layout } from '@/components/Layout'
|
|
||||||
import * as mdxComponents from '@/components/mdx'
|
|
||||||
import { useMobileNavigationStore } from '@/components/MobileNavigation'
|
|
||||||
|
|
||||||
import '@/styles/tailwind.css'
|
|
||||||
import 'focus-visible'
|
|
||||||
|
|
||||||
function onRouteChange() {
|
|
||||||
useMobileNavigationStore.getState().close()
|
|
||||||
}
|
|
||||||
|
|
||||||
Router.events.on('hashChangeStart', onRouteChange)
|
|
||||||
Router.events.on('routeChangeComplete', onRouteChange)
|
|
||||||
Router.events.on('routeChangeError', onRouteChange)
|
|
||||||
|
|
||||||
export default function App({ Component, pageProps }) {
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
{router.pathname === '/' ? (
|
|
||||||
<title>Plane Documentation</title>
|
|
||||||
) : (
|
|
||||||
<title>{`${pageProps.title} - Plane Documentation`}</title>
|
|
||||||
)}
|
|
||||||
<meta name="description" content={pageProps.description} />
|
|
||||||
</Head>
|
|
||||||
<MDXProvider components={mdxComponents}>
|
|
||||||
<Layout {...pageProps}>
|
|
||||||
<Component {...pageProps} />
|
|
||||||
</Layout>
|
|
||||||
</MDXProvider>
|
|
||||||
<script
|
|
||||||
defer
|
|
||||||
data-domain="docs.plane.so"
|
|
||||||
src="https://plausible.io/js/script.js"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
import { Head, Html, Main, NextScript } from 'next/document'
|
|
||||||
|
|
||||||
const modeScript = `
|
|
||||||
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
|
||||||
|
|
||||||
updateMode()
|
|
||||||
darkModeMediaQuery.addEventListener('change', updateModeWithoutTransitions)
|
|
||||||
window.addEventListener('storage', updateModeWithoutTransitions)
|
|
||||||
|
|
||||||
function updateMode() {
|
|
||||||
let isSystemDarkMode = darkModeMediaQuery.matches
|
|
||||||
let isDarkMode = window.localStorage.isDarkMode === 'true' || (!('isDarkMode' in window.localStorage) && isSystemDarkMode)
|
|
||||||
|
|
||||||
if (isDarkMode) {
|
|
||||||
document.documentElement.classList.add('dark')
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDarkMode === isSystemDarkMode) {
|
|
||||||
delete window.localStorage.isDarkMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableTransitionsTemporarily() {
|
|
||||||
document.documentElement.classList.add('[&_*]:!transition-none')
|
|
||||||
window.setTimeout(() => {
|
|
||||||
document.documentElement.classList.remove('[&_*]:!transition-none')
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateModeWithoutTransitions() {
|
|
||||||
disableTransitionsTemporarily()
|
|
||||||
updateMode()
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function Document() {
|
|
||||||
return (
|
|
||||||
<Html lang="en">
|
|
||||||
<Head>
|
|
||||||
<script dangerouslySetInnerHTML={{ __html: modeScript }} />
|
|
||||||
</Head>
|
|
||||||
<body className="bg-white antialiased dark:bg-zinc-900">
|
|
||||||
<Main />
|
|
||||||
<NextScript />
|
|
||||||
</body>
|
|
||||||
</Html>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export const description = 'Plane Architecture'
|
|
||||||
|
|
||||||
# Plane Architecture
|
|
||||||
|
|
||||||
Coming very soon.
|
|
@ -1,5 +0,0 @@
|
|||||||
export const description = 'Cycles'
|
|
||||||
|
|
||||||
# Cycles
|
|
||||||
|
|
||||||
Coming very soon.
|
|
@ -1,39 +0,0 @@
|
|||||||
import { Guides } from '@/components/Guides'
|
|
||||||
import { Resources } from '@/components/Resources'
|
|
||||||
import { HeroPattern } from '@/components/HeroPattern'
|
|
||||||
import { Heading } from '@/components/Heading'
|
|
||||||
import { Button } from '@/components/Button'
|
|
||||||
|
|
||||||
export const description = '.'
|
|
||||||
|
|
||||||
export const sections = [
|
|
||||||
{ title: 'Resources', id: 'resources' },
|
|
||||||
{ title: 'Contributing', id: 'contributing' },
|
|
||||||
]
|
|
||||||
|
|
||||||
<HeroPattern />
|
|
||||||
|
|
||||||
# Plane Documentation
|
|
||||||
|
|
||||||
This is where the learning begins and the veterans return for their references. This is the Plane documentation.
|
|
||||||
|
|
||||||
<div className="not-prose mb-16 mt-6 flex gap-3">
|
|
||||||
<Button href="/quickstart" arrow="right" children="Quickstart" />
|
|
||||||
<Button
|
|
||||||
href="https://github.com/makeplane/plane"
|
|
||||||
variant="outline"
|
|
||||||
children="Star us on GitHub"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Heading level={2} id="what-is-plane">
|
|
||||||
What is Plane?
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
Plane is an open-source project planning tool that is designed to help individuals and teams streamline their issues, sprints, and product roadmaps. It is easy to use and can be accessed by anyone, making it an ideal choice for a wide range of projects and organizations.
|
|
||||||
|
|
||||||
<Resources />
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Each page footer contains an "Edit on GitHub" Link. Make a pull request, and we'll merge it!
|
|
@ -1,65 +0,0 @@
|
|||||||
import { Note } from '@/components/mdx'
|
|
||||||
|
|
||||||
export const description = 'Integrations'
|
|
||||||
|
|
||||||
export const sections = [{ title: 'GitHub', id: 'github' }]
|
|
||||||
|
|
||||||
# Integrations (Dev Release)
|
|
||||||
|
|
||||||
Plane's integrations make it easy for users to connect their Workspace and
|
|
||||||
projects to popular third-party tools. This enables seamless management of
|
|
||||||
issues and notifications, all from within the Plane platform.
|
|
||||||
|
|
||||||
Rather than
|
|
||||||
having to switch back and forth between different tools and interfaces, users
|
|
||||||
can access and manage their third-party tools directly from within the Plane
|
|
||||||
platform. This streamlines workflows and improves efficiency by providing a
|
|
||||||
centralized hub for all project-related activities.
|
|
||||||
|
|
||||||
## GitHub Sync
|
|
||||||
|
|
||||||
GitHub Sync allows users to connect any GitHub repository to a Plane project,
|
|
||||||
enabling cross-synchronization of issues between Plane and GitHub in both
|
|
||||||
directions.
|
|
||||||
|
|
||||||
By connecting a GitHub repository to a Plane project, users can easily track
|
|
||||||
issues and changes in both platforms. This allows for a more streamlined
|
|
||||||
workflow, as users can manage their GitHub issues and pull requests from
|
|
||||||
within the Plane platform.
|
|
||||||
|
|
||||||
The bi-directional synchronization means that changes made in either platform
|
|
||||||
will be reflected in the other. For example, if a user creates a new issue in
|
|
||||||
Plane, it will automatically be synced with the corresponding repository in
|
|
||||||
GitHub. Similarly, if a user closes an issue in GitHub, it will be reflected
|
|
||||||
in Plane as well.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Plane is still in development stage, in case if there are any hiccups in
|
|
||||||
Integrations, do report us on our [Discord]() or [Github]().
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Plane + GitHub Sync Attribute Overivew
|
|
||||||
|
|
||||||
![Connect workspace](https://ik.imagekit.io/rdws4iz4v/Plane_Arch__1_.png?ik-sdk-version=javascript-1.4.3&updatedAt=1677529077471)
|
|
||||||
|
|
||||||
## Configuring GitHub Integration
|
|
||||||
|
|
||||||
**Step One: Add GitHub Integration to your workspace**
|
|
||||||
|
|
||||||
![Connect workspace](https://ik.imagekit.io/rdws4iz4v/ezgif-2-c71e43f5de.gif?ik-sdk-version=javascript-1.4.3&updatedAt=1677526036094)
|
|
||||||
|
|
||||||
**Step Two: Connect GitHub repository with project**
|
|
||||||
|
|
||||||
![Connect repo to project](https://ik.imagekit.io/rdws4iz4v/ezgif-2-cea843f5f8.gif?ik-sdk-version=javascript-1.4.3&updatedAt=1677526558466)
|
|
||||||
|
|
||||||
**Step Three: Create new or use existing issue on Plane**
|
|
||||||
|
|
||||||
![Issue on Plane](https://ik.imagekit.io/rdws4iz4v/ezgif-5-71aa60e87f.gif?ik-sdk-version=javascript-1.4.3&updatedAt=1677527704371)
|
|
||||||
|
|
||||||
**Step Four: Add GitHub label to Plane issue (Plane to GitHub)**
|
|
||||||
|
|
||||||
![GitHub label on Plane issue](https://ik.imagekit.io/rdws4iz4v/ezgif-5-58a5ac3b67.gif?ik-sdk-version=javascript-1.4.3&updatedAt=1677527944556)
|
|
||||||
|
|
||||||
**Step Five: Add Plane label to GitHub issue (GitHub to Plane)**
|
|
||||||
|
|
||||||
![Plane label on GitHub issue](https://ik.imagekit.io/rdws4iz4v/ezgif-5-ccce270df6.gif?ik-sdk-version=javascript-1.4.3&updatedAt=1677528064644)
|
|
@ -1,5 +0,0 @@
|
|||||||
export const description = 'Issues'
|
|
||||||
|
|
||||||
# Issues
|
|
||||||
|
|
||||||
Coming very soon.
|
|
@ -1,5 +0,0 @@
|
|||||||
export const description = 'Modules'
|
|
||||||
|
|
||||||
# Modules
|
|
||||||
|
|
||||||
Coming very soon.
|
|
@ -1,3 +0,0 @@
|
|||||||
# Plane Basics
|
|
||||||
|
|
||||||
Coming Soon.
|
|
@ -1,132 +0,0 @@
|
|||||||
import { Heading } from '@/components/Heading'
|
|
||||||
import { Note } from '@/components/mdx'
|
|
||||||
|
|
||||||
# Project setup
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Projects let you manage teams and tasks within your Workspace. After creating
|
|
||||||
your [Workspace](/workspace), you will need to create a new project.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Before inviting a user to a project, you'll need to invite them to the
|
|
||||||
workspace. Then, you can choose to add them to the project from the list of
|
|
||||||
users on the workspace.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
<Heading level={2} id="project-attributes">
|
|
||||||
Key project attributes
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
Every project comes with two key attributes that make it easier to plan and set permissions for users on the workspace.
|
|
||||||
|
|
||||||
1. **`Identifier`**: The project identifier is attached to every issue in the project to make it easier to track and differentiate each issue across projects and the workspace.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
You can always choose to update your identifier. This will only replace the ID
|
|
||||||
string on the issue, but not the number next to it. Note that identifiers can
|
|
||||||
only contain uppercase text and no special characters or numbers.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
These options can be found under the `General` tab inside `Project>Settings` ⬇️
|
|
||||||
|
|
||||||
`<project-name>/projects/<id>/settings`.
|
|
||||||
|
|
||||||
2. **`Network`**: To restrict other users on the workspace, you can set the network of the project to either Private or Secret. When made public, all users on the workspace can choose to join the project. When set to secret, admins need to invite workspace members to join the project.
|
|
||||||
|
|
||||||
<Heading level={2} id="project-controls">
|
|
||||||
Project controls
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
When working on multiple projects, you can set project controls to make it
|
|
||||||
easier. These controls include a project lead and default issue assignee.
|
|
||||||
|
|
||||||
- **Project lead**: The project lead will help you identify and connect with the project leader to make it easier for you to assign tasks or communicate.
|
|
||||||
|
|
||||||
- **Default issue assignee**: Everything inside a project is broken down into [issues](/issues). If you want every issue to be assigned, you can choose to set a default assignee for times when an issue is created without any assignees
|
|
||||||
|
|
||||||
These options can be found under the `Control` tab inside `Project>Settings` ⬇️
|
|
||||||
|
|
||||||
`<project-name>/projects/<id>/settings/control`.
|
|
||||||
|
|
||||||
<Heading level={2} id="project-controls">
|
|
||||||
Project members
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
When the project is set to the `Public` network under project controls, it is open to all members on the workspace. When set to `Secret` network, workspace members must be invited to the project to gain access.
|
|
||||||
|
|
||||||
For every member that is invited to the project, you can control their roles
|
|
||||||
just [like on the workspace](workspace/#workspace-roles). All roles (Admin, Member, Guest, Viewer) are
|
|
||||||
supported.
|
|
||||||
|
|
||||||
These options can be found under the `Members` tab inside `Project>Settings` ⬇️
|
|
||||||
|
|
||||||
`<project-name>/projects/<id>/settings/members`.
|
|
||||||
|
|
||||||
<Heading level={2} id="project-states">
|
|
||||||
Project states
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
To organize issues based on the principles of project management, we allow you
|
|
||||||
to create custom states in the default grouped states. This will make it
|
|
||||||
easier to use project views and filtering.
|
|
||||||
|
|
||||||
Default states:
|
|
||||||
|
|
||||||
- Backlog
|
|
||||||
- Unstarted
|
|
||||||
- Started
|
|
||||||
- Completed
|
|
||||||
- Cancelled
|
|
||||||
|
|
||||||
Inside these default states, you can create as many states as you like based
|
|
||||||
on the type of default state. In the future, we plan to allow users to create
|
|
||||||
or customize the default state based on their requirements. Please [raise a
|
|
||||||
feature request](https://github.com/makeplane/plane/issues) on our issues, if you would like to see this soon.
|
|
||||||
|
|
||||||
These options can be found under the `States` tab inside `Project>Settings` ⬇️
|
|
||||||
|
|
||||||
`<project-name>/projects/<id>/settings/states`.
|
|
||||||
|
|
||||||
<Heading level={2} id="project-labels">
|
|
||||||
Project labels
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
Similar to states, labels on projects help you filter or differentiate issues
|
|
||||||
within your project. You can create and control as many issue labels as
|
|
||||||
needed.
|
|
||||||
|
|
||||||
<Note>Labels can also be created from the issue detail page.</Note>
|
|
||||||
|
|
||||||
These options can be found under the `Labels` tab inside `Project>Settings` ⬇️
|
|
||||||
|
|
||||||
`<project-name>/projects/<id>/settings/labels`.
|
|
||||||
|
|
||||||
<Heading level={2} id="delete-a-project">
|
|
||||||
Delete a project
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
We restrict the delete access to project admin, _we plan to extend this feature to workspace owners as well_.
|
|
||||||
|
|
||||||
These options can be found under the `General` tab inside `Project>Settings` ⬇️
|
|
||||||
|
|
||||||
`<project-name>/projects/<id>/settings/general`.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
If a project is deleted, all of its content, issues, cycles, and modules will
|
|
||||||
also be deleted. Currently, Plane does not offer a automatic backup service on
|
|
||||||
cloud or self-hosted versions, but it is planned on our roadmap.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
<Heading level={2} id="project-shortcuts">
|
|
||||||
Project shortcuts
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
1. Create a new project: `CTRL/CMD + P`
|
|
||||||
|
|
||||||
<Heading level={2} id="delete-a-workspace">
|
|
||||||
Project plans
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
- **Cloud**: All-plans, no suggestion, no limits.
|
|
||||||
- **Self-hosted**: No plans, no suggestion, no limits.
|
|
@ -1,93 +0,0 @@
|
|||||||
import { Note } from '@/components/mdx'
|
|
||||||
|
|
||||||
# Get Started
|
|
||||||
|
|
||||||
This section of the Plane docs helps you get comfortable with the product and find your way around more effectively.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
If you are using the self-hosted version of Plane and want to install it on a
|
|
||||||
local or cloud machine, follow the instructions [here](/self-hosting).
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Onboarding and Invitation
|
|
||||||
|
|
||||||
If you have signed up for the first time, the first step in Plane is to create a new workspace. If you are a freelancer or a solo-player, you can skip the invitation part and be directly redirected to the workspace to start working on your issues. If you are part of a team, you will need to invite your team members to the workspace before you can begin.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
During the onboarding process, the invited users will be assigned a **Viewer**
|
|
||||||
role by default. You can adjust the permissions of invited users for your
|
|
||||||
project in the workspace or project settings page based on your specific
|
|
||||||
requirements.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## The Dashboard (AKA, Home)
|
|
||||||
|
|
||||||
After signing up or signing in to a workspace, you will be redirected to the active project home page. This is where you will see all of the issues that are assigned and pending for you.
|
|
||||||
|
|
||||||
## Creating Workspaces
|
|
||||||
|
|
||||||
Think of each workspace in Plane as a home for your content. Inside, workspace, you can creare projects as an individual, or invite collaborators to share a workspace as a team - it's up to you!
|
|
||||||
|
|
||||||
> When you sign up for the first time, we'll prompt you to create a new workspace. If you're invited to join, you can directly join the workspace without creating a new one.
|
|
||||||
|
|
||||||
## Creating Projects
|
|
||||||
|
|
||||||
Projects let you manage teams and tasks within your Workspace. After creating your Workspace, you will need to create a new project.
|
|
||||||
|
|
||||||
> You can share the workspace with other members by granting them full access to the whole workspace, or by granting them limited access to specific projects.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
You can manage members either by navigating to **Project > Member Settings**
|
|
||||||
page or by **Workspace > Member Settings** page.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Creating Issues
|
|
||||||
|
|
||||||
In Plane, an issue is a task or piece of work. It could be small, like _Update the primary color to blue_, or large, like _Building GitHub integration on plane_. It all depends on how you and your team decide to break down your work into issues.
|
|
||||||
|
|
||||||
Issues are identified by a project-specific and unique number (Example, `VIH-19`), and they must be provided with a title and a state. All the other properties and relations are optional, know more about there [here]().
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
You can create issues by clicking on the `New Issue` button in the right-hand
|
|
||||||
corner of your project, or by using `CTRL/CMD + I` shortcut.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
## Issue Details
|
|
||||||
|
|
||||||
Inside an issue, you can add as many details as you like to get your work done. Here are five critical things to kick-start:
|
|
||||||
|
|
||||||
1. **Issue description**: An enhanced mardown editor inside issues to write as much detail you want. Attachments can be added and dragged around with a simple click, no need to save anything. Everything is auto-saved for you.
|
|
||||||
|
|
||||||
2. **Issue sub-properties**: We support basic issue properties such as `priority`, `label`, `due date`, and `assignee`. You can create relations between issues: mark them as `blocked` or `blocking`.
|
|
||||||
|
|
||||||
3. **Sub-issues**: You can create a `sub-issue` for an issue to either split the issue into smaller chunks, or assign parts of the issue to different people. If you find that a `sub-issue` is holding up the resolution of an issue, you can convert it to an issue so that it can be worked on separately.
|
|
||||||
|
|
||||||
4. **Issue comments**: Think, comment or join the conversation by sharing your thoughts under Issue comments.
|
|
||||||
|
|
||||||
5. **Issue activity**: Track what's happening to your issue with activities. Everything is saved, so you can look back and see what happened. Real-time coming soon!
|
|
||||||
|
|
||||||
## Issue views
|
|
||||||
|
|
||||||
You can organize project issues into views, which let you see your work through different filters or parameters. You can choose from default views that come with your project and workspace or create your own.
|
|
||||||
|
|
||||||
- Every project has two primary views for issues - Board and List View.
|
|
||||||
- You can quickly and easily organize the issues on your board by grouping them or ordering them within views.
|
|
||||||
- You can toggle the visibility of issue sub-properties within views, at any time, based on your preferences.
|
|
||||||
|
|
||||||
## Creating Cycles
|
|
||||||
|
|
||||||
Cycles, (AKA Sprints) is a custom time period in which a team works to complete items on their backlog. At the end of the sprint, the team will usually have finished building and implementing a new version of their product.
|
|
||||||
|
|
||||||
- You can move existing issues to a new or existing cycle, or create a new issue directly.
|
|
||||||
- Bulk operations are supported--you can add or update multiple issues to cycles at once.
|
|
||||||
|
|
||||||
## Creating Modules (Early Preview)
|
|
||||||
|
|
||||||
Modules are smaller, focused projects that help you group and organize issues within a specific time frame. They allow you to break down your work into manageable chunks and track progress towards specific goals or objectives. You can create as many modules as you need within your workspace, and customize each one with its own set of issues, milestones, and team members.
|
|
||||||
|
|
||||||
[Coming soon]
|
|
||||||
|
|
||||||
- A Gantt chart is offered to help you stay on top of timelines and deliver modules more efficiently.
|
|
||||||
- Modules allow you to add documents and links within them, providing a convenient way to share resources with others within the module.
|
|
||||||
|
|
||||||
Have questions? Ask the [Plane](https://discord.com/invite/29tPNhaV) Community.
|
|
@ -1,83 +0,0 @@
|
|||||||
# Self Hosting Plane
|
|
||||||
|
|
||||||
import { Heading } from '@/components/Heading'
|
|
||||||
import { Note } from '@/components/mdx'
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Plane is still in its early days, not everything will be perfect yet, and
|
|
||||||
hiccups may happen. Please let us know of any suggestions, ideas, or bugs that
|
|
||||||
you encounter on our [Discord](https://discord.com/invite/A92xrEGCge).
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
<Heading level={2} id="project-requirements">
|
|
||||||
Requirements
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
1. Git
|
|
||||||
2. Docker
|
|
||||||
3. Docker-compose
|
|
||||||
|
|
||||||
<Heading level={2} id="project-requirements">
|
|
||||||
Project set-up
|
|
||||||
</Heading>
|
|
||||||
1. Clone the code
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone --depth 1 https://github.com/makeplane/plane
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Change Directory
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd plane
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Move `env.example` to `.env` in the `apiserver` directory and `apps/app`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mv.env.example.env
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Add all the variables - (the below are the compulsory variables)
|
|
||||||
|
|
||||||
Frontend - `apps/app/.env`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
NEXT_PUBLIC_API_BASE_URL = '<-- endpoint goes here -->'
|
|
||||||
NEXT_PUBLIC_GOOGLE_CLIENTID = '<-- google client id goes here -->'
|
|
||||||
NEXT_PUBLIC_GITHUB_ID = '<-- github id goes here -->'
|
|
||||||
NEXT_PUBLIC_APP_ENVIRONMENT = '<-- production | development -->'
|
|
||||||
```
|
|
||||||
|
|
||||||
Backend - `apiserver/.env`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backend
|
|
||||||
SECRET_KEY="<-- django secret -->"
|
|
||||||
EMAIL_HOST="<-- email smtp -->"
|
|
||||||
EMAIL_HOST_USER="<-- email host user -->"
|
|
||||||
EMAIL_HOST_PASSWORD="<-- email host password -->"
|
|
||||||
|
|
||||||
AWS_REGION="<-- aws region -->"
|
|
||||||
AWS_ACCESS_KEY_ID="<-- aws access key -->"
|
|
||||||
AWS_SECRET_ACCESS_KEY="<-- aws secret acess key -->"
|
|
||||||
AWS_S3_BUCKET_NAME="<-- aws s3 bucket name -->"
|
|
||||||
|
|
||||||
SENTRY_DSN="<-- sentry dsn -->"
|
|
||||||
WEB_URL="<-- frontend web url -->"
|
|
||||||
|
|
||||||
GITHUB_CLIENT_SECRET="<-- github secret -->"
|
|
||||||
|
|
||||||
DISABLE_COLLECTSTATIC=1
|
|
||||||
DOCKERIZED=1
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Visit
|
|
||||||
1. [http://localhost:3000](http://localhost:3000) - (Frontend)
|
|
||||||
2. [http://localhost:8000](http://localhost:8000) - (Backend)
|
|
@ -1,96 +0,0 @@
|
|||||||
import { Heading } from '@/components/Heading'
|
|
||||||
import { Note } from '@/components/mdx'
|
|
||||||
|
|
||||||
# Workspace setup
|
|
||||||
|
|
||||||
Workspaces in Plane are where you can access all of your content, including issues, cycles, and modules. Everything is organized within a workspace. To help you understand, you can think of a workspace as similar to a server on Discord or an organization on Slack, where you can bring your entire team together to work.
|
|
||||||
|
|
||||||
<Heading level={2} id="creating-a-workspace">
|
|
||||||
Creating workspace
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
If you are a first-time user who has signed up for Plane, you will create a new workspace for your team during the onboarding process.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
On Plane cloud and self-hosted versions, the workspace URLs (also known as
|
|
||||||
slugs) must be unique. We allow users to have the same organization names, but
|
|
||||||
the URLs must still be unique to ensure a better experience.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
If you are a team member or contributor and are invited to a workspace, you do
|
|
||||||
not need to have your own workspace. Instead, after accepting the invitation,
|
|
||||||
you will be redirected directly to the workspace you were invited to.
|
|
||||||
|
|
||||||
<Heading level={2} id="workspace-roles">
|
|
||||||
Workspace roles
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
Every user on a workspace has a specific role associated with them, which
|
|
||||||
determines their permissions and what actions they are allowed to take within
|
|
||||||
the workspace. This helps to ensure that users only have access to the
|
|
||||||
resources and functions that they need in order to complete their work.
|
|
||||||
|
|
||||||
These roles can be configured when inviting a user to a workspace from the
|
|
||||||
workspace invite page, or later from the workspace settings page. _Note that
|
|
||||||
when you invite a user during the onboarding process, they will be assigned
|
|
||||||
the default role of 'Viewer'_.
|
|
||||||
|
|
||||||
**Available roles**
|
|
||||||
|
|
||||||
- **Owner** - The owner of the workspace is the supreme administrator and has all permissions set to `true` within the workspace.
|
|
||||||
- **Administrator** - Workspace administrators have similar privileges to the owner, but they are not able to delete the workspace.
|
|
||||||
- **Member** - Workspace members are essentially team members within the organization, and they have the ability to read, write, edit, and delete entities inside projects, cycles, and modules within the workspace. It is important to note that workspace members are not able to create new projects, cycles, or modules
|
|
||||||
- **Guest** - External members of organizations can be invited as guests, which grants them the ability to view all content to which they have been invited. However, they do not have permissions to write, update, or delete entities inside workspace.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Users should first join the workspace, in ordered to be invited to public and
|
|
||||||
secret projects. View [projects](/projects) docs for more.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
<Heading level={2} id="workspace-inivitation">
|
|
||||||
Inviting user to a workspace
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
You can easily invite members of your organization with differnt roles onto a workspace.
|
|
||||||
|
|
||||||
On cloud, here's how you can invite a user to your workspace,
|
|
||||||
|
|
||||||
1. When you are onboarded, you will see a screen that allows you to invite your team members to the workspace. By default, we set the role to `View` only. This is because we perform a bulk operation to get your team up and running.
|
|
||||||
2. If you've missed step one, you can manage all your members under `/<workspace-name>/settings/members` page.
|
|
||||||
3. To invite team members, use the Add Member option. When an invitation is sent, the invitee will receive an email to accept the invitation.
|
|
||||||
4. To update workspace member permissions, use the collapsable menu, next to the member.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Self-hosted users should configure an email service in order to send
|
|
||||||
invitations. Follow [this](/self-hosting) document to learn how to set up
|
|
||||||
self-hosted instances.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
<Heading level={2} id="switch-workspaces">
|
|
||||||
Switching between workspaces
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
When working with multiple organizations or teams, you can switch between them
|
|
||||||
using the workspace menu on the left sidebar. Here, you can find all the
|
|
||||||
workspaces you've created or joined and switch between them anytime, as long
|
|
||||||
as they are linked to your email. In the future, we plan to allow logging into
|
|
||||||
multiple workspaces with different emails and improve the overall experience.
|
|
||||||
|
|
||||||
<Heading level={2} id="delete-a-workspace">
|
|
||||||
Delete a workspace
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
Only workspace owners have the ability to delete a workspace, these options can be found under the workspace settings.
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
If a workspace is deleted, all of its content, issues, cycles, and modules
|
|
||||||
will also be deleted. Currently, Plane does not offer a automatic backup
|
|
||||||
service on cloud or self-hosted versions, but it is planned on our roadmap.
|
|
||||||
</Note>
|
|
||||||
|
|
||||||
<Heading level={2} id="delete-a-workspace">
|
|
||||||
Workspace plan
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
- **Cloud**: All-plans, no suggestion, no limits.
|
|
||||||
- **Self-hosted**: No plans, no suggestion, no limits.
|
|
@ -1,15 +0,0 @@
|
|||||||
:root {
|
|
||||||
--shiki-color-text: theme('colors.white');
|
|
||||||
--shiki-token-constant: theme('colors.blue.300');
|
|
||||||
--shiki-token-string: theme('colors.blue.300');
|
|
||||||
--shiki-token-comment: theme('colors.zinc.500');
|
|
||||||
--shiki-token-keyword: theme('colors.sky.300');
|
|
||||||
--shiki-token-parameter: theme('colors.pink.300');
|
|
||||||
--shiki-token-function: theme('colors.violet.300');
|
|
||||||
--shiki-token-string-expression: theme('colors.blue.300');
|
|
||||||
--shiki-token-punctuation: theme('colors.zinc.200');
|
|
||||||
}
|
|
||||||
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
@ -1,42 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: ['./{src,mdx}/**/*.{js,mjs,jsx,mdx}'],
|
|
||||||
darkMode: 'class',
|
|
||||||
theme: {
|
|
||||||
fontSize: {
|
|
||||||
'2xs': ['0.75rem', { lineHeight: '1.25rem' }],
|
|
||||||
xs: ['0.8125rem', { lineHeight: '1.5rem' }],
|
|
||||||
sm: ['0.875rem', { lineHeight: '1.5rem' }],
|
|
||||||
base: ['1rem', { lineHeight: '1.75rem' }],
|
|
||||||
lg: ['1.125rem', { lineHeight: '1.75rem' }],
|
|
||||||
xl: ['1.25rem', { lineHeight: '1.75rem' }],
|
|
||||||
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
|
||||||
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
|
|
||||||
'4xl': ['2.25rem', { lineHeight: '2.5rem' }],
|
|
||||||
'5xl': ['3rem', { lineHeight: '1' }],
|
|
||||||
'6xl': ['3.75rem', { lineHeight: '1' }],
|
|
||||||
'7xl': ['4.5rem', { lineHeight: '1' }],
|
|
||||||
'8xl': ['6rem', { lineHeight: '1' }],
|
|
||||||
'9xl': ['8rem', { lineHeight: '1' }],
|
|
||||||
},
|
|
||||||
typography: require('./typography'),
|
|
||||||
extend: {
|
|
||||||
boxShadow: {
|
|
||||||
glow: '0 0 4px rgb(0 0 0 / 0.1)',
|
|
||||||
},
|
|
||||||
maxWidth: {
|
|
||||||
lg: '33rem',
|
|
||||||
'2xl': '40rem',
|
|
||||||
'3xl': '50rem',
|
|
||||||
'5xl': '66rem',
|
|
||||||
},
|
|
||||||
opacity: {
|
|
||||||
1: '0.01',
|
|
||||||
2.5: '0.025',
|
|
||||||
7.5: '0.075',
|
|
||||||
15: '0.15',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [require('@tailwindcss/typography')],
|
|
||||||
}
|
|
@ -1,357 +0,0 @@
|
|||||||
module.exports = ({ 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'),
|
|
||||||
|
|
||||||
// Layout
|
|
||||||
'> *': {
|
|
||||||
maxWidth: theme('maxWidth.2xl'),
|
|
||||||
marginLeft: 'auto',
|
|
||||||
marginRight: 'auto',
|
|
||||||
'@screen lg': {
|
|
||||||
maxWidth: theme('maxWidth.3xl'),
|
|
||||||
marginLeft: `calc(50% - min(50%, ${theme('maxWidth.lg')}))`,
|
|
||||||
marginRight: `calc(50% - min(50%, ${theme('maxWidth.lg')}))`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// 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'),
|
|
||||||
},
|
|
||||||
':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)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
@ -4,9 +4,6 @@
|
|||||||
"NEXT_PUBLIC_GITHUB_ID",
|
"NEXT_PUBLIC_GITHUB_ID",
|
||||||
"NEXT_PUBLIC_GOOGLE_CLIENTID",
|
"NEXT_PUBLIC_GOOGLE_CLIENTID",
|
||||||
"NEXT_PUBLIC_API_BASE_URL",
|
"NEXT_PUBLIC_API_BASE_URL",
|
||||||
"NEXT_PUBLIC_DOCSEARCH_API_KEY",
|
|
||||||
"NEXT_PUBLIC_DOCSEARCH_APP_ID",
|
|
||||||
"NEXT_PUBLIC_DOCSEARCH_INDEX_NAME",
|
|
||||||
"NEXT_PUBLIC_SENTRY_DSN",
|
"NEXT_PUBLIC_SENTRY_DSN",
|
||||||
"SENTRY_AUTH_TOKEN",
|
"SENTRY_AUTH_TOKEN",
|
||||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||||
|
Loading…
Reference in New Issue
Block a user