From 18ff810a145ef599377714372009824943181142 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Thu, 22 Dec 2022 00:33:32 +0530 Subject: [PATCH 1/9] feat: add plane docs to turbo --- apps/docs/README.md | 3 + apps/docs/jsconfig.json | 8 + apps/docs/mdx/recma.mjs | 19 + apps/docs/mdx/rehype.mjs | 126 + apps/docs/mdx/remark.mjs | 3 + apps/docs/next.config.mjs | 23 + apps/docs/package.json | 46 + apps/docs/postcss.config.js | 9 + apps/docs/prettier.config.js | 5 + apps/docs/public/favicon.ico | Bin 0 -> 15406 bytes apps/docs/src/components/Button.jsx | 62 + apps/docs/src/components/Code.jsx | 297 ++ apps/docs/src/components/Footer.jsx | 228 ++ apps/docs/src/components/GridPattern.jsx | 42 + apps/docs/src/components/Guides.jsx | 54 + apps/docs/src/components/Header.jsx | 87 + apps/docs/src/components/Heading.jsx | 102 + apps/docs/src/components/HeroPattern.jsx | 32 + apps/docs/src/components/Layout.jsx | 41 + apps/docs/src/components/Libraries.jsx | 82 + apps/docs/src/components/Logo.jsx | 5 + apps/docs/src/components/MobileNavigation.jsx | 115 + apps/docs/src/components/ModeToggle.jsx | 54 + apps/docs/src/components/Navigation.jsx | 236 ++ apps/docs/src/components/Prose.jsx | 10 + apps/docs/src/components/Resources.jsx | 157 + apps/docs/src/components/Search.jsx | 515 +++ apps/docs/src/components/SectionProvider.jsx | 117 + apps/docs/src/components/Tag.jsx | 58 + apps/docs/src/components/icons/BellIcon.jsx | 17 + apps/docs/src/components/icons/BoltIcon.jsx | 11 + apps/docs/src/components/icons/BookIcon.jsx | 17 + .../src/components/icons/CalendarIcon.jsx | 23 + apps/docs/src/components/icons/CartIcon.jsx | 15 + .../src/components/icons/ChatBubbleIcon.jsx | 17 + apps/docs/src/components/icons/CheckIcon.jsx | 17 + .../components/icons/ChevronRightLeftIcon.jsx | 17 + .../src/components/icons/ClipboardIcon.jsx | 17 + apps/docs/src/components/icons/CogIcon.jsx | 19 + apps/docs/src/components/icons/CopyIcon.jsx | 17 + .../src/components/icons/DocumentIcon.jsx | 17 + .../src/components/icons/EnvelopeIcon.jsx | 17 + .../src/components/icons/FaceSmileIcon.jsx | 17 + apps/docs/src/components/icons/FolderIcon.jsx | 22 + apps/docs/src/components/icons/LinkIcon.jsx | 12 + apps/docs/src/components/icons/ListIcon.jsx | 17 + .../components/icons/MagnifyingGlassIcon.jsx | 13 + apps/docs/src/components/icons/MapPinIcon.jsx | 19 + .../docs/src/components/icons/PackageIcon.jsx | 16 + .../components/icons/PaperAirplaneIcon.jsx | 17 + .../src/components/icons/PaperClipIcon.jsx | 12 + apps/docs/src/components/icons/ShapesIcon.jsx | 17 + apps/docs/src/components/icons/ShirtIcon.jsx | 11 + .../src/components/icons/SquaresPlusIcon.jsx | 17 + apps/docs/src/components/icons/TagIcon.jsx | 19 + apps/docs/src/components/icons/UserIcon.jsx | 24 + apps/docs/src/components/icons/UsersIcon.jsx | 28 + apps/docs/src/components/mdx.jsx | 94 + apps/docs/src/images/logos/go.svg | 14 + apps/docs/src/images/logos/node.svg | 4 + apps/docs/src/images/logos/php.svg | 10 + apps/docs/src/images/logos/python.svg | 13 + apps/docs/src/images/logos/ruby.svg | 4 + apps/docs/src/lib/remToPx.js | 8 + apps/docs/src/pages/_app.jsx | 40 + apps/docs/src/pages/_document.jsx | 50 + apps/docs/src/pages/get-started.mdx | 64 + apps/docs/src/pages/index.mdx | 32 + apps/docs/src/pages/plane-basics.mdx | 3 + apps/docs/src/pages/self-hosting.mdx | 3 + apps/docs/src/styles/tailwind.css | 15 + apps/docs/tailwind.config.js | 42 + apps/docs/typography.js | 357 ++ apps/docs/yarn.lock | 3333 +++++++++++++++++ pnpm-lock.yaml | 1769 ++++++++- 75 files changed, 8856 insertions(+), 17 deletions(-) create mode 100644 apps/docs/README.md create mode 100644 apps/docs/jsconfig.json create mode 100644 apps/docs/mdx/recma.mjs create mode 100644 apps/docs/mdx/rehype.mjs create mode 100644 apps/docs/mdx/remark.mjs create mode 100644 apps/docs/next.config.mjs create mode 100644 apps/docs/package.json create mode 100644 apps/docs/postcss.config.js create mode 100644 apps/docs/prettier.config.js create mode 100644 apps/docs/public/favicon.ico create mode 100644 apps/docs/src/components/Button.jsx create mode 100644 apps/docs/src/components/Code.jsx create mode 100644 apps/docs/src/components/Footer.jsx create mode 100644 apps/docs/src/components/GridPattern.jsx create mode 100644 apps/docs/src/components/Guides.jsx create mode 100644 apps/docs/src/components/Header.jsx create mode 100644 apps/docs/src/components/Heading.jsx create mode 100644 apps/docs/src/components/HeroPattern.jsx create mode 100644 apps/docs/src/components/Layout.jsx create mode 100644 apps/docs/src/components/Libraries.jsx create mode 100644 apps/docs/src/components/Logo.jsx create mode 100644 apps/docs/src/components/MobileNavigation.jsx create mode 100644 apps/docs/src/components/ModeToggle.jsx create mode 100644 apps/docs/src/components/Navigation.jsx create mode 100644 apps/docs/src/components/Prose.jsx create mode 100644 apps/docs/src/components/Resources.jsx create mode 100644 apps/docs/src/components/Search.jsx create mode 100644 apps/docs/src/components/SectionProvider.jsx create mode 100644 apps/docs/src/components/Tag.jsx create mode 100644 apps/docs/src/components/icons/BellIcon.jsx create mode 100644 apps/docs/src/components/icons/BoltIcon.jsx create mode 100644 apps/docs/src/components/icons/BookIcon.jsx create mode 100644 apps/docs/src/components/icons/CalendarIcon.jsx create mode 100644 apps/docs/src/components/icons/CartIcon.jsx create mode 100644 apps/docs/src/components/icons/ChatBubbleIcon.jsx create mode 100644 apps/docs/src/components/icons/CheckIcon.jsx create mode 100644 apps/docs/src/components/icons/ChevronRightLeftIcon.jsx create mode 100644 apps/docs/src/components/icons/ClipboardIcon.jsx create mode 100644 apps/docs/src/components/icons/CogIcon.jsx create mode 100644 apps/docs/src/components/icons/CopyIcon.jsx create mode 100644 apps/docs/src/components/icons/DocumentIcon.jsx create mode 100644 apps/docs/src/components/icons/EnvelopeIcon.jsx create mode 100644 apps/docs/src/components/icons/FaceSmileIcon.jsx create mode 100644 apps/docs/src/components/icons/FolderIcon.jsx create mode 100644 apps/docs/src/components/icons/LinkIcon.jsx create mode 100644 apps/docs/src/components/icons/ListIcon.jsx create mode 100644 apps/docs/src/components/icons/MagnifyingGlassIcon.jsx create mode 100644 apps/docs/src/components/icons/MapPinIcon.jsx create mode 100644 apps/docs/src/components/icons/PackageIcon.jsx create mode 100644 apps/docs/src/components/icons/PaperAirplaneIcon.jsx create mode 100644 apps/docs/src/components/icons/PaperClipIcon.jsx create mode 100644 apps/docs/src/components/icons/ShapesIcon.jsx create mode 100644 apps/docs/src/components/icons/ShirtIcon.jsx create mode 100644 apps/docs/src/components/icons/SquaresPlusIcon.jsx create mode 100644 apps/docs/src/components/icons/TagIcon.jsx create mode 100644 apps/docs/src/components/icons/UserIcon.jsx create mode 100644 apps/docs/src/components/icons/UsersIcon.jsx create mode 100644 apps/docs/src/components/mdx.jsx create mode 100644 apps/docs/src/images/logos/go.svg create mode 100644 apps/docs/src/images/logos/node.svg create mode 100644 apps/docs/src/images/logos/php.svg create mode 100644 apps/docs/src/images/logos/python.svg create mode 100644 apps/docs/src/images/logos/ruby.svg create mode 100644 apps/docs/src/lib/remToPx.js create mode 100644 apps/docs/src/pages/_app.jsx create mode 100644 apps/docs/src/pages/_document.jsx create mode 100644 apps/docs/src/pages/get-started.mdx create mode 100644 apps/docs/src/pages/index.mdx create mode 100644 apps/docs/src/pages/plane-basics.mdx create mode 100644 apps/docs/src/pages/self-hosting.mdx create mode 100644 apps/docs/src/styles/tailwind.css create mode 100644 apps/docs/tailwind.config.js create mode 100644 apps/docs/typography.js create mode 100644 apps/docs/yarn.lock diff --git a/apps/docs/README.md b/apps/docs/README.md new file mode 100644 index 000000000..23afa0ea9 --- /dev/null +++ b/apps/docs/README.md @@ -0,0 +1,3 @@ +# Plane Docs + +Source code that powers plane.so/docs \ No newline at end of file diff --git a/apps/docs/jsconfig.json b/apps/docs/jsconfig.json new file mode 100644 index 000000000..2c8ee2bb0 --- /dev/null +++ b/apps/docs/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +} diff --git a/apps/docs/mdx/recma.mjs b/apps/docs/mdx/recma.mjs new file mode 100644 index 000000000..489011603 --- /dev/null +++ b/apps/docs/mdx/recma.mjs @@ -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, +] diff --git a/apps/docs/mdx/rehype.mjs b/apps/docs/mdx/rehype.mjs new file mode 100644 index 000000000..d13c65aaa --- /dev/null +++ b/apps/docs/mdx/rehype.mjs @@ -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 }) => `${children}`, + }, + }) + } + } + }) + } +} + +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()}]`, + }), + ], +] diff --git a/apps/docs/mdx/remark.mjs b/apps/docs/mdx/remark.mjs new file mode 100644 index 000000000..f5c0500bc --- /dev/null +++ b/apps/docs/mdx/remark.mjs @@ -0,0 +1,3 @@ +import { mdxAnnotations } from 'mdx-annotations' + +export const remarkPlugins = [mdxAnnotations.remark] diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs new file mode 100644 index 000000000..3fda9ef1e --- /dev/null +++ b/apps/docs/next.config.mjs @@ -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) diff --git a/apps/docs/package.json b/apps/docs/package.json new file mode 100644 index 000000000..17d16c803 --- /dev/null +++ b/apps/docs/package.json @@ -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" + } +} diff --git a/apps/docs/postcss.config.js b/apps/docs/postcss.config.js new file mode 100644 index 000000000..6573c2532 --- /dev/null +++ b/apps/docs/postcss.config.js @@ -0,0 +1,9 @@ +module.exports = { + plugins: { + tailwindcss: {}, + 'postcss-focus-visible': { + replaceWith: '[data-focus-visible-added]', + }, + autoprefixer: {}, + }, +} diff --git a/apps/docs/prettier.config.js b/apps/docs/prettier.config.js new file mode 100644 index 000000000..35bb2b2a6 --- /dev/null +++ b/apps/docs/prettier.config.js @@ -0,0 +1,5 @@ +module.exports = { + singleQuote: true, + semi: false, + plugins: [require('prettier-plugin-tailwindcss')], +} diff --git a/apps/docs/public/favicon.ico b/apps/docs/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..56366837ad40513fa2c1ce3941948656b87c74d3 GIT binary patch literal 15406 zcmeHNJ8u**5H^B>G8FWw5M?Tw6hr}$AAnFQ5Kw_=c|`#Ui8qKx2?P-y1q1{Flmrn9 zC+yvW@yxm{}zmCLu&)W9fP0Iw5XxUQ^Tj`wc?8TQ9^$F2hy0Mk${Y*IzoFK44jv&7hF5 zBh*#0Dv_(|`lgw|=QMZwxJ2gdGvBgq{mwqQN_2R$ME3EOn$(BRkSo!5gJuTqN@RPl zm-91&Pd#-(f0fRttNJzb@=iH_uDi3GuimRPYJKnwt|VAz$OUp;hXTo0dLVW3ulj`4 zPycGx2hWhD!^x{tUWETq=0KD=DC@d9g%FwWGmVTE2K^Y@akgUkXFEqq?LU7*n*H4Se zqrNp6eqLaX!OnF+w)J;{tkl#(a9-i`4ldui2$>HrLX9zE5$3m=Th0{yma=s_o0_7+ z$?dLnGVlA1e4Vc)p6ky)+aLQ>9cBCD@wS|pWc#b)_#>9UpX!$!|$AgUke_qwQMWRjBdsVqreEgwp->p&=ZKE9c<;ga-R%7{eC{?LKmLvW^&Jz#zu`ZMPm8ntAl~5H0Q{Z@?9pMGeR!UV z$J(gaxy>E?ZuY@hvPXwJ+mY=W7g@fiBHQ48pf7I7 z_Fm&Ya-5E0501K66Cke2*o64x0`Lyqt#qoy9r)9cO3}>g9v+)E*EUqU`HQ>c_ok}q z&uaqb*|q*3HdE5yXKlm%De1q>x2^nzokoA%ehmL|J{bE6f7T%VPvY$-p#NkaxyWxS zKU(&mHTFM&6Yfb&95jNk zZT}xA4r2YrW8k}goWEbI$3Fep=pWukVclr>-z?{~u^(eUx=#u7To?Pn`62I{u=jxc zD9GJ7NcPCDs_nl5`F~y`V9%%D>#OJ)x?>&USi_tD70hYFUl1Eg@;5N`O+H9J<5))B yCy1kNL3HY7zto)M`vbTGk>vXu>JAFVfr0Ue>o<8dSEbFU`!mqr{0sw?GVmX3(}Q9F literal 0 HcmV?d00001 diff --git a/apps/docs/src/components/Button.jsx b/apps/docs/src/components/Button.jsx new file mode 100644 index 000000000..46f14f34c --- /dev/null +++ b/apps/docs/src/components/Button.jsx @@ -0,0 +1,62 @@ +import Link from 'next/link' +import clsx from 'clsx' + +function ArrowIcon(props) { + return ( + + ) +} + +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 = ( + + ) + + return ( + + {arrow === 'left' && arrowIcon} + {children} + {arrow === 'right' && arrowIcon} + + ) +} diff --git a/apps/docs/src/components/Code.jsx b/apps/docs/src/components/Code.jsx new file mode 100644 index 000000000..82c3d38bd --- /dev/null +++ b/apps/docs/src/components/Code.jsx @@ -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 ( + + ) +} + +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 ( + + ) +} + +function CodePanelHeader({ tag, label }) { + if (!tag && !label) { + return null + } + + return ( +
+ {tag && ( +
+ {tag} +
+ )} + {tag && label && ( + + )} + {label && ( + {label} + )} +
+ ) +} + +function CodePanel({ tag, label, code, children }) { + let child = Children.only(children) + + return ( +
+ +
+
{children}
+ +
+
+ ) +} + +function CodeGroupHeader({ title, children, selectedIndex }) { + let hasTabs = Children.count(children) > 1 + + if (!title && !hasTabs) { + return null + } + + return ( +
+ {title && ( +

+ {title} +

+ )} + {hasTabs && ( + + {Children.map(children, (child, childIndex) => ( + + {getPanelTitle(child.props)} + + ))} + + )} +
+ ) +} + +function CodeGroupPanels({ children, ...props }) { + let hasTabs = Children.count(children) > 1 + + if (hasTabs) { + return ( + + {Children.map(children, (child) => ( + + {child} + + ))} + + ) + } + + return {children} +} + +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 ( + + + + {children} + + {children} + + + ) +} + +export function Code({ children, ...props }) { + let isGrouped = useContext(CodeGroupContext) + + if (isGrouped) { + return + } + + return {children} +} + +export function Pre({ children, ...props }) { + let isGrouped = useContext(CodeGroupContext) + + if (isGrouped) { + return children + } + + return {children} +} diff --git a/apps/docs/src/components/Footer.jsx b/apps/docs/src/components/Footer.jsx new file mode 100644 index 000000000..b1486ef32 --- /dev/null +++ b/apps/docs/src/components/Footer.jsx @@ -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 ( + + ) +} + +function FeedbackButton(props) { + return ( + + + {page.title} + + + ) +} + +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 ( +
+ {previousPage && ( +
+ +
+ )} + {nextPage && ( +
+ +
+ )} +
+ ) +} + +function TwitterIcon(props) { + return ( + + ) +} + +function GitHubIcon(props) { + return ( + + ) +} + +function DiscordIcon(props) { + return ( + + ) +} + +function SocialLink({ href, icon: Icon, children }) { + return ( + + {children} + + + ) +} + +function SmallPrint() { + return ( +
+

+ © Copyrights Plane {new Date().getFullYear()}. All rights reserved. +

+
+ + Follow us on Twitter + + + Follow us on GitHub + + + Join our Discord server + +
+
+ ) +} + +export function Footer() { + let router = useRouter() + + return ( +
+ + + +
+ ) +} diff --git a/apps/docs/src/components/GridPattern.jsx b/apps/docs/src/components/GridPattern.jsx new file mode 100644 index 000000000..d8337656f --- /dev/null +++ b/apps/docs/src/components/GridPattern.jsx @@ -0,0 +1,42 @@ +import { useId } from 'react' + +export function GridPattern({ width, height, x, y, squares, ...props }) { + let patternId = useId() + + return ( + + ) +} diff --git a/apps/docs/src/components/Guides.jsx b/apps/docs/src/components/Guides.jsx new file mode 100644 index 000000000..395003903 --- /dev/null +++ b/apps/docs/src/components/Guides.jsx @@ -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 ( +
+ + Guides + +
+ {guides.map((guide) => ( +
+

+ {guide.name} +

+

+ {guide.description} +

+

+ +

+
+ ))} +
+
+ ) +} diff --git a/apps/docs/src/components/Header.jsx b/apps/docs/src/components/Header.jsx new file mode 100644 index 000000000..40f60f327 --- /dev/null +++ b/apps/docs/src/components/Header.jsx @@ -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 ( +
  • + + {children} + +
  • + ) +} + +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 ( + +
    + +
    + + + + +
    +
    + +
    +
    + + +
    +
    + +
    +
    + + ) +}) diff --git a/apps/docs/src/components/Heading.jsx b/apps/docs/src/components/Heading.jsx new file mode 100644 index 000000000..bf7b7e2be --- /dev/null +++ b/apps/docs/src/components/Heading.jsx @@ -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 ( + + ) +} + +function Eyebrow({ tag, label }) { + if (!tag && !label) { + return null + } + + return ( +
    + {tag && {tag}} + {tag && label && ( + + )} + {label && ( + {label} + )} +
    + ) +} + +function Anchor({ id, inView, children }) { + return ( + + {inView && ( +
    +
    + +
    +
    + )} + {children} + + ) +} + +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 ( + <> + + + {anchor ? ( + + {children} + + ) : ( + children + )} + + + ) +} diff --git a/apps/docs/src/components/HeroPattern.jsx b/apps/docs/src/components/HeroPattern.jsx new file mode 100644 index 000000000..857895802 --- /dev/null +++ b/apps/docs/src/components/HeroPattern.jsx @@ -0,0 +1,32 @@ +import { GridPattern } from '@/components/GridPattern' + +export function HeroPattern() { + return ( +
    +
    +
    + +
    + +
    +
    + ) +} diff --git a/apps/docs/src/components/Layout.jsx b/apps/docs/src/components/Layout.jsx new file mode 100644 index 000000000..0c8e3aa64 --- /dev/null +++ b/apps/docs/src/components/Layout.jsx @@ -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 ( + +
    + +
    + + + + + + + +
    +
    + + +
    +
    + {children} +
    +
    +
    +
    +
    + ) +} diff --git a/apps/docs/src/components/Libraries.jsx b/apps/docs/src/components/Libraries.jsx new file mode 100644 index 000000000..c3b452c66 --- /dev/null +++ b/apps/docs/src/components/Libraries.jsx @@ -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 ( +
    + + Official libraries + +
    + {libraries.map((library) => ( +
    +
    +

    + {library.name} +

    +

    + {library.description} +

    +

    + +

    +
    + +
    + ))} +
    +
    + ) +} diff --git a/apps/docs/src/components/Logo.jsx b/apps/docs/src/components/Logo.jsx new file mode 100644 index 000000000..8b90593b6 --- /dev/null +++ b/apps/docs/src/components/Logo.jsx @@ -0,0 +1,5 @@ +export function Logo() { + return ( + Plane + ) +} diff --git a/apps/docs/src/components/MobileNavigation.jsx b/apps/docs/src/components/MobileNavigation.jsx new file mode 100644 index 000000000..e2618d43c --- /dev/null +++ b/apps/docs/src/components/MobileNavigation.jsx @@ -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 ( + + ) +} + +function XIcon(props) { + return ( + + ) +} + +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 ( + + + {!isInsideMobileNavigation && ( + + + +
    + + + + +
    + + + + + + + + +
    +
    + )} +
    + ) +} diff --git a/apps/docs/src/components/ModeToggle.jsx b/apps/docs/src/components/ModeToggle.jsx new file mode 100644 index 000000000..f58c65759 --- /dev/null +++ b/apps/docs/src/components/ModeToggle.jsx @@ -0,0 +1,54 @@ +function SunIcon(props) { + return ( + + ) +} + +function MoonIcon(props) { + return ( + + ) +} + +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 ( + + ) +} diff --git a/apps/docs/src/components/Navigation.jsx b/apps/docs/src/components/Navigation.jsx new file mode 100644 index 000000000..a729a27a8 --- /dev/null +++ b/apps/docs/src/components/Navigation.jsx @@ -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 ( +
  • + + {children} + +
  • + ) +} + +function NavLink({ href, tag, active, isAnchorLink = false, children }) { + return ( + + {children} + {tag && ( + + {tag} + + )} + + ) +} + +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 ( + + ) +} + +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 ( + + ) +} + +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 ( +
  • + + {group.title} + +
    + + {isActiveGroup && ( + + )} + + + + {isActiveGroup && ( + + )} + +
      + {group.links.map((link) => ( + + + {link.title} + + + {link.href === router.pathname && sections.length > 0 && ( + + {sections.map((section) => ( +
    • + + {section.title} + +
    • + ))} +
      + )} +
      +
      + ))} +
    +
    +
  • + ) +} + +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 ( + + ) +} diff --git a/apps/docs/src/components/Prose.jsx b/apps/docs/src/components/Prose.jsx new file mode 100644 index 000000000..9fff2b391 --- /dev/null +++ b/apps/docs/src/components/Prose.jsx @@ -0,0 +1,10 @@ +import clsx from 'clsx' + +export function Prose({ as: Component = 'div', className, ...props }) { + return ( + + ) +} diff --git a/apps/docs/src/components/Resources.jsx b/apps/docs/src/components/Resources.jsx new file mode 100644 index 000000000..86d0b202a --- /dev/null +++ b/apps/docs/src/components/Resources.jsx @@ -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 ( +
    + +
    + ) +} + +function ResourcePattern({ mouseX, mouseY, ...gridProps }) { + let maskImage = useMotionTemplate`radial-gradient(180px at ${mouseX}px ${mouseY}px, white, transparent)` + let style = { maskImage, WebkitMaskImage: maskImage } + + return ( +
    +
    + +
    + + + + +
    + ) +} + +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 ( +
    + +
    +
    + +

    + + + {resource.name} + +

    +

    + {resource.description} +

    +
    +
    + ) +} + +export function Resources() { + return ( +
    + + Resources + +
    + {resources.map((resource) => ( + + ))} +
    +
    + ) +} diff --git a/apps/docs/src/components/Search.jsx b/apps/docs/src/components/Search.jsx new file mode 100644 index 000000000..28e1f0f01 --- /dev/null +++ b/apps/docs/src/components/Search.jsx @@ -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: + '', + highlightPostTag: '', + }, + }, + ], + }) + }, + }, + ] + }, + }) + ) + + 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 ( + + ) +} + +function NoResultsIcon(props) { + return ( + + ) +} + +function LoadingIcon(props) { + let id = useId() + + return ( + + ) +} + +function SearchResult({ result, resultIndex, autocomplete, collection }) { + let id = useId() + let { titleHtml, hierarchyHtml } = resolveResult(result) + + return ( +
  • 0 && 'border-t border-zinc-100 dark:border-zinc-800' + )} + aria-labelledby={`${id}-hierarchy ${id}-title`} + {...autocomplete.getItemProps({ + item: result, + source: collection.source, + })} + > +
  • + ) +} + +function SearchResults({ autocomplete, query, collection }) { + if (collection.items.length === 0) { + return ( +
    + +

    + Nothing found for{' '} + + ‘{query}’ + + . Please try again. +

    +
    + ) + } + + return ( +
      + {collection.items.map((result, resultIndex) => ( + + ))} +
    + ) +} + +const SearchInput = forwardRef(function SearchInput( + { autocomplete, autocompleteState, onClose }, + inputRef +) { + let inputProps = autocomplete.getInputProps({}) + + return ( +
    + + { + 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' && ( +
    + +
    + )} +
    + ) +}) + +function AlgoliaLogo(props) { + return ( + + + + + + + ) +} + +function SearchButton(props) { + let [modifierKey, setModifierKey] = useState() + + useEffect(() => { + setModifierKey( + /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? 'āŒ˜' : 'Ctrl ' + ) + }, []) + + return ( + <> + + + + ) +} + +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 ( + autocomplete.setQuery('')} + > + + +
    + + +
    + + +
    +
    + setOpen(false)} + /> +
    + {autocompleteState.isOpen && ( + <> + +

    + Search by{' '} + +

    + + )} +
    + +
    +
    +
    +
    +
    +
    + ) +} + +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 ( +
    + + +
    + ) +} + +export function MobileSearch() { + let { buttonProps, dialogProps } = useSearchProps() + + return ( +
    + + +
    + ) +} diff --git a/apps/docs/src/components/SectionProvider.jsx b/apps/docs/src/components/SectionProvider.jsx new file mode 100644 index 000000000..95f1496c5 --- /dev/null +++ b/apps/docs/src/components/SectionProvider.jsx @@ -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 ( + + {children} + + ) +} + +export function useSectionStore(selector) { + let store = useContext(SectionStoreContext) + return useStore(store, selector) +} diff --git a/apps/docs/src/components/Tag.jsx b/apps/docs/src/components/Tag.jsx new file mode 100644 index 000000000..4fba599c2 --- /dev/null +++ b/apps/docs/src/components/Tag.jsx @@ -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 ( + + {children} + + ) +} diff --git a/apps/docs/src/components/icons/BellIcon.jsx b/apps/docs/src/components/icons/BellIcon.jsx new file mode 100644 index 000000000..09062dd9b --- /dev/null +++ b/apps/docs/src/components/icons/BellIcon.jsx @@ -0,0 +1,17 @@ +export function BellIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/BoltIcon.jsx b/apps/docs/src/components/icons/BoltIcon.jsx new file mode 100644 index 000000000..1278a7fe8 --- /dev/null +++ b/apps/docs/src/components/icons/BoltIcon.jsx @@ -0,0 +1,11 @@ +export function BoltIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/BookIcon.jsx b/apps/docs/src/components/icons/BookIcon.jsx new file mode 100644 index 000000000..d560f41c8 --- /dev/null +++ b/apps/docs/src/components/icons/BookIcon.jsx @@ -0,0 +1,17 @@ +export function BookIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/CalendarIcon.jsx b/apps/docs/src/components/icons/CalendarIcon.jsx new file mode 100644 index 000000000..1a36be6cb --- /dev/null +++ b/apps/docs/src/components/icons/CalendarIcon.jsx @@ -0,0 +1,23 @@ +export function CalendarIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/CartIcon.jsx b/apps/docs/src/components/icons/CartIcon.jsx new file mode 100644 index 000000000..e42462467 --- /dev/null +++ b/apps/docs/src/components/icons/CartIcon.jsx @@ -0,0 +1,15 @@ +export function CartIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/ChatBubbleIcon.jsx b/apps/docs/src/components/icons/ChatBubbleIcon.jsx new file mode 100644 index 000000000..b929ec643 --- /dev/null +++ b/apps/docs/src/components/icons/ChatBubbleIcon.jsx @@ -0,0 +1,17 @@ +export function ChatBubbleIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/CheckIcon.jsx b/apps/docs/src/components/icons/CheckIcon.jsx new file mode 100644 index 000000000..33d2b249a --- /dev/null +++ b/apps/docs/src/components/icons/CheckIcon.jsx @@ -0,0 +1,17 @@ +export function CheckIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/ChevronRightLeftIcon.jsx b/apps/docs/src/components/icons/ChevronRightLeftIcon.jsx new file mode 100644 index 000000000..2dbaa18c4 --- /dev/null +++ b/apps/docs/src/components/icons/ChevronRightLeftIcon.jsx @@ -0,0 +1,17 @@ +export function ChevronRightLeftIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/ClipboardIcon.jsx b/apps/docs/src/components/icons/ClipboardIcon.jsx new file mode 100644 index 000000000..9c8c55cee --- /dev/null +++ b/apps/docs/src/components/icons/ClipboardIcon.jsx @@ -0,0 +1,17 @@ +export function ClipboardIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/CogIcon.jsx b/apps/docs/src/components/icons/CogIcon.jsx new file mode 100644 index 000000000..57023fc7c --- /dev/null +++ b/apps/docs/src/components/icons/CogIcon.jsx @@ -0,0 +1,19 @@ +export function CogIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/CopyIcon.jsx b/apps/docs/src/components/icons/CopyIcon.jsx new file mode 100644 index 000000000..aadede17d --- /dev/null +++ b/apps/docs/src/components/icons/CopyIcon.jsx @@ -0,0 +1,17 @@ +export function CopyIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/DocumentIcon.jsx b/apps/docs/src/components/icons/DocumentIcon.jsx new file mode 100644 index 000000000..b576db048 --- /dev/null +++ b/apps/docs/src/components/icons/DocumentIcon.jsx @@ -0,0 +1,17 @@ +export function DocumentIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/EnvelopeIcon.jsx b/apps/docs/src/components/icons/EnvelopeIcon.jsx new file mode 100644 index 000000000..17ab4fa95 --- /dev/null +++ b/apps/docs/src/components/icons/EnvelopeIcon.jsx @@ -0,0 +1,17 @@ +export function EnvelopeIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/FaceSmileIcon.jsx b/apps/docs/src/components/icons/FaceSmileIcon.jsx new file mode 100644 index 000000000..4c01755a3 --- /dev/null +++ b/apps/docs/src/components/icons/FaceSmileIcon.jsx @@ -0,0 +1,17 @@ +export function FaceSmileIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/FolderIcon.jsx b/apps/docs/src/components/icons/FolderIcon.jsx new file mode 100644 index 000000000..0e8478e4c --- /dev/null +++ b/apps/docs/src/components/icons/FolderIcon.jsx @@ -0,0 +1,22 @@ +export function FolderIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/LinkIcon.jsx b/apps/docs/src/components/icons/LinkIcon.jsx new file mode 100644 index 000000000..2dace2318 --- /dev/null +++ b/apps/docs/src/components/icons/LinkIcon.jsx @@ -0,0 +1,12 @@ +export function LinkIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/ListIcon.jsx b/apps/docs/src/components/icons/ListIcon.jsx new file mode 100644 index 000000000..ea45c0a6d --- /dev/null +++ b/apps/docs/src/components/icons/ListIcon.jsx @@ -0,0 +1,17 @@ +export function ListIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/MagnifyingGlassIcon.jsx b/apps/docs/src/components/icons/MagnifyingGlassIcon.jsx new file mode 100644 index 000000000..ff7e8a6a8 --- /dev/null +++ b/apps/docs/src/components/icons/MagnifyingGlassIcon.jsx @@ -0,0 +1,13 @@ +export function MagnifyingGlassIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/MapPinIcon.jsx b/apps/docs/src/components/icons/MapPinIcon.jsx new file mode 100644 index 000000000..b581a6d68 --- /dev/null +++ b/apps/docs/src/components/icons/MapPinIcon.jsx @@ -0,0 +1,19 @@ +export function MapPinIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/PackageIcon.jsx b/apps/docs/src/components/icons/PackageIcon.jsx new file mode 100644 index 000000000..cb0bc98d2 --- /dev/null +++ b/apps/docs/src/components/icons/PackageIcon.jsx @@ -0,0 +1,16 @@ +export function PackageIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/PaperAirplaneIcon.jsx b/apps/docs/src/components/icons/PaperAirplaneIcon.jsx new file mode 100644 index 000000000..6f96ea84b --- /dev/null +++ b/apps/docs/src/components/icons/PaperAirplaneIcon.jsx @@ -0,0 +1,17 @@ +export function PaperAirplaneIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/PaperClipIcon.jsx b/apps/docs/src/components/icons/PaperClipIcon.jsx new file mode 100644 index 000000000..cdd72437a --- /dev/null +++ b/apps/docs/src/components/icons/PaperClipIcon.jsx @@ -0,0 +1,12 @@ +export function PaperClipIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/ShapesIcon.jsx b/apps/docs/src/components/icons/ShapesIcon.jsx new file mode 100644 index 000000000..5dd29fd0d --- /dev/null +++ b/apps/docs/src/components/icons/ShapesIcon.jsx @@ -0,0 +1,17 @@ +export function ShapesIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/ShirtIcon.jsx b/apps/docs/src/components/icons/ShirtIcon.jsx new file mode 100644 index 000000000..30c67b920 --- /dev/null +++ b/apps/docs/src/components/icons/ShirtIcon.jsx @@ -0,0 +1,11 @@ +export function ShirtIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/SquaresPlusIcon.jsx b/apps/docs/src/components/icons/SquaresPlusIcon.jsx new file mode 100644 index 000000000..39a507a4e --- /dev/null +++ b/apps/docs/src/components/icons/SquaresPlusIcon.jsx @@ -0,0 +1,17 @@ +export function SquaresPlusIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/TagIcon.jsx b/apps/docs/src/components/icons/TagIcon.jsx new file mode 100644 index 000000000..c7e57b038 --- /dev/null +++ b/apps/docs/src/components/icons/TagIcon.jsx @@ -0,0 +1,19 @@ +export function TagIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/UserIcon.jsx b/apps/docs/src/components/icons/UserIcon.jsx new file mode 100644 index 000000000..350e3c42d --- /dev/null +++ b/apps/docs/src/components/icons/UserIcon.jsx @@ -0,0 +1,24 @@ +export function UserIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/icons/UsersIcon.jsx b/apps/docs/src/components/icons/UsersIcon.jsx new file mode 100644 index 000000000..68ef13120 --- /dev/null +++ b/apps/docs/src/components/icons/UsersIcon.jsx @@ -0,0 +1,28 @@ +export function UsersIcon(props) { + return ( + + ) +} diff --git a/apps/docs/src/components/mdx.jsx b/apps/docs/src/components/mdx.jsx new file mode 100644 index 000000000..34ce07bc0 --- /dev/null +++ b/apps/docs/src/components/mdx.jsx @@ -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 +} + +function InfoIcon(props) { + return ( + + ) +} + +export function Note({ children }) { + return ( +
    + +
    + {children} +
    +
    + ) +} + +export function Row({ children }) { + return ( +
    + {children} +
    + ) +} + +export function Col({ children, sticky = false }) { + return ( +
    :first-child]:mt-0 [&>:last-child]:mb-0', + sticky && 'xl:sticky xl:top-24' + )} + > + {children} +
    + ) +} + +export function Properties({ children }) { + return ( +
    +
      + {children} +
    +
    + ) +} + +export function Property({ name, type, children }) { + return ( +
  • +
    +
    Name
    +
    + {name} +
    +
    Type
    +
    + {type} +
    +
    Description
    +
    + {children} +
    +
    +
  • + ) +} diff --git a/apps/docs/src/images/logos/go.svg b/apps/docs/src/images/logos/go.svg new file mode 100644 index 000000000..7f7b19de5 --- /dev/null +++ b/apps/docs/src/images/logos/go.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/apps/docs/src/images/logos/node.svg b/apps/docs/src/images/logos/node.svg new file mode 100644 index 000000000..1d09de22b --- /dev/null +++ b/apps/docs/src/images/logos/node.svg @@ -0,0 +1,4 @@ + + + diff --git a/apps/docs/src/images/logos/php.svg b/apps/docs/src/images/logos/php.svg new file mode 100644 index 000000000..0a9ac462a --- /dev/null +++ b/apps/docs/src/images/logos/php.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/apps/docs/src/images/logos/python.svg b/apps/docs/src/images/logos/python.svg new file mode 100644 index 000000000..9bceb587a --- /dev/null +++ b/apps/docs/src/images/logos/python.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/apps/docs/src/images/logos/ruby.svg b/apps/docs/src/images/logos/ruby.svg new file mode 100644 index 000000000..b22a5bf10 --- /dev/null +++ b/apps/docs/src/images/logos/ruby.svg @@ -0,0 +1,4 @@ + + + diff --git a/apps/docs/src/lib/remToPx.js b/apps/docs/src/lib/remToPx.js new file mode 100644 index 000000000..9ae9628d6 --- /dev/null +++ b/apps/docs/src/lib/remToPx.js @@ -0,0 +1,8 @@ +export function remToPx(remValue) { + let rootFontSize = + typeof window === 'undefined' + ? 16 + : parseFloat(window.getComputedStyle(document.documentElement).fontSize) + + return parseFloat(remValue) * rootFontSize +} diff --git a/apps/docs/src/pages/_app.jsx b/apps/docs/src/pages/_app.jsx new file mode 100644 index 000000000..40c617521 --- /dev/null +++ b/apps/docs/src/pages/_app.jsx @@ -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 ( + <> + + {router.pathname === '/' ? ( + Plane Documentation + ) : ( + {`${pageProps.title} - Plane Documentation`} + )} + + + + + + + + + ) +} diff --git a/apps/docs/src/pages/_document.jsx b/apps/docs/src/pages/_document.jsx new file mode 100644 index 000000000..db198263e --- /dev/null +++ b/apps/docs/src/pages/_document.jsx @@ -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 ( + + +