chore: remove docs (#553)

* feat: block sync

* chore: remove docs from repo
This commit is contained in:
Aaryan Khandelwal 2023-03-28 03:54:48 +05:30 committed by GitHub
parent d9f31a1eb7
commit fbbf97f3a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
82 changed files with 0 additions and 4219 deletions

View File

@ -1,4 +0,0 @@
module.exports = {
root: true,
extends: ['custom'],
}

View File

@ -1,3 +0,0 @@
# Plane Docs
Source code that powers plane.so/docs

View File

@ -1,8 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

View File

@ -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,
]

View File

@ -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()}]`,
}),
],
]

View File

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

View File

@ -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)

View File

@ -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"
}
}

View File

@ -1,9 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
'postcss-focus-visible': {
replaceWith: '[data-focus-visible-added]',
},
autoprefixer: {},
},
}

View File

@ -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

View File

@ -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>
)
}

View File

@ -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>
}

View File

@ -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">
&copy; 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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
})

View File

@ -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>
</>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -1,10 +0,0 @@
export function Logo() {
return (
<img
src="https://camo.githubusercontent.com/ef32512ae486a8cef8e000e74b2ff11c92c89c2cadb2d79674c6bd1599b99a56/68747470733a2f2f696b2e696d6167656b69742e696f2f77326f6b77627475322f706c616e652d6c6f676f5f306d383378756537522e706e673f696b2d73646b2d76657273696f6e3d6a6176617363726970742d312e342e33267570646174656441743d31363638383632373137303834"
height={100}
width={100}
alt="Plane"
/>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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}
/>
)
}

View File

@ -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>
)
}

View File

@ -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">
&lsquo;{query}&rsquo;
</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>
)
}

View File

@ -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)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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"
/>
</>
)
}

View File

@ -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>
)
}

View File

@ -1,5 +0,0 @@
export const description = 'Plane Architecture'
# Plane Architecture
Coming very soon.

View File

@ -1,5 +0,0 @@
export const description = 'Cycles'
# Cycles
Coming very soon.

View File

@ -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!

View File

@ -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)

View File

@ -1,5 +0,0 @@
export const description = 'Issues'
# Issues
Coming very soon.

View File

@ -1,5 +0,0 @@
export const description = 'Modules'
# Modules
Coming very soon.

View File

@ -1,3 +0,0 @@
# Plane Basics
Coming Soon.

View File

@ -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.

View File

@ -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.

View File

@ -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)

View File

@ -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.

View File

@ -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;

View File

@ -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')],
}

View File

@ -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)',
},
},
})

View File

@ -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",