feat: add plane docs to turbo

This commit is contained in:
pablohashescobar 2022-12-22 00:33:32 +05:30
parent 9539fca585
commit 18ff810a14
75 changed files with 8856 additions and 17 deletions

3
apps/docs/README.md Normal file
View File

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

8
apps/docs/jsconfig.json Normal file
View File

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

19
apps/docs/mdx/recma.mjs Normal file
View File

@ -0,0 +1,19 @@
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,
]

126
apps/docs/mdx/rehype.mjs Normal file
View File

@ -0,0 +1,126 @@
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()}]`,
}),
],
]

3
apps/docs/mdx/remark.mjs Normal file
View File

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

23
apps/docs/next.config.mjs Normal file
View File

@ -0,0 +1,23 @@
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)

46
apps/docs/package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "plane-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": {
"eslint": "8.26.0",
"eslint-config-next": "13.0.2",
"prettier": "^2.7.1",
"prettier-plugin-tailwindcss": "^0.1.13"
}
}

View File

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

View File

@ -0,0 +1,5 @@
module.exports = {
singleQuote: true,
semi: false,
plugins: [require('prettier-plugin-tailwindcss')],
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,62 @@
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
}) {
let Component = props.href ? Link : 'button'
className = clsx(
'inline-flex gap-0.5 justify-center overflow-hidden text-sm font-medium transition',
variantStyles[variant],
className
)
let arrowIcon = (
<ArrowIcon
className={clsx(
'mt-0.5 h-5 w-5',
variant === 'text' && 'relative top-px',
arrow === 'left' && '-ml-1 rotate-180',
arrow === 'right' && '-mr-1'
)}
/>
)
return (
<Component className={className} {...props}>
{arrow === 'left' && arrowIcon}
{children}
{arrow === 'right' && arrowIcon}
</Component>
)
}

View File

@ -0,0 +1,297 @@
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 }) {
let [copyCount, setCopyCount] = useState(0)
let copied = copyCount > 0
useEffect(() => {
if (copyCount > 0) {
let timeout = setTimeout(() => setCopyCount(0), 1000)
return () => {
clearTimeout(timeout)
}
}
}, [copyCount])
return (
<button
type="button"
className={clsx(
'group/button absolute top-3.5 right-4 overflow-hidden rounded-full py-1 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 }) {
let 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 }) {
let 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 }) {
let 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() {
let positionRef = useRef()
let rafRef = useRef()
useEffect(() => {
return () => {
window.cancelAnimationFrame(rafRef.current)
}
}, [])
return {
positionRef,
preventLayoutShift(callback) {
let initialTop = positionRef.current.getBoundingClientRect().top
callback()
rafRef.current = window.requestAnimationFrame(() => {
let 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) {
let { preferredLanguages, addPreferredLanguage } = usePreferredLanguageStore()
let [selectedIndex, setSelectedIndex] = useState(0)
let activeLanguage = [...availableLanguages].sort(
(a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a)
)[0]
let languageIndex = availableLanguages.indexOf(activeLanguage)
let newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex
if (newSelectedIndex !== selectedIndex) {
setSelectedIndex(newSelectedIndex)
}
let { positionRef, preventLayoutShift } = usePreventLayoutShift()
return {
as: 'div',
ref: positionRef,
selectedIndex,
onChange: (newSelectedIndex) => {
preventLayoutShift(() =>
addPreferredLanguage(availableLanguages[newSelectedIndex])
)
},
}
}
const CodeGroupContext = createContext(false)
export function CodeGroup({ children, title, ...props }) {
let languages = Children.map(children, (child) => getPanelTitle(child.props))
let tabGroupProps = useTabGroupProps(languages)
let hasTabs = Children.count(children) > 1
let Container = hasTabs ? Tab.Group : 'div'
let containerProps = hasTabs ? tabGroupProps : {}
let 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 }) {
let isGrouped = useContext(CodeGroupContext)
if (isGrouped) {
return <code {...props} dangerouslySetInnerHTML={{ __html: children }} />
}
return <code {...props}>{children}</code>
}
export function Pre({ children, ...props }) {
let isGrouped = useContext(CodeGroupContext)
if (isGrouped) {
return children
}
return <CodeGroup {...props}>{children}</CodeGroup>
}

View File

@ -0,0 +1,228 @@
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() {
let [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() {
let router = useRouter()
let allPages = navigation.flatMap((group) => group.links)
let currentPageIndex = allPages.findIndex(
(page) => page.href === router.pathname
)
if (currentPageIndex === -1) {
return null
}
let previousPage = allPages[currentPageIndex - 1]
let nextPage = allPages[currentPageIndex + 1]
if (!previousPage && !nextPage) {
return null
}
return (
<div className="flex">
{previousPage && (
<div className="flex flex-col items-start gap-3">
<PageLink label="Previous" page={previousPage} previous />
</div>
)}
{nextPage && (
<div className="ml-auto flex flex-col items-end gap-3">
<PageLink label="Next" page={nextPage} />
</div>
)}
</div>
)
}
function 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() {
let 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

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

View File

@ -0,0 +1,54 @@
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

@ -0,0 +1,87 @@
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,
} from '@/components/MobileNavigation'
import { 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) {
let { isOpen: mobileNavIsOpen } = useMobileNavigationStore()
let isInsideMobileNavigation = useIsInsideMobileNavigation()
let { scrollY } = useScroll()
let bgOpacityLight = useTransform(scrollY, [0, 72], [0.5, 0.9])
let 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">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

@ -0,0 +1,102 @@
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
}) {
let Component = `h${level}`
let ref = useRef()
let registerHeading = useSectionStore((s) => s.registerHeading)
let inView = useInView(ref, {
margin: `${remToPx(-3.5)}px 0px 0px 0px`,
amount: 'all',
})
useEffect(() => {
if (level === 2) {
registerHeading({ id, 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

@ -0,0 +1,32 @@
import { GridPattern } from '@/components/GridPattern'
export function HeroPattern() {
return (
<div className="absolute inset-0 -z-10 mx-0 max-w-none overflow-hidden">
<div className="absolute 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

@ -0,0 +1,41 @@
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

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

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

View File

@ -0,0 +1,115 @@
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() {
let isInsideMobileNavigation = useIsInsideMobileNavigation()
let { isOpen, toggle, close } = useMobileNavigationStore()
let 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

@ -0,0 +1,54 @@
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()
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
let isSystemDarkMode = darkModeMediaQuery.matches
let 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

@ -0,0 +1,236 @@
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) {
let 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 }) {
let [sections, visibleSections] = useInitialValue(
[
useSectionStore((s) => s.sections),
useSectionStore((s) => s.visibleSections),
],
useIsInsideMobileNavigation()
)
let isPresent = useIsPresent()
let firstVisibleSectionIndex = Math.max(
0,
[{ id: '_top' }, ...sections].findIndex(
(section) => section.id === visibleSections[0]
)
)
let itemHeight = remToPx(2)
let height = isPresent
? Math.max(1, visibleSections.length) * itemHeight
: itemHeight
let top =
group.links.findIndex((link) => link.href === pathname) * itemHeight +
firstVisibleSectionIndex * itemHeight
return (
<motion.div
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.2 } }}
exit={{ opacity: 0 }}
className="absolute inset-x-0 top-0 bg-zinc-800/2.5 will-change-transform dark:bg-white/2.5"
style={{ borderRadius: 8, height, top }}
/>
)
}
function ActivePageMarker({ group, pathname }) {
let itemHeight = remToPx(2)
let offset = remToPx(0.25)
let activePageIndex = group.links.findIndex((link) => link.href === pathname)
let top = offset + activePageIndex * itemHeight
return (
<motion.div
layout
className="absolute left-2 h-6 w-px bg-blue-500"
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.2 } }}
exit={{ opacity: 0 }}
style={{ top }}
/>
)
}
function NavigationGroup({ group, className }) {
// If this is the mobile navigation then we always render the initial
// state, so that the state does not change during the close animation.
// The state will still update when we re-open (re-render) the navigation.
let isInsideMobileNavigation = useIsInsideMobileNavigation()
let [router, sections] = useInitialValue(
[useRouter(), useSectionStore((s) => s.sections)],
isInsideMobileNavigation
)
let 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: 'Get Started', href: '/get-started' },
{ title: 'Self Hosting', href: '/self-hosting' },
{ title: 'Plane Basics', href: '/plane-basics' },
// { title: 'Quickstart', href: '/quickstart' },
// { title: 'SDKs', href: '/sdks' },
// { title: 'Authentication', href: '/authentication' },
// { title: 'Pagination', href: '/pagination' },
// { title: 'Errors', href: '/errors' },
// { title: 'Webhooks', href: '/webhooks' },
],
},
// {
// title: 'Resources',
// links: [
// { title: 'Contacts', href: '/contacts' },
// { title: 'Conversations', href: '/conversations' },
// { title: 'Messages', href: '/messages' },
// { title: 'Groups', href: '/groups' },
// { title: 'Attachments', href: '/attachments' },
// ],
// },
]
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

@ -0,0 +1,10 @@
import clsx from 'clsx'
export function Prose({ as: Component = 'div', className, ...props }) {
return (
<Component
className={clsx(className, 'prose dark:prose-invert')}
{...props}
/>
)
}

View File

@ -0,0 +1,157 @@
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: '/get-started',
name: 'Get Started',
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-host',
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: '/',
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 }) {
let maskImage = useMotionTemplate`radial-gradient(180px at ${mouseX}px ${mouseY}px, white, transparent)`
let style = { maskImage, WebkitMaskImage: maskImage }
return (
<div className="pointer-events-none">
<div className="absolute inset-0 rounded-2xl 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 }) {
let mouseX = useMotionValue(0)
let mouseY = useMotionValue(0)
function onMouseMove({ currentTarget, clientX, clientY }) {
let { left, top } = currentTarget.getBoundingClientRect()
mouseX.set(clientX - left)
mouseY.set(clientY - top)
}
return (
<div
key={resource.href}
onMouseMove={onMouseMove}
className="group relative flex rounded-2xl bg-zinc-50 transition-shadow hover:shadow-md hover:shadow-zinc-900/5 dark:bg-white/2.5 dark:hover:shadow-black/5"
>
<ResourcePattern {...resource.pattern} mouseX={mouseX} mouseY={mouseY} />
<div className="absolute inset-0 rounded-2xl ring-1 ring-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

@ -0,0 +1,515 @@
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() {
let id = useId()
let router = useRouter()
let [autocompleteState, setAutocompleteState] = useState({})
let [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 }) {
let 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) {
let allLevels = Object.keys(result.hierarchy)
let hierarchy = Object.entries(result._highlightResult.hierarchy).filter(
([, { value }]) => Boolean(value)
)
let levels = hierarchy.map(([level]) => level)
let 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) {
let id = useId()
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
<circle cx="10" cy="10" r="5.5" strokeLinejoin="round" />
<path
stroke={`url(#${id})`}
strokeLinecap="round"
strokeLinejoin="round"
d="M15.5 10a5.5 5.5 0 1 0-5.5 5.5"
/>
<defs>
<linearGradient
id={id}
x1="13"
x2="9.5"
y1="9"
y2="15"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="currentColor" />
<stop offset="1" stopColor="currentColor" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
)
}
function SearchResult({ result, resultIndex, autocomplete, collection }) {
let id = useId()
let { 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
) {
let 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) {
let [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 }) {
let router = useRouter()
let formRef = useRef()
let panelRef = useRef()
let inputRef = useRef()
let { 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() {
let buttonRef = useRef()
let [open, setOpen] = useState(false)
return {
buttonProps: {
ref: buttonRef,
onClick() {
setOpen(true)
},
},
dialogProps: {
open,
setOpen(open) {
let { width, height } = buttonRef.current.getBoundingClientRect()
if (!open || (width !== 0 && height !== 0)) {
setOpen(open)
}
},
},
}
}
export function Search() {
let [modifierKey, setModifierKey] = useState()
let { buttonProps, dialogProps } = useSearchProps()
useEffect(() => {
setModifierKey(
/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl '
)
}, [])
return (
<div className="hidden lg:block lg:max-w-md lg:flex-auto">
<button
type="button"
className="hidden h-8 w-full items-center gap-2 rounded-full bg-white 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() {
let { 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

@ -0,0 +1,117 @@
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) => {
return {
sections: state.sections.map((section) => {
if (section.id === id) {
return {
...section,
headingRef: ref,
offsetRem,
}
}
return section
}),
}
}),
}))
}
function useVisibleSections(sectionStore) {
let setVisibleSections = useStore(sectionStore, (s) => s.setVisibleSections)
let sections = useStore(sectionStore, (s) => s.sections)
useEffect(() => {
function checkVisibleSections() {
let { innerHeight, scrollY } = window
let newVisibleSections = []
for (
let sectionIndex = 0;
sectionIndex < sections.length;
sectionIndex++
) {
let { id, headingRef, offsetRem } = sections[sectionIndex]
let offset = remToPx(offsetRem)
let top = headingRef.current.getBoundingClientRect().top + scrollY
if (sectionIndex === 0 && top - offset > scrollY) {
newVisibleSections.push('_top')
}
let nextSection = sections[sectionIndex + 1]
let bottom =
(nextSection?.headingRef.current.getBoundingClientRect().top ??
Infinity) +
scrollY -
remToPx(nextSection?.offsetRem ?? 0)
if (
(top > scrollY && top < scrollY + innerHeight) ||
(bottom > scrollY && bottom < scrollY + innerHeight) ||
(top <= scrollY && bottom >= scrollY + innerHeight)
) {
newVisibleSections.push(id)
}
}
setVisibleSections(newVisibleSections)
}
let raf = window.requestAnimationFrame(() => checkVisibleSections())
window.addEventListener('scroll', checkVisibleSections, { passive: true })
window.addEventListener('resize', checkVisibleSections)
return () => {
window.cancelAnimationFrame(raf)
window.removeEventListener('scroll', checkVisibleSections)
window.removeEventListener('resize', checkVisibleSections)
}
}, [setVisibleSections, sections])
}
const SectionStoreContext = createContext()
const useIsomorphicLayoutEffect =
typeof window === 'undefined' ? useEffect : useLayoutEffect
export function SectionProvider({ sections, children }) {
let [sectionStore] = useState(() => createSectionStore(sections))
useVisibleSections(sectionStore)
useIsomorphicLayoutEffect(() => {
sectionStore.setState({ sections })
}, [sectionStore, sections])
return (
<SectionStoreContext.Provider value={sectionStore}>
{children}
</SectionStoreContext.Provider>
)
}
export function useSectionStore(selector) {
let store = useContext(SectionStoreContext)
return useStore(store, selector)
}

View File

@ -0,0 +1,58 @@
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

@ -0,0 +1,17 @@
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

@ -0,0 +1,11 @@
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

@ -0,0 +1,17 @@
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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,17 @@
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

@ -0,0 +1,17 @@
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

@ -0,0 +1,19 @@
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

@ -0,0 +1,17 @@
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

@ -0,0 +1,17 @@
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

@ -0,0 +1,17 @@
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

@ -0,0 +1,17 @@
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

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
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"
stroke-linecap="round"
stroke-linejoin="round"
d="m13 13 4.5 4.5m-9-3a6 6 0 1 1 0-12 6 6 0 0 1 0 12Z"
/>
</svg>
)
}

View File

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

View File

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

View File

@ -0,0 +1,17 @@
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

@ -0,0 +1,12 @@
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

@ -0,0 +1,17 @@
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

@ -0,0 +1,11 @@
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

@ -0,0 +1,17 @@
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

@ -0,0 +1,19 @@
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

@ -0,0 +1,24 @@
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

@ -0,0 +1,28 @@
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

@ -0,0 +1,94 @@
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

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

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

After

Width:  |  Height:  |  Size: 864 B

View File

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

After

Width:  |  Height:  |  Size: 561 B

View File

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

View File

@ -0,0 +1,40 @@
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 }) {
let 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>
</>
)
}

View File

@ -0,0 +1,50 @@
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

@ -0,0 +1,64 @@
# Get Started
This section of the Plane docs helps you get comfortable with the product and find your way around more effectively.
## 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.
## 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>
## 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 Discussion**: Think, comment or join the conversation by sharing your thoughts under Issue discussion.
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.
## Plan better with 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.
---
Have questions? Ask the [Plane](https://discord.com/invite/29tPNhaV) Community.

View File

@ -0,0 +1,32 @@
import { Guides } from '@/components/Guides'
import { Resources } from '@/components/Resources'
import { HeroPattern } from '@/components/HeroPattern'
export const description =
'Learn everything there is to know about the Protocol API and integrate Protocol into your product.'
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>
<Resources />
## Contributing
Each page footer contains an "Edit on GitHub" Link. Make a pull request, and we'll merge it!

View File

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

View File

@ -0,0 +1,3 @@
# Self Hosting Plane
Coming soon.

View File

@ -0,0 +1,15 @@
: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

@ -0,0 +1,42 @@
/** @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')],
}

357
apps/docs/typography.js Normal file
View File

@ -0,0 +1,357 @@
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)',
},
},
})

3333
apps/docs/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff