diff --git a/.changeset/config.json b/.changeset/config.json index 967537e..a569f8e 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -2,7 +2,7 @@ "$schema": "https://unpkg.com/@changesets/config@1.4.0/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, - "linked": [["@stacks/ui", "@stacks/ui-*"]], + "linked": [], "access": "public", "baseBranch": "master", "updateInternalDependencies": "patch", diff --git a/.gitignore b/.gitignore index a250dfd..eed510e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ dist node_modules .idea .vscode -lib +./lib mdincludes emotion yarn-error.log diff --git a/docs/.eslintrc.js b/docs/.eslintrc.js new file mode 100644 index 0000000..9a5b99f --- /dev/null +++ b/docs/.eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + extends: ['@blockstack/eslint-config'], + parser: '@typescript-eslint/parser', + parserOptions: { + createDefaultProgram: true, + project: './tsconfig.json', + }, + env: { + browser: true, + node: true, + es6: true, + }, + globals: { + page: true, + browser: true, + context: true, + jestPuppeteer: true, + }, + rules: { + '@typescript-eslint/no-unsafe-assignment': 0, + '@typescript-eslint/no-unsafe-member-access': 0, + '@typescript-eslint/no-unsafe-call': 0, + '@typescript-eslint/no-unsafe-return': 0, + '@typescript-eslint/explicit-module-boundary-types': 0, + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/ban-ts-ignore': 0, + '@typescript-eslint/restrict-template-expressions': 0, + }, +}; diff --git a/docs/.github/workflows/main.yml b/docs/.github/workflows/main.yml new file mode 100644 index 0000000..ee13dfa --- /dev/null +++ b/docs/.github/workflows/main.yml @@ -0,0 +1,19 @@ +name: Code quality +on: [push] + +jobs: + code_quality: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set Node Version + uses: actions/setup-node@v1.4.2 + with: + node-version: 14 + - name: Install deps + run: yarn --frozen-lockfile + - name: Lint + run: yarn lint + - name: Typecheck + run: yarn typecheck diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..c87defb --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,45 @@ +# OS or Editor folders +.DS_Store +node_modules + +# Jekyllg +_site +.sass-cache +.jekyll-metadata +Gemfile.lock +**/.DS_Store +**/desktop.ini +**/.svn + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +.vercel +/.next/ +/out/ +/build/ + +# production +/build + +# misc +.env* +.idea + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.now +.mdx-data +.cache +.yalc +yalc.lock +.cache diff --git a/docs/.prettierrc b/docs/.prettierrc new file mode 100644 index 0000000..97becfe --- /dev/null +++ b/docs/.prettierrc @@ -0,0 +1 @@ +module.exports = require('@blockstack/prettier-config'); diff --git a/docs/.vercelignore b/docs/.vercelignore new file mode 100644 index 0000000..bc9abd0 --- /dev/null +++ b/docs/.vercelignore @@ -0,0 +1,11 @@ +.github +.next +.vercel +.mdx-data +.idea +node_modules +build +README.md +.cache +.yalc +yalc.lock diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4683fdf --- /dev/null +++ b/docs/README.md @@ -0,0 +1,30 @@ +# Blockstack documentation + +![A screenshot of docs.blockstack.org](/public/images/docs-homepage.png) + +## Running and building the site locally + +If you are interested in contributing to the site and making changes, please refer to our [contributing guide here](https://docs.blockstack.org/ecosystem/contributing). + +## Generated documentation + +### Blockstack CLI reference + +The `src/_data/cli-reference.json` file is generated from the `blockstack-cli` subcommand `docs`. + +1. Install the latest version of the cli according to the instructions at: https://github.com/blockstack/cli-blockstack + +2. Generate the json for the cli in the `docs.blockstack` repo. + + ``` + $ blockstack-cli docs > src/_data/cli-reference.json + ``` + +### Clarity Reference + +There is a json file that is generated via the `stacks-blockchain` repo, which automatically brings it over to this repo +via a github action. + +### FAQs + +All of the FAQs found at `/reference/faqs` are pulled dynamically from the zendesk api and rendered in this project. diff --git a/docs/babel.config.js b/docs/babel.config.js new file mode 100644 index 0000000..4d9d5d7 --- /dev/null +++ b/docs/babel.config.js @@ -0,0 +1,4 @@ +module.exports = { + presets: ['next/babel', '@emotion/babel-preset-css-prop'], + plugins: ['./lib/babel-plugin-nextjs-mdx-patch', 'babel-plugin-macros', '@emotion'], +}; diff --git a/docs/lib/babel-plugin-nextjs-mdx-patch.js b/docs/lib/babel-plugin-nextjs-mdx-patch.js new file mode 100644 index 0000000..3b86945 --- /dev/null +++ b/docs/lib/babel-plugin-nextjs-mdx-patch.js @@ -0,0 +1,30 @@ +/** + * Currently it's not possible to export data fetching functions from MDX pages + * because MDX includes them in `layoutProps`, and Next.js removes them at some + * point, causing a `ReferenceError`. + * + * https://github.com/mdx-js/mdx/issues/742#issuecomment-612652071 + * + * This plugin can be removed once MDX removes `layoutProps`, at least that + * seems to be the current plan. + */ + +// https://nextjs.org/docs/basic-features/data-fetching +const DATA_FETCH_FNS = ['getStaticPaths', 'getStaticProps', 'getServerProps']; + +module.exports = () => { + return { + visitor: { + ObjectProperty(path) { + if ( + DATA_FETCH_FNS.includes(path.node.value.name) && + path.findParent( + path => path.isVariableDeclarator() && path.node.id.name === 'layoutProps' + ) + ) { + path.remove(); + } + }, + }, + }; +}; diff --git a/docs/lib/mdx-frontmatter-loader.js b/docs/lib/mdx-frontmatter-loader.js new file mode 100644 index 0000000..7ece966 --- /dev/null +++ b/docs/lib/mdx-frontmatter-loader.js @@ -0,0 +1,50 @@ +const fm = require('gray-matter'); +const remark = require('remark'); +const strip = require('strip-markdown'); + +const getHeadings = mdxContent => { + const regex = /\n(#+)(.*)/gm; + const found = mdxContent.match(regex); + const getLevel = string => string.split('#'); + const headings = + found && found.length + ? found.map(f => { + const md = f.split('# ')[1]; + let content = md; + remark() + .use(strip) + .process(md, (err, file) => { + if (err) throw err; + content = file.contents.toString().trim(); + }); + const level = getLevel(f).length; + return { content, level }; + }) + : []; + return headings; +}; + +// @see https://github.com/expo/expo/blob/master/docs/common/md-loader.js +async function mdxFrontmatterLoader(src) { + const callback = this.async(); + const { content, data } = fm(src); + const headings = getHeadings(content); + const code = + `import { MDWrapper } from '@components/mdx/markdown-wrapper'; +export default function Layout({ children, ...props }){ + return ( + + {children} + +) +} + +` + content; + + return callback(null, code); +} + +module.exports = mdxFrontmatterLoader; diff --git a/docs/lib/rehype-image-size.js b/docs/lib/rehype-image-size.js new file mode 100644 index 0000000..fa7c11d --- /dev/null +++ b/docs/lib/rehype-image-size.js @@ -0,0 +1,36 @@ +const memoize = require('micro-memoize'); +const visit = require('unist-util-visit'); +const pAll = require('p-all'); +const sizeOf = require('image-size'); + +/** + * Simple plugin to get the size of local images so we can use it in react + */ +const rehypeImageSize = () => { + async function transformer(tree) { + const nodes = []; + visit(tree, 'element', node => { + if (node.tagName !== 'img') { + return; + } else { + nodes.push(node); + } + }); + await pAll( + nodes.map(node => () => visitor(node)), + { concurrency: 25 } + ); + return tree; + } + async function visitor(node) { + const isRelative = + node && node.properties && node.properties.src && node.properties.src.startsWith('/'); + if (isRelative) { + const dimensions = sizeOf(`public/${node.properties.src}`); + node.properties['dimensions'] = dimensions; + } + } + return transformer; +}; + +module.exports = memoize(rehypeImageSize); diff --git a/docs/lib/rehype-plugins.js b/docs/lib/rehype-plugins.js new file mode 100644 index 0000000..27894bb --- /dev/null +++ b/docs/lib/rehype-plugins.js @@ -0,0 +1,7 @@ +const memoize = require('micro-memoize'); +const { rehypeVscode } = require('unified-vscode'); +const rehypeImgs = require('./rehype-image-size'); + +const rehypePlugins = [memoize(rehypeVscode), rehypeImgs]; + +module.exports = { rehypePlugins }; diff --git a/docs/lib/remark-custom-blocks.js b/docs/lib/remark-custom-blocks.js new file mode 100644 index 0000000..ce563f3 --- /dev/null +++ b/docs/lib/remark-custom-blocks.js @@ -0,0 +1,160 @@ +const spaceSeparated = require('space-separated-tokens'); + +function escapeRegExp(str) { + return str.replace(new RegExp(`[-[\\]{}()*+?.\\\\^$|/]`, 'g'), '\\$&'); +} + +const C_NEWLINE = '\n'; +const C_FENCE = '|'; + +function compilerFactory(nodeType) { + let text; + let title; + + return { + blockHeading(node) { + title = this.all(node).join(''); + return ''; + }, + blockBody(node) { + text = this.all(node) + .map(s => s.replace(/\n/g, '\n| ')) + .join('\n|\n| '); + return text; + }, + block(node) { + text = ''; + title = ''; + this.all(node); + if (title) { + return `[[${nodeType} | ${title}]]\n| ${text}`; + } else { + return `[[${nodeType}]]\n| ${text}`; + } + }, + }; +} + +module.exports = function blockPlugin(availableBlocks = {}) { + const pattern = Object.keys(availableBlocks).map(escapeRegExp).join('|'); + + if (!pattern) { + throw new Error('remark-custom-blocks needs to be passed a configuration object as option'); + } + + const regex = new RegExp(`\\[\@(${pattern})(?: *\\| *(.*))?\]\n`); + + function blockTokenizer(eat, value, silent) { + const now = eat.now(); + const keep = regex.exec(value); + if (!keep) return; + if (keep.index !== 0) return; + const [eaten, blockType, blockTitle] = keep; + + /* istanbul ignore if - never used (yet) */ + if (silent) return true; + + const linesToEat = []; + const content = []; + + let idx = 0; + while ((idx = value.indexOf(C_NEWLINE)) !== -1) { + const next = value.indexOf(C_NEWLINE, idx + 1); + // either slice until next NEWLINE or slice until end of string + const lineToEat = next !== -1 ? value.slice(idx + 1, next) : value.slice(idx + 1); + if (lineToEat[0] !== C_FENCE) break; + // remove leading `FENCE ` or leading `FENCE` + const line = lineToEat.slice(lineToEat.startsWith(`${C_FENCE} `) ? 2 : 1); + linesToEat.push(lineToEat); + content.push(line); + value = value.slice(idx + 1); + } + + const contentString = content.join(C_NEWLINE); + + const stringToEat = eaten + linesToEat.join(C_NEWLINE); + + const potentialBlock = availableBlocks[blockType]; + const titleAllowed = + potentialBlock.title && ['optional', 'required'].includes(potentialBlock.title); + const titleRequired = potentialBlock.title && potentialBlock.title === 'required'; + + if (titleRequired && !blockTitle) return; + if (!titleAllowed && blockTitle) return; + + const add = eat(stringToEat); + if (potentialBlock.details) { + potentialBlock.containerElement = 'details'; + potentialBlock.titleElement = 'summary'; + } + + const exit = this.enterBlock(); + const contents = { + type: `${blockType}CustomBlockBody`, + data: { + hName: potentialBlock.contentsElement ? potentialBlock.contentsElement : 'div', + hProperties: { + className: 'custom-block-body', + }, + }, + children: this.tokenizeBlock(contentString, now), + }; + exit(); + + const blockChildren = [contents]; + if (titleAllowed && blockTitle) { + const titleElement = potentialBlock.titleElement ? potentialBlock.titleElement : 'div'; + const titleNode = { + type: `${blockType}CustomBlockHeading`, + data: { + hName: titleElement, + hProperties: { + className: 'custom-block-heading', + }, + }, + children: this.tokenizeInline(blockTitle, now), + }; + + blockChildren.unshift(titleNode); + } + + const classList = spaceSeparated.parse(potentialBlock.classes || ''); + + return add({ + type: `${blockType}CustomBlock`, + children: blockChildren, + data: { + hName: potentialBlock.containerElement ? potentialBlock.containerElement : 'div', + hProperties: { + className: ['custom-block', ...classList], + }, + }, + }); + } + + const Parser = this.Parser; + + // Inject blockTokenizer + const blockTokenizers = Parser.prototype.blockTokenizers; + const blockMethods = Parser.prototype.blockMethods; + blockTokenizers.customBlocks = blockTokenizer; + blockMethods.splice(blockMethods.indexOf('fencedCode') + 1, 0, 'customBlocks'); + const Compiler = this.Compiler; + if (Compiler) { + const visitors = Compiler.prototype.visitors; + if (!visitors) return; + Object.keys(availableBlocks).forEach(key => { + const compiler = compilerFactory(key); + visitors[`${key}CustomBlock`] = compiler.block; + visitors[`${key}CustomBlockHeading`] = compiler.blockHeading; + visitors[`${key}CustomBlockBody`] = compiler.blockBody; + }); + } + // Inject into interrupt rules + const interruptParagraph = Parser.prototype.interruptParagraph; + const interruptList = Parser.prototype.interruptList; + const interruptBlockquote = Parser.prototype.interruptBlockquote; + interruptParagraph.splice(interruptParagraph.indexOf('fencedCode') + 1, 0, ['customBlocks']); + interruptList.splice(interruptList.indexOf('fencedCode') + 1, 0, ['customBlocks']); + interruptBlockquote.splice(interruptBlockquote.indexOf('fencedCode') + 1, 0, ['customBlocks']); +}; diff --git a/docs/lib/remark-include.js b/docs/lib/remark-include.js new file mode 100644 index 0000000..62bef1f --- /dev/null +++ b/docs/lib/remark-include.js @@ -0,0 +1,51 @@ +const path = require('path'); +const remark = require('remark'); +const flatMap = require('unist-util-flatmap'); +const { readSync } = require('to-vfile'); + +module.exports = function includeMarkdownPlugin({ resolveFrom } = {}) { + return function transformer(tree, file) { + return flatMap(tree, node => { + if (node.type !== 'paragraph') return [node]; + + // detect an `@include` statement + const includeMatch = + node.children[0].value && node.children[0].value.match(/^@include\s['"](.*)['"]$/); + if (!includeMatch) return [node]; + + // read the file contents + const includePath = path.join(resolveFrom || file.dirname, includeMatch[1]); + let includeContents; + try { + includeContents = readSync(includePath, 'utf8'); + } catch (err) { + console.log(err); + throw new Error( + `The @include file path at ${includePath} was not found.\n\nInclude Location: ${file.path}:${node.position.start.line}:${node.position.start.column}` + ); + } + + // if we are including a ".md" or ".mdx" file, we add the contents as processed markdown + // if any other file type, they are embedded into a code block + if (includePath.match(/\.md(?:x)?$/)) { + // return the file contents in place of the @include + // this takes a couple steps because we allow recursive includes + const processor = remark().use(includeMarkdownPlugin, { resolveFrom }); + const ast = processor.parse(includeContents); + return processor.runSync(ast, includeContents).children; + } else { + // trim trailing newline + includeContents.contents = includeContents.contents.trim(); + + // return contents wrapped inside a "code" node + return [ + { + type: 'code', + lang: includePath.match(/\.(\w+)$/)[1], + value: includeContents, + }, + ]; + } + }); + }; +}; diff --git a/docs/lib/remark-paragraph-alerts.js b/docs/lib/remark-paragraph-alerts.js new file mode 100644 index 0000000..bd0c663 --- /dev/null +++ b/docs/lib/remark-paragraph-alerts.js @@ -0,0 +1,41 @@ +const is = require('unist-util-is'); +const visit = require('unist-util-visit'); + +const sigils = { + '=>': 'success', + '->': 'info', + '~>': 'warning', + '!>': 'danger', +}; + +module.exports = function paragraphCustomAlertsPlugin() { + return function transformer(tree) { + visit(tree, 'paragraph', (pNode, _, parent) => { + visit(pNode, 'text', textNode => { + Object.keys(sigils).forEach(sigil => { + if (textNode.value.startsWith(`${sigil} `)) { + // Remove the literal sigil symbol from string contents + textNode.value = textNode.value.replace(`${sigil} `, ''); + + // Wrap matched nodes with
(containing proper attributes) + parent.children = parent.children.map(node => { + return is(pNode, node) + ? { + type: 'wrapper', + children: [node], + data: { + hName: 'blockquote', + hProperties: { + className: ['alert', `alert-${sigils[sigil]}`], + role: 'alert', + }, + }, + } + : node; + }); + } + }); + }); + }); + }; +}; diff --git a/docs/lib/remark-plugins.js b/docs/lib/remark-plugins.js new file mode 100644 index 0000000..cc5c52f --- /dev/null +++ b/docs/lib/remark-plugins.js @@ -0,0 +1,38 @@ +const memoize = require('micro-memoize'); +const path = require('path'); + +const include = require('./remark-include'); +const emoji = require('remark-emoji'); +const paragraphAlerts = require('./remark-paragraph-alerts'); +const images = require('remark-images'); +const unwrapImages = require('remark-unwrap-images'); +const slug = require('remark-slug'); +const headingID = require('remark-heading-id'); +const sectionize = require('remark-sectionize'); +const customBlocks = require('./remark-custom-blocks'); +const externalLinks = require('remark-external-links'); + +const remarkPlugins = [ + [memoize(include), { resolveFrom: path.join(__dirname, '../src/includes') }], + memoize(paragraphAlerts), + memoize(emoji), + memoize(images), + memoize(unwrapImages), + memoize(slug), + memoize(headingID), + memoize(sectionize), + memoize(externalLinks), + [ + customBlocks, + { + ['page-reference']: { + containerElement: 'pagereference', + titleElement: 'span', + bodyElement: 'span', + title: 'optional', + }, + }, + ], +]; + +module.exports = { remarkPlugins }; diff --git a/docs/lib/remark-sectons.js b/docs/lib/remark-sectons.js new file mode 100644 index 0000000..bf47765 --- /dev/null +++ b/docs/lib/remark-sectons.js @@ -0,0 +1,42 @@ +// https://github.com/jake-low/remark-sectionize +const findAfter = require('unist-util-find-after'); +const visit = require('unist-util-visit-parents'); + +const MAX_HEADING_DEPTH = 6; + +module.exports = plugin; + +function plugin() { + return transform; +} + +function transform(tree) { + for (let depth = MAX_HEADING_DEPTH; depth > 0; depth--) { + visit(tree, node => node.type === 'heading' && node.depth === depth, sectionize); + } +} + +function sectionize(node, ancestors) { + const start = node; + const depth = start.depth; + const parent = ancestors[ancestors.length - 1]; + + const isEnd = node => (node.type === 'heading' && node.depth <= depth) || node.type === 'export'; + const end = findAfter(parent, start, isEnd); + + const startIndex = parent.children.indexOf(start); + const endIndex = parent.children.indexOf(end); + + const between = parent.children.slice(startIndex, endIndex > 0 ? endIndex : undefined); + + const section = { + type: 'section', + depth: depth, + children: between, + data: { + hName: 'section', + }, + }; + + parent.children.splice(startIndex, section.children.length, section); +} diff --git a/docs/next-env.d.ts b/docs/next-env.d.ts new file mode 100644 index 0000000..7b7aa2c --- /dev/null +++ b/docs/next-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/docs/next.config.js b/docs/next.config.js new file mode 100755 index 0000000..845c868 --- /dev/null +++ b/docs/next.config.js @@ -0,0 +1,76 @@ +const withBundleAnalyzer = require('@next/bundle-analyzer')({ + enabled: process.env.ANALYZE === 'true', +}); +const path = require('path'); +const { remarkPlugins } = require('./lib/remark-plugins'); +const { rehypePlugins } = require('./lib/rehype-plugins'); +const withFonts = require('next-fonts'); +const withTM = require('next-transpile-modules')(['@tabler/icons/icons-react/dist/index.esm.js']); + +module.exports = withTM( + withFonts( + withBundleAnalyzer({ + experimental: { + modern: true, + polyfillsOptimization: true, + jsconfigPaths: true, + trailingSlash: true, + }, + pageExtensions: ['js', 'ts', 'tsx', 'md', 'mdx'], + webpack: (config, options) => { + config.module.rules.push({ + test: /.mdx?$/, // load both .md and .mdx files + use: [ + options.defaultLoaders.babel, + { + loader: '@mdx-js/loader', + options: { + remarkPlugins, + rehypePlugins, + }, + }, + path.join(__dirname, './lib/mdx-frontmatter-loader'), + ], + }); + + config.module.rules.push({ + test: /\.ya?ml$/, + type: 'json', + use: 'yaml-loader', + }); + + if (!options.dev) { + const splitChunks = config.optimization && config.optimization.splitChunks; + if (splitChunks) { + const cacheGroups = splitChunks.cacheGroups; + const test = /[\\/]node_modules[\\/](preact|preact-render-to-string|preact-context-provider)[\\/]/; + if (cacheGroups.framework) { + cacheGroups.preact = Object.assign({}, cacheGroups.framework, { + test, + }); + cacheGroups.commons.name = 'framework'; + } else { + cacheGroups.preact = { + name: 'commons', + chunks: 'all', + test, + }; + } + } + + // Install webpack aliases: + const aliases = config.resolve.alias || (config.resolve.alias = {}); + aliases.react = aliases['react-dom'] = 'preact/compat'; + // aliases['@stacks/ui'] = '@stacks/ui/dist/index.esm.js'; + aliases['react-ssr-prepass'] = 'preact-ssr-prepass'; + } + config.resolve.alias['@emotion/react'] = path.resolve( + __dirname, + './node_modules/@emotion/react' + ); + + return config; + }, + }) + ) +); diff --git a/docs/package.json b/docs/package.json new file mode 100755 index 0000000..fd1840a --- /dev/null +++ b/docs/package.json @@ -0,0 +1,145 @@ +{ + "name": "@stacks/ui-docs", + "version": "1.0.0", + "dependencies": { + "@docsearch/css": "^1.0.0-alpha.28", + "@docsearch/react": "^1.0.0-alpha.28", + "@emotion/babel-plugin": "^11.0.0-next.15", + "@emotion/babel-preset-css-prop": "^11.0.0-next.10", + "@emotion/cache": "^11.0.0-next.15", + "@emotion/core": "^11.0.0-next.10", + "@emotion/css": "^11.0.0-next.15", + "@emotion/react": "^11.0.0-next.15", + "@emotion/server": "^11.0.0-next.15", + "@hashicorp/remark-plugins": "^3.0.0", + "@mdx-js/loader": "1.6.22", + "@mdx-js/mdx": "^1.6.22", + "@mdx-js/react": "^1.6.22", + "@next/mdx": "^10.0.3", + "@reach/accordion": "^0.12.1", + "@reach/tooltip": "^0.12.1", + "@stacks/ui": "^7.0.0", + "@stacks/ui-core": "^7.0.0", + "@styled-system/theme-get": "^5.1.2", + "@tabler/icons": "^1.36.0", + "@types/mdx-js__react": "^1.5.2", + "@types/node": "^14.14.10", + "@types/nprogress": "^0.2.0", + "@types/reach__tooltip": "^0.2.0", + "algoliasearch": "^4.8.2", + "babel-plugin-macros": "^3.0.0", + "cache-manager": "^3.4.0", + "cache-manager-fs-hash": "^0.0.9", + "capsize": "^1.1.0", + "csstype": "^3.0.3", + "csvtojson": "^2.0.10", + "docsearch.js": "^2.6.3", + "emotion": "^11.0.0", + "emotion-server": "^11.0.0", + "fathom-client": "^3.0.0", + "front-matter": "^4.0.2", + "fs-extra": "^9.0.1", + "github-slugger": "^1.3.0", + "gray-matter": "^4.0.2", + "hast-util-to-string": "^1.0.4", + "html-react-parser": "^0.14.2", + "image-size": "^0.9.1", + "lodash.debounce": "^4.0.8", + "mdi-react": "7.4.0", + "micro-memoize": "^4.0.9", + "modern-normalize": "^1.0.0", + "next": "^10.0.3", + "next-fonts": "^1.4.0", + "next-mdx-remote": "^1.0.0", + "nprogress": "^0.2.0", + "p-all": "^3.0.0", + "preact": "^10.4.8", + "preact-render-to-string": "^5.1.4", + "preact-ssr-prepass": "^1.1.1", + "prettier": "^2.2.1", + "preval.macro": "^5.0.0", + "prismjs": "^1.22.0", + "react-gesture-responder": "^2.1.0", + "react-intersection-observer": "^8.31.0", + "react-spring": "^8.0.27", + "recoil": "^0.1.2", + "remark": "^13.0.0", + "remark-custom-blocks": "^2.5.0", + "remark-emoji": "2.1.0", + "remark-external-links": "^8.0.0", + "remark-footnotes": "^3.0.0", + "remark-frontmatter": "^3.0.0", + "remark-heading-id": "^1.0.0", + "remark-images": "2.0.0", + "remark-normalize-headings": "^2.0.0", + "remark-parse": "^9.0.0", + "remark-sectionize": "^1.1.1", + "remark-slug": "6.0.0", + "remark-squeeze-paragraphs": "^4.0.0", + "remark-unwrap-images": "2.0.0", + "remark-vscode": "^1.0.0-beta.2", + "strip-markdown": "^4.0.0", + "stylis": "^4.0.6", + "swr": "^0.3.2", + "turndown": "^7.0.0", + "typescript": "^4.0.2", + "unified-vscode": "^1.0.0-beta.2", + "unist-builder": "^2.0.3", + "unist-util-is": "^4.0.4", + "unist-util-select": "^3.0.3", + "unist-util-visit": "^2.0.3", + "use-events": "^1.4.2", + "use-is-in-viewport": "^1.0.9", + "yaml-loader": "^0.6.0" + }, + "devDependencies": { + "@babel/preset-react": "^7.12.7", + "@blockstack/eslint-config": "^1.0.5", + "@blockstack/prettier-config": "^0.0.6", + "@next/bundle-analyzer": "^10.0.3", + "@typescript-eslint/eslint-plugin": "^4.9.0", + "@typescript-eslint/parser": "^4.9.0", + "babel-plugin-styled-components": "^1.12.0", + "eslint": "^7.14.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-prettier": "^3.1.4", + "husky": "^4.2.5", + "next-transpile-modules": "^6.0.0", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "rimraf": "^3.0.2", + "tsconfig-paths-webpack-plugin": "^3.3.0" + }, + "private": true, + "scripts": { + "build": "yarn clean:build-files && next telemetry disable && NODE_ENV=production next build", + "build:analyze": "yarn clean:build-files && next telemetry disable && NODE_ENV=production ANALYZE=true next build", + "start": "next telemetry disable && NODE_ENV=production next start", + "clean:build-files": "rimraf .next && rimraf .cache", + "dev": "yarn clean:build-files && next dev", + "build:cli-reference": "blockstack-cli docs > src/_data/cli-reference.json", + "export": "next export", + "lint": "yarn lint:eslint && yarn lint:prettier", + "lint:eslint": "eslint \"src/**/*.{ts,tsx}\" -f unix", + "lint:fix": "eslint \"src/**/*.{ts,tsx}\" -f unix --fix", + "lint:prettier": "prettier --check \"src/**/*.{ts,tsx,md,mdx}\" *.js", + "lint:prettier:fix": "prettier --write \"src/**/*.{ts,tsx,md,mdx}\" *.js", + "typecheck": "tsc --noEmit", + "typecheck:watch": "npm run typecheck -- --watch" + }, + "resolutions": { + "preact": "^10.4.4", + "eslint": "^7.4.0", + "@emotion/cache": "^11.0.0-next.15", + "@emotion/core": "^11.0.0-next.10", + "@emotion/css": "^11.0.0-next.15", + "@emotion/react": "^11.0.0-next.15", + "@emotion/server": "^11.0.0-next.15" + }, + "husky": { + "hooks": { + "pre-commit": "yarn lint:prettier" + } + }, + "prettier": "@blockstack/prettier-config" +} diff --git a/docs/public/app-icon.png b/docs/public/app-icon.png new file mode 100644 index 0000000..85f3a33 Binary files /dev/null and b/docs/public/app-icon.png differ diff --git a/docs/public/favicon-dark.svg b/docs/public/favicon-dark.svg new file mode 100755 index 0000000..4ef1855 --- /dev/null +++ b/docs/public/favicon-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/public/favicon-light.svg b/docs/public/favicon-light.svg new file mode 100755 index 0000000..8f2ddea --- /dev/null +++ b/docs/public/favicon-light.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico new file mode 100644 index 0000000..ddeeb6c Binary files /dev/null and b/docs/public/favicon.ico differ diff --git a/docs/public/images/pages/colors.png b/docs/public/images/pages/colors.png new file mode 100644 index 0000000..ba7b242 Binary files /dev/null and b/docs/public/images/pages/colors.png differ diff --git a/docs/public/images/pages/getting-started.png b/docs/public/images/pages/getting-started.png new file mode 100644 index 0000000..ff00d8d Binary files /dev/null and b/docs/public/images/pages/getting-started.png differ diff --git a/docs/public/images/pages/patterns-principles.png b/docs/public/images/pages/patterns-principles.png new file mode 100644 index 0000000..c8eb8df Binary files /dev/null and b/docs/public/images/pages/patterns-principles.png differ diff --git a/docs/public/images/pages/system-props.png b/docs/public/images/pages/system-props.png new file mode 100644 index 0000000..52c36e0 Binary files /dev/null and b/docs/public/images/pages/system-props.png differ diff --git a/docs/public/images/pages/theme.png b/docs/public/images/pages/theme.png new file mode 100644 index 0000000..fc1861d Binary files /dev/null and b/docs/public/images/pages/theme.png differ diff --git a/docs/public/static/favicon.png b/docs/public/static/favicon.png new file mode 100644 index 0000000..73a8a6e Binary files /dev/null and b/docs/public/static/favicon.png differ diff --git a/docs/public/static/fonts/opensaucesans-medium-webfont.woff b/docs/public/static/fonts/opensaucesans-medium-webfont.woff new file mode 100644 index 0000000..474f2d8 Binary files /dev/null and b/docs/public/static/fonts/opensaucesans-medium-webfont.woff differ diff --git a/docs/public/static/fonts/opensaucesans-medium-webfont.woff2 b/docs/public/static/fonts/opensaucesans-medium-webfont.woff2 new file mode 100644 index 0000000..1ece014 Binary files /dev/null and b/docs/public/static/fonts/opensaucesans-medium-webfont.woff2 differ diff --git a/docs/public/static/fonts/opensaucesans-regular-webfont.woff b/docs/public/static/fonts/opensaucesans-regular-webfont.woff new file mode 100644 index 0000000..9e8c8de Binary files /dev/null and b/docs/public/static/fonts/opensaucesans-regular-webfont.woff differ diff --git a/docs/public/static/fonts/opensaucesans-regular-webfont.woff2 b/docs/public/static/fonts/opensaucesans-regular-webfont.woff2 new file mode 100644 index 0000000..4a54144 Binary files /dev/null and b/docs/public/static/fonts/opensaucesans-regular-webfont.woff2 differ diff --git a/docs/src/common/constants.ts b/docs/src/common/constants.ts new file mode 100644 index 0000000..98c47c2 --- /dev/null +++ b/docs/src/common/constants.ts @@ -0,0 +1,9 @@ +export const SIDEBAR_WIDTH = 208; +export const TOC_WIDTH = 208; +export const CONTENT_MAX_WIDTH = 1104; +export const PAGE_WIDTH = 1104; + +export const SHIKI_THEME = 'Material-Theme-Default'; +export const THEME_STORAGE_KEY = 'theme'; + +export const STATUS_CHECKER_URL = 'https://status.test-blockstack.com'; diff --git a/docs/src/common/data/mdx.ts b/docs/src/common/data/mdx.ts new file mode 100644 index 0000000..4f3fb6d --- /dev/null +++ b/docs/src/common/data/mdx.ts @@ -0,0 +1,23 @@ +import { Components } from '@components/mdx/mdx-components'; +import renderToString from 'next-mdx-remote/render-to-string'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { remarkPlugins } = require('../../../lib/remark-plugins'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { rehypePlugins } = require('../../../lib/rehype-plugins'); + +// eslint-disable-next-line @typescript-eslint/restrict-plus-operands +export const wrapValueInTicks = value => '`' + value.replace('`', '').replace('`', '') + '`'; + +const mdxOptions = { + remarkPlugins, + rehypePlugins, +}; + +const renderToStringOptions = { components: Components, mdxOptions }; + +export const convertRemoteDataToMDX = async (arr: any[], key: string) => + Promise.all(arr.map(entry => renderToString(entry[key], renderToStringOptions))); + +export const renderMdx = async (content: string): Promise => + renderToString(content, renderToStringOptions) as Promise; diff --git a/docs/src/common/hooks/use-active-heading.tsx b/docs/src/common/hooks/use-active-heading.tsx new file mode 100644 index 0000000..233668a --- /dev/null +++ b/docs/src/common/hooks/use-active-heading.tsx @@ -0,0 +1,54 @@ +import React, { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { useAppState } from '@common/hooks/use-app-state'; + +interface ActiveHeadingReturn { + isActive: boolean; + doChangeActiveSlug: (value: string) => void; + location: string; + slugInView?: string; + doChangeSlugInView: (value: string) => void; +} + +const getHash = (url: string) => url?.includes('#') && url.split('#')[1]; + +export const useWatchActiveHeadingChange = () => { + const router = useRouter(); + const asPath = router && router.asPath; + const { activeSlug, doChangeActiveSlug } = useAppState(); + const urlHash = getHash(asPath); + + const handleRouteChange = url => { + if (url) { + const hash = getHash(url); + if (hash) doChangeActiveSlug(hash); + } + }; + + useEffect(() => { + if ((urlHash && !activeSlug) || (urlHash && urlHash !== activeSlug)) { + doChangeActiveSlug(urlHash); + } + router.events.on('hashChangeStart', handleRouteChange); + router.events.on('routeChangeStart', handleRouteChange); + return () => { + router.events.off('hashChangeStart', handleRouteChange); + router.events.off('routeChangeStart', handleRouteChange); + }; + }, []); +}; + +export const useActiveHeading = (_slug: string): ActiveHeadingReturn => { + const { activeSlug, slugInView, doChangeActiveSlug, doChangeSlugInView } = useAppState(); + const location = typeof window !== 'undefined' && window.location.href; + + const isActive = _slug === activeSlug; + + return { + isActive, + doChangeActiveSlug, + location, + slugInView, + doChangeSlugInView, + }; +}; diff --git a/docs/src/common/hooks/use-app-state.tsx b/docs/src/common/hooks/use-app-state.tsx new file mode 100644 index 0000000..ad3c3ff --- /dev/null +++ b/docs/src/common/hooks/use-app-state.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { AppStateContext } from '@components/app-state/context'; +import { State } from '@components/app-state/types'; + +interface UseAppStateReturn extends State { + doChangeActiveSlug: (activeSlug: string) => void; + doChangeSlugInView: (slugInView: string) => void; +} + +export const useAppState = (): UseAppStateReturn => { + const { setState, ...rest } = React.useContext(AppStateContext); + + function setter(key: string) { + return (value: T) => + setState((state: State) => ({ + ...state, + [key]: value, + })); + } + + const doChangeActiveSlug = setter('activeSlug'); + + const doChangeSlugInView = setter('slugInView'); + + return { + ...rest, + doChangeActiveSlug, + doChangeSlugInView, + setState, + }; +}; diff --git a/docs/src/common/hooks/use-color-mode.ts b/docs/src/common/hooks/use-color-mode.ts new file mode 100644 index 0000000..5f324f4 --- /dev/null +++ b/docs/src/common/hooks/use-color-mode.ts @@ -0,0 +1,69 @@ +import React from 'react'; +import { useMediaQuery } from '@common/hooks/use-media-query'; +import { THEME_STORAGE_KEY } from '@common/constants'; + +export const useColorMode = (): [string, () => void, (mode: 'dark' | 'light') => void] => { + const [darkmode] = useMediaQuery('(prefers-color-scheme: dark)'); + const [lightmode] = useMediaQuery('(prefers-color-scheme: light)'); + const setMode = typeof localStorage !== 'undefined' && localStorage.getItem(THEME_STORAGE_KEY); + + const [colorMode, setColorMode] = React.useState<'dark' | 'light' | undefined>(undefined); + + const setHtmlBackgroundColor = React.useCallback(() => { + document.documentElement.style.background = getComputedStyle( + document.documentElement + ).getPropertyValue('--colors-bg'); + }, []); + + const setDarkMode = React.useCallback(() => { + localStorage.setItem(THEME_STORAGE_KEY, 'dark'); + setColorMode('dark'); + document.documentElement.classList.add('dark'); + document.documentElement.classList.remove('light'); + setHtmlBackgroundColor(); + }, []); + + const setLightMode = React.useCallback(() => { + localStorage.setItem(THEME_STORAGE_KEY, 'light'); + setColorMode('light'); + document.documentElement.classList.add('light'); + document.documentElement.classList.remove('dark'); + setHtmlBackgroundColor(); + }, []); + + React.useEffect(() => { + if (setMode) { + if (setMode === 'dark') { + setColorMode('dark'); + } + if (setMode === 'light') { + setColorMode('light'); + } + } else { + if (darkmode) { + setDarkMode(); + } + if (lightmode) { + setLightMode(); + } + } + }, [setMode, lightmode, darkmode]); + + const toggleColorMode = React.useCallback(() => { + if (typeof document !== 'undefined') { + if (setMode) { + if (setMode === 'light') { + setDarkMode(); + } else { + setLightMode(); + } + } else if (darkmode) { + setLightMode(); + } else { + setDarkMode(); + } + } + }, [darkmode, lightmode, setMode, colorMode]); + + return [colorMode, toggleColorMode, setColorMode]; +}; diff --git a/docs/src/common/hooks/use-fathom.ts b/docs/src/common/hooks/use-fathom.ts new file mode 100644 index 0000000..51dba2d --- /dev/null +++ b/docs/src/common/hooks/use-fathom.ts @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; +import { useRouter } from 'next/router'; +import * as Fathom from 'fathom-client'; + +export const useFathom = () => { + const router = useRouter(); + + useEffect(() => { + Fathom.load(process.env.FATHOM_ID, { + includedDomains: ['docs.blockstack.org'], + }); + + function onRouteChangeComplete() { + Fathom.trackPageview(); + } + + router.events.on('routeChangeComplete', onRouteChangeComplete); + + return () => { + router.events.off('routeChangeComplete', onRouteChangeComplete); + }; + }, []); +}; diff --git a/docs/src/common/hooks/use-favicon.ts b/docs/src/common/hooks/use-favicon.ts new file mode 100644 index 0000000..a8d733f --- /dev/null +++ b/docs/src/common/hooks/use-favicon.ts @@ -0,0 +1,7 @@ +import { useColorMode } from '@common/hooks/use-color-mode'; + +export const useFaviconName = () => { + const [mode] = useColorMode(); + const darkmode = mode === 'dark'; + return `favicon-${darkmode ? 'light' : 'dark'}.svg`; +}; diff --git a/docs/src/common/hooks/use-headroom.ts b/docs/src/common/hooks/use-headroom.ts new file mode 100644 index 0000000..7196617 --- /dev/null +++ b/docs/src/common/hooks/use-headroom.ts @@ -0,0 +1,43 @@ +import { Ref, useEffect, useState } from 'react'; +import debounce from 'lodash.debounce'; +import { useRect } from '@reach/rect'; +import { useScroll } from '@common/hooks/use-scroll'; + +export const useHeadroom = (target: Ref, { useStyle = true, wait = 0 } = {}) => { + const styleInserted = false; + const rect = useRect(target as any); + const { scrollY, scrollDirection } = useScroll(); + + if (typeof document !== 'undefined') { + const header = document.querySelector('.headroom'); + + const listener = debounce(() => { + header?.classList?.toggle('unpinned', window.pageYOffset >= rect?.height); + }, 50); + + useEffect(() => { + if ( + scrollDirection === 'down' && + header.classList.contains('unpinned') && + header.classList.contains('hidden') + ) { + header.classList.remove('hidden'); + } + if ( + scrollDirection === 'up' && + header.classList.contains('unpinned') && + !header.classList.contains('hidden') + ) { + header.classList.add('hidden'); + } + }, [scrollDirection]); + + useEffect(() => { + if (rect) { + document.addEventListener('scroll', listener, { passive: true }); + + return () => document.removeEventListener('scroll', listener); + } + }, [rect]); + } +}; diff --git a/docs/src/common/hooks/use-lock-body-scroll.tsx b/docs/src/common/hooks/use-lock-body-scroll.tsx new file mode 100644 index 0000000..3055599 --- /dev/null +++ b/docs/src/common/hooks/use-lock-body-scroll.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { useSafeLayoutEffect } from '@stacks/ui'; + +export const useLockBodyScroll = (lock: boolean) => { + useSafeLayoutEffect(() => { + // Get original body overflow + const originalStyle = window.getComputedStyle(document.body).overflow; + + if (lock) { + if (document.body.style.overflow !== 'hidden') { + document.body.style.overflow = 'hidden'; + } + } else { + if (document.body.style.overflow !== originalStyle) { + document.body.style.overflow = originalStyle; + } + } + + // Re-enable scrolling when component unmounts + return () => { + document.body.style.overflow = originalStyle; + }; + }, [lock]); // Empty array ensures effect is only run on mount and unmount +}; diff --git a/docs/src/common/hooks/use-media-query.tsx b/docs/src/common/hooks/use-media-query.tsx new file mode 100644 index 0000000..e20b99b --- /dev/null +++ b/docs/src/common/hooks/use-media-query.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { useSafeLayoutEffect } from '@stacks/ui'; + +const isBrowser = typeof window !== 'undefined'; + +const isSupported = (api: string) => isBrowser && api in window; + +/** + * React hook that tracks state of a CSS media query + * + * @param query the media query to match + */ +export function useMediaQuery(query: string) { + const [matches, setMatches] = React.useState(() => { + if (!isSupported('matchMedia')) return false; + return window.matchMedia(query).matches; + }); + + useSafeLayoutEffect(() => { + if (!isSupported('matchMedia')) return; + + const mediaQueryList = window.matchMedia(query); + const listener = () => setMatches(mediaQueryList.matches); + + mediaQueryList.addListener(listener); + + listener(); + + return () => { + mediaQueryList.removeListener(listener); + }; + }, [query]); + + return [matches, setMatches] as const; +} diff --git a/docs/src/common/hooks/use-mobile-menu.tsx b/docs/src/common/hooks/use-mobile-menu.tsx new file mode 100644 index 0000000..7b86452 --- /dev/null +++ b/docs/src/common/hooks/use-mobile-menu.tsx @@ -0,0 +1,27 @@ +import React, { useCallback } from 'react'; +import { useAppState } from '@common/hooks/use-app-state'; +import { State } from '@components/app-state/types'; + +export const useMobileMenuState = () => { + const { setState, mobileMenu: isOpen } = useAppState(); + + const handleToggle = useCallback( + () => setState((s: State) => ({ ...s, mobileMenu: !s.mobileMenu })), + [isOpen] + ); + const handleClose = useCallback( + () => isOpen && setState((s: State) => ({ ...s, mobileMenu: false })), + [isOpen] + ); + const handleOpen = useCallback( + () => !isOpen && setState((s: State) => ({ ...s, mobileMenu: true })), + [isOpen] + ); + return { + isOpen, + setOpen: setState, + handleToggle, + handleClose, + handleOpen, + }; +}; diff --git a/docs/src/common/hooks/use-on-screen.tsx b/docs/src/common/hooks/use-on-screen.tsx new file mode 100644 index 0000000..bbe19fc --- /dev/null +++ b/docs/src/common/hooks/use-on-screen.tsx @@ -0,0 +1,26 @@ +import { useState, useEffect } from 'react'; + +export function useOnScreen(ref, rootMargin = '0px') { + // State and setter for storing whether element is visible + const [isIntersecting, setIntersecting] = useState(false); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + // Update our state when observer callback fires + setIntersecting(entry.isIntersecting); + }, + { + rootMargin, + } + ); + if (ref.current) { + observer.observe(ref.current); + } + return () => { + observer.unobserve(ref.current); + }; + }, []); // Empty array ensures that effect is only run on mount and unmount + + return isIntersecting; +} diff --git a/docs/src/common/hooks/use-scroll.tsx b/docs/src/common/hooks/use-scroll.tsx new file mode 100644 index 0000000..f60fa28 --- /dev/null +++ b/docs/src/common/hooks/use-scroll.tsx @@ -0,0 +1,65 @@ +/** + * useScroll React custom hook + * Usage: + * const { scrollX, scrollY, scrollDirection } = useScroll();7 + * Original Source: https://gist.github.com/joshuacerbito/ea318a6a7ca4336e9fadb9ae5bbb8f4 + */ +import { useState, useEffect } from 'react'; + +type SSRRect = { + bottom: number; + height: number; + left: number; + right: number; + top: number; + width: number; + x: number; + y: number; +}; +const EmptySSRRect: SSRRect = { + bottom: 0, + height: 0, + left: 0, + right: 0, + top: 0, + width: 0, + x: 0, + y: 0, +}; + +export const useScroll = () => { + const [lastScrollTop, setLastScrollTop] = useState(0); + const [bodyOffset, setBodyOffset] = useState( + typeof window === 'undefined' || !window.document + ? EmptySSRRect + : document.body.getBoundingClientRect() + ); + const [scrollY, setScrollY] = useState(bodyOffset.top); + const [scrollX, setScrollX] = useState(bodyOffset.left); + const [scrollDirection, setScrollDirection] = useState<'down' | 'up' | undefined>(); + + const listener = () => { + setBodyOffset( + typeof window === 'undefined' || !window.document + ? EmptySSRRect + : document.body.getBoundingClientRect() + ); + setScrollY(-bodyOffset.top); + setScrollX(bodyOffset.left); + setScrollDirection(lastScrollTop > -bodyOffset.top ? 'down' : 'up'); + setLastScrollTop(-bodyOffset.top); + }; + + useEffect(() => { + window.addEventListener('scroll', listener); + return () => { + window.removeEventListener('scroll', listener); + }; + }); + + return { + scrollY, + scrollX, + scrollDirection, + }; +}; diff --git a/docs/src/common/hooks/use-side-nav.ts b/docs/src/common/hooks/use-side-nav.ts new file mode 100644 index 0000000..f6496a8 --- /dev/null +++ b/docs/src/common/hooks/use-side-nav.ts @@ -0,0 +1,95 @@ +import React from 'react'; +// @ts-ignore +import nav from '@common/navigation.yaml'; +import { useRouter } from 'next/router'; +import { useMobileMenuState } from '@common/hooks/use-mobile-menu'; +import { useRecoilState } from 'recoil'; +import { sideNavState, SideNavState } from '@common/store'; + +export const useSideNavState = (): [state: SideNavState, setState: any] => { + const [selected, setSelected] = useRecoilState(sideNavState); + + return [selected, setSelected]; +}; +const useAutoUpdateSideNav = () => { + const [selected, setSelected] = useSideNavState(); + const router = useRouter(); + React.useEffect(() => { + let currentSection; + + if (router.pathname === '/') { + currentSection = { + items: nav.sections, + type: 'default', + }; + } else { + nav.sections.forEach(section => { + section.pages.forEach(page => { + if (page.pages) { + const pagesFound = page.pages.find(_page => { + return router.pathname.endsWith(`${page.path}${_page.path}`); + }); + const sectionsFound = page?.sections?.find(_section => { + return _section.pages.find(_page => { + return router.pathname.endsWith(`${page.path}${_page.path}`); + }); + }); + if (pagesFound || sectionsFound) { + currentSection = { + type: 'page', + items: page, + }; + } + } else if (!currentSection && router.pathname.endsWith(page.path)) { + currentSection = { + items: nav.sections, + type: 'default', + }; + } + }); + }); + } + + if (currentSection?.items && selected.items !== currentSection.items) { + setSelected(currentSection); + } + }, [router.pathname]); +}; + +export const useSideNav = () => { + const [{ items, type, expanded, selected }, setSelected] = useSideNavState(); + const { handleClose } = useMobileMenuState(); + + const handleUpdateExpanded = (expanded: string) => setSelected(s => ({ ...s, expanded })); + const handleUpdateSelected = (selected: any | undefined) => + setSelected(s => ({ ...s, selected })); + const handleUpdateType = (type: 'page' | 'default') => setSelected(s => ({ ...s, type })); + + const handleClick = (_selected: any) => { + if (_selected.pages) { + setSelected({ + type: 'page', + items: selected, + }); + } + handleClose(); + }; + + const handleBack = () => + setSelected({ + type: 'default', + items: nav.sections, + }); + + return { + items, + type, + expanded, + selected, + handleClick, + handleBack, + handleUpdateExpanded, + handleUpdateSelected, + handleUpdateType, + }; +}; diff --git a/docs/src/common/hooks/use-touchable.tsx b/docs/src/common/hooks/use-touchable.tsx new file mode 100644 index 0000000..89891d1 --- /dev/null +++ b/docs/src/common/hooks/use-touchable.tsx @@ -0,0 +1,14 @@ +import { useHover, useActive } from 'use-events'; + +export const useTouchable = (options?: any) => { + const [hover, hoverBind] = useHover(); + const [active, activeBind] = useActive(); + return { + bind: { + ...hoverBind, + ...activeBind, + }, + hover, + active, + }; +}; diff --git a/docs/src/common/navigation.yaml b/docs/src/common/navigation.yaml new file mode 100644 index 0000000..2cbdf74 --- /dev/null +++ b/docs/src/common/navigation.yaml @@ -0,0 +1,31 @@ +sections: + - pages: + - path: /installation + - path: /patterns-and-principles + - path: /responsive-styles + - path: /contributing + - title: Core + slug: core + pages: + - path: /system-props + - path: /theme + - path: /colors + - path: /space + - path: /typography + - title: Components + slug: components + pages: + - path: /box + - path: /grid + - path: /flex + - path: /text + - path: /stack + - path: /button + - path: /codeblock + - path: /icons + - path: /color-modes + - path: /css-reset + - title: Hooks + slug: hooks + pages: + - path: /use-theme diff --git a/docs/src/common/routes/get-routes.js b/docs/src/common/routes/get-routes.js new file mode 100644 index 0000000..aa42edb --- /dev/null +++ b/docs/src/common/routes/get-routes.js @@ -0,0 +1,83 @@ +/** + * Routes + * + * This file contains our paths for all of our markdown files and is pre evaluated at runtime to get the content + * from all the markdown: front matter and extracts all the headings from the document. + * + * This data is used to dynamically generate the sidenav. + * + */ +const fm = require('front-matter'); +const fs = require('fs-extra'); +const path = require('path'); + +const slugify = string => + string + .toLowerCase() + .replace(/\s+/g, '-') // Replace spaces with - + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '-') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text + +const YAML = require('yaml'); +const yamlFile = fs.readFileSync(path.resolve(__dirname, '../navigation.yaml'), 'utf8'); +const navigation = YAML.parse(yamlFile); + +const getFlatMap = navigation => { + return navigation.sections.flatMap(section => + section.pages.flatMap(page => { + if (page.pages) { + let sectionPages = []; + if (page.sections) { + sectionPages = page.sections.flatMap(_section => { + return _section.pages.flatMap( + sectionPage => sectionPage && sectionPage.path && `${page.path}${sectionPage.path}` + ); + }); + } + const pages = page.pages.flatMap(_page => { + if (_page.pages) { + return _page.pages.flatMap(p => `${page.path}${_page.path}${p.path}`); + } else { + return _page.path && `${page.path}${_page.path}`; + } + }); + return [...pages, ...sectionPages]; + } else { + return `${section.slug ? `/${slugify(section.slug)}` : ''}${page.path}`; + } + }) + ); +}; + +const allRoutes = getFlatMap(navigation).filter(route => route); + +const getHeadings = mdContent => { + const regex = /(#+)(.*)/gm; + const found = mdContent.match(regex); + return found && found.length + ? found.map(f => f && f.split('# ')[1]).filter(f => typeof f !== 'undefined') + : null; +}; + +const routes = allRoutes.map(route => { + try { + const fileContent = fs.readFileSync( + path.join('./src/pages', (route === '/' ? 'index' : route) + '.mdx'), + 'utf8' + ); + const data = fm(fileContent); + const headings = getHeadings(data.body); + return { + path: route, + ...data.attributes, + headings, + }; + } catch (e) { + console.error('ROUTES ERROR'); + console.warn(e); + } +}); + +module.exports = routes; diff --git a/docs/src/common/routes/index.ts b/docs/src/common/routes/index.ts new file mode 100644 index 0000000..9bcb6f6 --- /dev/null +++ b/docs/src/common/routes/index.ts @@ -0,0 +1,5 @@ +import preval from 'preval.macro'; + +const routes = preval`module.exports = require('./get-routes')`; + +export default routes; diff --git a/docs/src/common/store/index.ts b/docs/src/common/store/index.ts new file mode 100644 index 0000000..40c438a --- /dev/null +++ b/docs/src/common/store/index.ts @@ -0,0 +1,19 @@ +import { atom } from 'recoil'; +// @ts-ignore +import nav from '@common/navigation.yaml'; + +export interface SideNavState { + type: 'default' | 'page'; + items: any; + selected: any; + expanded: string; +} +export const sideNavState = atom({ + key: 'app.sidenav', + default: { + type: 'default', + items: nav.sections, + selected: undefined, + expanded: nav.sections[1].title, + }, +}); diff --git a/docs/src/common/utils/hover-enabled.ts b/docs/src/common/utils/hover-enabled.ts new file mode 100644 index 0000000..efde154 --- /dev/null +++ b/docs/src/common/utils/hover-enabled.ts @@ -0,0 +1,43 @@ +const canUseDOM = !!( + typeof window !== 'undefined' && + window.document && + window.document.createElement +); + +let isEnabled = false; + +const HOVER_THRESHOLD_MS = 1000; +let lastTouchTimestamp = 0; + +function enableHover() { + if (isEnabled || Date.now() - lastTouchTimestamp < HOVER_THRESHOLD_MS) { + return; + } + isEnabled = true; +} + +function disableHover() { + lastTouchTimestamp = Date.now(); + if (isEnabled) { + isEnabled = false; + } +} + +if (canUseDOM) { + document.addEventListener('touchstart', disableHover, { + capture: true, + passive: true, + }); + document.addEventListener('touchmove', disableHover, { + capture: true, + passive: true, + }); + document.addEventListener('mousemove', enableHover, { + capture: true, + passive: true, + }); +} + +export function isHoverEnabled() { + return isEnabled; +} diff --git a/docs/src/common/utils/index.ts b/docs/src/common/utils/index.ts new file mode 100644 index 0000000..a613413 --- /dev/null +++ b/docs/src/common/utils/index.ts @@ -0,0 +1,125 @@ +import { Children, isValidElement, ReactNode, ReactElement, ReactText } from 'react'; +import { Property } from 'csstype'; +import { color } from '@stacks/ui'; +import { ColorsStringLiteral } from '@stacks/ui'; + +const camelToKebab = (string: string) => + string + .toString() + .replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2') + .toLowerCase(); + +export const slugify = (string: string): string => + string + .toLowerCase() + .replace(/\s+/g, '-') // Replace spaces with - + .replace(/[^\w\-]+/g, '') // Remove all non-word chars + .replace(/\-\-+/g, '-') // Replace multiple - with single - + .replace(/^-+/, '') // Trim - from start of text + .replace(/-+$/, ''); // Trim - from end of text + +export const capitalize = ([s, ...tring]: string): string => [s.toUpperCase(), ...tring].join(''); +export const convertToTitle = (path: string) => + !path ? null : path === '/' ? 'Home' : capitalize(path.replace('/', '').replace(/-/g, ' ')); +export const border = ( + width = 1, + style: Property.BorderStyle = 'solid', + _color: ColorsStringLiteral = 'border' +): string => `${width}px ${style} ${color(_color)}`; + +// https://github.com/fernandopasik/react-children-utilities/blob/master/src/lib/hasChildren.ts +const hasChildren = (element: ReactNode): element is ReactElement<{ children: ReactNode[] }> => + isValidElement<{ children?: ReactNode[] }>(element) && Boolean(element.props.children); + +// https://github.com/fernandopasik/react-children-utilities/blob/master/src/lib/onlyText.ts + +export const childToString = (child?: ReactText | boolean | unknown | null): string => { + if (typeof child === 'undefined' || child === null || typeof child === 'boolean') { + return ''; + } + + if (JSON.stringify(child) === '{}') { + return ''; + } + + return (child as string | number).toString(); +}; + +export const onlyText = (children: ReactNode): string => { + if (!(children instanceof Array) && !isValidElement(children)) { + return childToString(children); + } + + return Children.toArray(children).reduce((text: string, child: ReactNode): string => { + let newText = ''; + + if (isValidElement(child) && hasChildren(child)) { + newText = onlyText(child.props.children) + '\n'; + } else if (isValidElement(child) && !hasChildren(child)) { + newText = ''; + } else { + newText = childToString(child); + } + + return text.concat(newText); + }, '') as string; +}; + +const getTitleFromHeading = (headings?: any[]) => + headings?.length + ? typeof headings[0] === 'string' + ? headings[0] + : headings[0].content + : undefined; + +export const getTitle = ({ title, headings }: { title?: string; headings?: any[] }): string => + title || getTitleFromHeading(headings); + +export const transition = (timing = '0.2s', properties = 'all') => + `${properties} ${timing} cubic-bezier(0.23, 1, 0.32, 1)`; + +export const getCategory = (pathname: string) => { + const arr = pathname.split('/'); + if (arr.length > 1) { + return arr[1]; + } + return undefined; +}; + +export const getSlug = (asPath: string) => { + if (asPath.includes('#')) { + const slug = asPath.split('#')[1]; + return slug; + } + return; +}; + +interface CancelablePromise { + promise: Promise; + cancel: () => void; +} + +/** Make a Promise "cancelable". + * + * Rejects with {isCanceled: true} if canceled. + * + * The way this works is by wrapping it with internal hasCanceled_ state + * and checking it before resolving. + */ +export const makeCancelable = (promise: Promise): CancelablePromise => { + let hasCanceled_ = false; + + const wrappedPromise = new Promise((resolve, reject) => { + void promise.then((val: any) => (hasCanceled_ ? reject({ isCanceled: true }) : resolve(val))); + void promise.catch((error: any) => + hasCanceled_ ? reject({ isCanceled: true }) : reject(error) + ); + }); + + return { + promise: wrappedPromise, + cancel() { + hasCanceled_ = true; + }, + }; +}; diff --git a/docs/src/components/app-state/context.ts b/docs/src/components/app-state/context.ts new file mode 100644 index 0000000..37a8b0a --- /dev/null +++ b/docs/src/components/app-state/context.ts @@ -0,0 +1,11 @@ +import React from 'react'; +import { State } from '@components/app-state/types'; +import routes from '@common/routes'; + +export const initialState: State = { + mobileMenu: false, + activeSlug: '', + setState: (value: any) => null, + routes, +}; +export const AppStateContext = React.createContext(initialState); diff --git a/docs/src/components/app-state/index.tsx b/docs/src/components/app-state/index.tsx new file mode 100644 index 0000000..b66854b --- /dev/null +++ b/docs/src/components/app-state/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { AppStateContext, initialState } from '@components/app-state/context'; + +const AppStateProvider = ({ ...props }: any) => { + const [state, setState] = React.useState(initialState); + + return ( + + ); +}; + +export { AppStateProvider, AppStateContext }; diff --git a/docs/src/components/app-state/types.ts b/docs/src/components/app-state/types.ts new file mode 100644 index 0000000..e3391fb --- /dev/null +++ b/docs/src/components/app-state/types.ts @@ -0,0 +1,7 @@ +export interface State { + mobileMenu: boolean; + activeSlug: string; + slugInView?: string; + setState: (value: any) => void; + routes: any; +} diff --git a/docs/src/components/app-wrapper.tsx b/docs/src/components/app-wrapper.tsx new file mode 100644 index 0000000..57e9638 --- /dev/null +++ b/docs/src/components/app-wrapper.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { ColorModeProvider } from '@stacks/ui'; +import { AppStateProvider } from '@components/app-state'; +import { ProgressBar } from '@components/progress-bar'; +import { BaseLayout } from '@components/layouts/base-layout'; +import { Meta } from '@components/meta-head'; +import { useFathom } from '@common/hooks/use-fathom'; + +export const AppWrapper: React.FC = ({ children, isHome }) => { + useFathom(); + return ( + + + + + {children} + + + ); +}; diff --git a/docs/src/components/back-button.tsx b/docs/src/components/back-button.tsx new file mode 100644 index 0000000..1bb0f93 --- /dev/null +++ b/docs/src/components/back-button.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Flex, Box, color, space } from '@stacks/ui'; +import ArrowLeftIcon from 'mdi-react/ArrowLeftIcon'; +import { Text } from '@components/typography'; +import Link from 'next/link'; + +const Wrapper = ({ href, children }) => + href ? ( + + {children} + + ) : ( + children + ); + +export const BackButton = ({ href, ...rest }) => ( + + + + + + Back + + +); diff --git a/docs/src/components/color-mode-button.tsx b/docs/src/components/color-mode-button.tsx new file mode 100644 index 0000000..9619bdf --- /dev/null +++ b/docs/src/components/color-mode-button.tsx @@ -0,0 +1,20 @@ +import React, { forwardRef, memo, Ref } from 'react'; +import { BoxProps, IconButton, useColorMode } from '@stacks/ui'; +import { IconSun, IconSunOff } from '@tabler/icons/icons-react/dist/index.esm'; + +export const ColorModeButton = memo( + forwardRef((props: BoxProps, ref: Ref) => { + const { colorMode, toggleColorMode } = useColorMode(); + const Icon = colorMode === 'light' ? IconSun : IconSunOff; + return ( + + ); + }) +); diff --git a/docs/src/components/common.tsx b/docs/src/components/common.tsx new file mode 100644 index 0000000..3cc75ea --- /dev/null +++ b/docs/src/components/common.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { + Box, + Grid, + Flex, + BoxProps, + transition, + space, + GridProps, + color, + FlexProps, +} from '@stacks/ui'; +import { CONTENT_MAX_WIDTH } from '@common/constants'; + +export const CircleIcon: React.FC< + FlexProps & { icon: React.FC; hover?: boolean; dark?: boolean } +> = ({ size = '72px', icon: Icon, hover, dark, ...rest }) => ( + + + + + +); + +export const Section: React.FC = props => ( + +); + +export const SectionWrapper: React.FC = props => ( + +); diff --git a/docs/src/components/content-wrapper.tsx b/docs/src/components/content-wrapper.tsx new file mode 100644 index 0000000..a091a44 --- /dev/null +++ b/docs/src/components/content-wrapper.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Flex, FlexProps, space } from '@stacks/ui'; +import { ForwardRefExoticComponentWithAs, forwardRefWithAs } from '@stacks/ui-core'; + +const ContentWrapper: ForwardRefExoticComponentWithAs = forwardRefWithAs< + FlexProps, + 'div' +>((props, ref) => ( + +)); + +export { ContentWrapper }; diff --git a/docs/src/components/custom-blocks/page-reference.tsx b/docs/src/components/custom-blocks/page-reference.tsx new file mode 100644 index 0000000..196023e --- /dev/null +++ b/docs/src/components/custom-blocks/page-reference.tsx @@ -0,0 +1,303 @@ +import React from 'react'; +import { Box, BoxProps, color, Flex, Grid, space } from '@stacks/ui'; +import { BlockstackLogo } from '@components/icons/blockstack-logo'; +import { StackIcon } from '@components/icons/stack'; +import { SitemapIcon } from '@components/icons/sitemap'; +import { border, onlyText, transition } from '@common/utils'; +import { useTouchable } from '@common/hooks/use-touchable'; +import { Text } from '@components/typography'; +import Link from 'next/link'; +import { useAppState } from '@common/hooks/use-app-state'; +import Image from 'next/image'; +import { getCapsizeStyles, getHeadingStyles } from '@components/mdx/typography'; + +// const Image = ({ +// src, +// isHovered, +// size, +// alt, +// ...rest +// }: BoxProps & { src?: string; isHovered?: boolean; alt?: string }) => ( +// +// {alt} +// +// ); + +const Title: React.FC = ({ children, ...props }) => ( + + {children} + +); + +const Description = ({ children, ...props }) => ( + + {children} + +); + +const FloatingLink = ({ href, contents, ...props }: any) => ( + + + {contents} + + +); +const InlineCard = ({ page }) => { + const { hover, active, bind } = useTouchable({ + behavior: 'link', + }); + return ( + + + {/**/} + + + + + {page.title || page.headings[0]} + + {page.tags?.length ? ( + + {page.tags.map((tag, key) => ( + + {tag} + + ))} + + ) : null} + + {page.description} + + + + ); +}; + +const GridCardImage: React.FC< + BoxProps & { isHovered?: boolean; src?: string; alt?: string } +> = React.memo(({ isHovered, src, alt, ...props }) => ( + + + + + +)); + +const GridItemDetails: React.FC = React.memo( + ({ isHovered, page, ...props }) => ( + <> + + + {page.title || page.headings[0]} + + {page.description} + + + + ) +); + +const GridCard: React.FC = React.memo(({ page, ...rest }) => { + const { hover, active, bind } = useTouchable({ + behavior: 'link', + }); + return ( + + + + + ); +}); + +const getIcon = (icon: string) => { + switch (icon) { + case 'BlockstackIcon': + return (p: BoxProps) => ; + case 'StacksIcon': + return (p: BoxProps) => ( + + + + ); + case 'TestnetIcon': + return (p: BoxProps) => ( + + + + ); + default: + return (p: BoxProps) => ; + } +}; +const GridSmallItem: React.FC = ({ page, ...rest }) => { + const { hover, active, bind } = useTouchable({ + behavior: 'link', + }); + const Icon = getIcon(page.icon); + return ( + + {page.icon ? : null} + + + ); +}; + +const getComponent = (type: 'default' | 'inline' | 'grid' | 'grid-small') => { + switch (type) { + case 'inline': + return InlineCard; + case 'grid': + return GridCard; + case 'grid-small': + return GridSmallItem; + default: + return InlineCard; + } +}; + +export const PageReference: React.FC = React.memo(({ children, ...rest }) => { + const content = onlyText(children).trim(); + const [variant, _paths] = content.includes('\n') ? content.split('\n') : ['default', content]; + const paths = _paths.includes(', ') ? _paths.split(', ') : [_paths]; + const { routes } = useAppState(); + + if (!routes) return null; + + const pages = paths.map(path => routes?.find(route => route.path === path)).filter(page => page); + + const Comp = getComponent(variant as any); + return ( + + + {pages.map((page, key) => ( + + ))} + + + ); +}); diff --git a/docs/src/components/example.tsx b/docs/src/components/example.tsx new file mode 100644 index 0000000..29e0899 --- /dev/null +++ b/docs/src/components/example.tsx @@ -0,0 +1,196 @@ +import React from 'react'; +import { + BlockstackIcon, + Box, + BoxProps, + ChevronIcon, + CodeBlock as BaseCodeBlock, + color, + Flex, + FlexProps, + space, + Stack, + themeColor, +} from '@stacks/ui'; +import Prism from 'prismjs/components/prism-core'; +// import 'prismjs/components/prism-jsx'; +import { Caption, Text } from '@components/typography'; + +import { border } from '@common/utils'; + +export const SimpleCodeBlock = ({ editorCode, language, ...rest }) => ( + +); + +const Circle: React.FC = ({ size = '72px', ...rest }) => ( + +); + +const Avatar = ({ ...rest }) => ; + +const Progress = ({ amount, ...rest }) => ( + + + +); + +const AppItem: React.FC = ({ + name, + usage, + ...rest +}) => ( + + + + {name} + + + {usage} + + + +); + +const ListItem: React.FC = props => ( + +); + +const Title: React.FC = props => ( + +); + +export const ExampleComponent: React.FC = props => ( + + + Data storage + + + + Connected to + + + + My Gaia Hub + + + + + + + App data + + + + + + + +); + +export const exampleCode = ` + + + Data storage + + + + Connected to + + App data + + + + + + +`; + +export const gaiaHubUsage = ` + + + + My Gaia Hub + + + + + +`; + +export const appItem = ` + + + + {name} + + + {usage} + + +`; diff --git a/docs/src/components/feedback.tsx b/docs/src/components/feedback.tsx new file mode 100644 index 0000000..d7eb77c --- /dev/null +++ b/docs/src/components/feedback.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { + Box, + Button, + BoxProps, + color, + Flex, + space, + Stack, + transition, + SlideFade, +} from '@stacks/ui'; +import { Text } from '@components/typography'; +import { Link } from '@components/mdx'; +import { SadIcon, NeutralIcon, HappyIcon } from '@components/icons/feedback'; +import { useTouchable } from '@common/hooks/use-touchable'; +import { border } from '@common/utils'; +import { useRouter } from 'next/router'; +import { getHeadingStyles } from '@components/mdx/typography'; +import { css } from '@stacks/ui-core'; +import { StatusCheck } from '@components/status-check'; +import { useColorMode } from '@common/hooks/use-color-mode'; + +const Icon: React.FC }> = ({ icon: IconComponent, ...props }) => { + const { bind, hover, active } = useTouchable(); + const isHovered = hover || active; + const [mode] = useColorMode(); + return ( + + + + ); +}; + +const FeedbackCard = ({ show, onClose }) => { + return ( + + {styles => ( + + + + + + + Dismiss + + + + + + )} + + ); +}; + +export const FeedbackSection: React.FC = props => { + const { pathname } = useRouter(); + const [showButton, setShowButton] = React.useState(false); + const handleShow = () => { + setShowButton(!showButton); + }; + const editPage = pathname === '/' ? '/index' : pathname; + return ( + + + + Was this page helpful? + + handleShow()} icon={SadIcon} /> + handleShow()} icon={NeutralIcon} /> + handleShow()} icon={HappyIcon} /> + + + setShowButton(false)} /> + + + + Edit this page on GitHub + + + + + ); +}; diff --git a/docs/src/components/footer.tsx b/docs/src/components/footer.tsx new file mode 100644 index 0000000..0ee8de4 --- /dev/null +++ b/docs/src/components/footer.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { space } from '@stacks/ui'; +import { Pagination } from '@components/pagination'; +import { Section, SectionWrapper } from '@components/common'; +import { FeedbackSection } from '@components/feedback'; + +const Footer = ({ hidePagination, ...rest }: any) => { + return ( +
+ + + + +
+ ); +}; + +export { Footer }; diff --git a/docs/src/components/header.tsx b/docs/src/components/header.tsx new file mode 100644 index 0000000..b35d0dd --- /dev/null +++ b/docs/src/components/header.tsx @@ -0,0 +1,258 @@ +import React from 'react'; +import { + Flex, + Box, + BlockstackIcon, + Stack, + color, + space, + BoxProps, + ChevronIcon, + FlexProps, + Fade, +} from '@stacks/ui'; +import { Link, LinkProps, Text } from '@components/typography'; +import MenuIcon from 'mdi-react/MenuIcon'; +import CloseIcon from 'mdi-react/CloseIcon'; +import { useMobileMenuState } from '@common/hooks/use-mobile-menu'; + +import { ForwardRefExoticComponentWithAs, forwardRefWithAs } from '@stacks/ui-core'; +import NextLink from 'next/link'; +import { ColorModeButton } from '@components/color-mode-button'; +import { PAGE_WIDTH } from '@common/constants'; +import { border, transition } from '@common/utils'; +import { getCapsizeStyles } from '@components/mdx/typography'; +import { useTouchable } from '@common/hooks/use-touchable'; +import { StacksLogo } from '@components/stacks-logo'; + +const MenuButton = ({ ...rest }: any) => { + const { isOpen, handleOpen, handleClose } = useMobileMenuState(); + const Icon = isOpen ? CloseIcon : MenuIcon; + const handleClick = isOpen ? handleClose : handleOpen; + return ( + + + + ); +}; + +const HeaderWrapper: React.FC = React.forwardRef((props, ref: any) => ( + +)); + +interface NavChildren { + label: string; + href?: string; + target?: string; +} + +interface NavItem { + label: string; + href: string; + target?: string; + children?: NavItem[]; +} + +const nav: NavItem[] = [ + { + label: 'Start building', + href: '', + children: [ + { + label: 'Documentation', + href: 'https://docs.blockstack.org/', + target: '_self', + }, + { + label: 'GitHub', + href: 'https://github.com/blockstack', + }, + { + label: 'Papers', + href: 'https://www.blockstack.org/papers', + }, + { + label: 'Discord', + href: 'https://discord.com/invite/6PcCMU', + }, + ], + }, + { label: 'Testnet', href: 'https://www.blockstack.org/testnet' }, + { label: 'Discover apps', href: 'https://app.co/' }, +]; + +export const HEADER_HEIGHT = 132; + +const HeaderTextItem: ForwardRefExoticComponentWithAs = forwardRefWithAs< + BoxProps & LinkProps, + 'a' +>(({ children, href, as = 'a', ...rest }, ref) => ( + + {children} + +)); + +const NavItem: React.FC = ({ item, ...props }) => { + const { hover, active, bind } = useTouchable({ + behavior: 'link', + }); + return ( + + + {item.label} + + + {item.children ? ( + + + + ) : null} + {item.children ? ( + + {styles => ( + + + {item.children.map((child, _key) => ( + + {child.label} + + ))} + + + )} + + ) : null} + + ); +}; + +const Navigation: React.FC = props => { + return ( + + + {nav.map((item, key) => ( + + ))} + + + ); +}; + +const LogoLink = () => { + const { hover, active, bind } = useTouchable(); + return ( + + + + + + + + UI + + + + + ); +}; + +const Header = ({ hideSubBar, ...rest }: any) => { + return ( + <> + + + + + + + + + + + + + + ); +}; + +export { Header }; diff --git a/docs/src/components/home/card.tsx b/docs/src/components/home/card.tsx new file mode 100644 index 0000000..6a7e918 --- /dev/null +++ b/docs/src/components/home/card.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Grid, Box, Flex, space, color, transition, FlexProps } from '@stacks/ui'; +import NextLink from 'next/link'; +import { useTouchable } from '@common/hooks/use-touchable'; + +interface CardProps extends FlexProps { + href?: string; + dark?: boolean; +} +const LinkComponent = ({ href }: { href: string }) => + href ? ( + + + + ) : null; + +export const Card: React.FC = ({ children, onClick, dark = false, href, ...rest }) => { + const { bind, hover, active } = useTouchable({ + behavior: 'link', + }); + return ( + + {href && } + + {(children as any)({ hover, active })} + + + ); +}; diff --git a/docs/src/components/home/code-example-section.tsx b/docs/src/components/home/code-example-section.tsx new file mode 100644 index 0000000..32739e7 --- /dev/null +++ b/docs/src/components/home/code-example-section.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { Box, Flex, space } from '@stacks/ui'; +import { ExampleComponent } from '@components/example'; + +import { CodeExamples } from '@components/home/code-examples'; +import { BodyLarge, H2 } from '@components/home/text'; +import { Section, SectionWrapper } from '@components/home/common'; + +export const CodeSection = () => { + return ( +
+ + + +

Iterate quickly

+ + Build complex UI easily with primitives and highly composable components. + +
+ + + +
+ +
+
+ ); +}; + +export default CodeSection; diff --git a/docs/src/components/home/code-examples.tsx b/docs/src/components/home/code-examples.tsx new file mode 100644 index 0000000..cbe184e --- /dev/null +++ b/docs/src/components/home/code-examples.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { Box, Flex, space, color, themeColor } from '@stacks/ui'; + +import { border } from '@common/utils'; + +import { + ExampleComponent, + SimpleCodeBlock, + exampleCode, + gaiaHubUsage, + appItem, +} from '@components/example'; +import { InlineCode } from '@components/mdx'; + +export const CodeExamples = () => { + const [tab, setTab] = React.useState<'example' | 'gaiaHubUsage' | 'appItem'>('example'); + const getExampleCode = () => { + switch (tab) { + case 'example': + return exampleCode; + case 'gaiaHubUsage': + return gaiaHubUsage; + case 'appItem': + return appItem; + } + }; + const handleSetTab = (value: 'example' | 'gaiaHubUsage' | 'appItem') => { + console.log(value); + setTab(value); + }; + const tabs: { label: string; slug: 'example' | 'gaiaHubUsage' | 'appItem' }[] = [ + { label: `ExampleComponent`, slug: 'example' }, + { label: `GaiaHubUsage`, slug: 'gaiaHubUsage' }, + { label: `AppItem`, slug: 'appItem' }, + ]; + return ( + + + + {tabs.map((_tab, key) => { + return ( + handleSetTab(_tab.slug)} + > + {_tab.label} + + ); + })} + + + + + + + + ); +}; diff --git a/docs/src/components/home/common.tsx b/docs/src/components/home/common.tsx new file mode 100644 index 0000000..3cc75ea --- /dev/null +++ b/docs/src/components/home/common.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { + Box, + Grid, + Flex, + BoxProps, + transition, + space, + GridProps, + color, + FlexProps, +} from '@stacks/ui'; +import { CONTENT_MAX_WIDTH } from '@common/constants'; + +export const CircleIcon: React.FC< + FlexProps & { icon: React.FC; hover?: boolean; dark?: boolean } +> = ({ size = '72px', icon: Icon, hover, dark, ...rest }) => ( + + + + + +); + +export const Section: React.FC = props => ( + +); + +export const SectionWrapper: React.FC = props => ( + +); diff --git a/docs/src/components/home/footer.tsx b/docs/src/components/home/footer.tsx new file mode 100644 index 0000000..cf00b86 --- /dev/null +++ b/docs/src/components/home/footer.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Box, BoxProps, Grid, space, themeColor } from '@stacks/ui'; + +import { Section, SectionWrapper } from '@components/common'; +import { Text } from '@components/typography'; + +const SectionHeading: React.FC = props => ( + +); + +const SectionItem: React.FC = props => ( + +); + +export const Footer: React.FC = props => ( + +
+ + + + Primitives + Box + Flex + Grid + Text + + + Global & layout + ThemeProvider + CSS Reset + Color Modes + Stack + + + Utilities & hooks + space() + color() + border() + useClipboard + + + Stay up to date + GitHub + Twitter + Discord + Branding assets + + + +
+
+); diff --git a/docs/src/components/home/grid.tsx b/docs/src/components/home/grid.tsx new file mode 100644 index 0000000..a6bde6d --- /dev/null +++ b/docs/src/components/home/grid.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Box, space, color } from '@stacks/ui'; + +import { Body, SubHeading } from '@components/home/text'; +export const GridItem = ({ icon: Icon, title, body, ...rest }) => { + return ( + + {Icon && ( + + + + )} + + {title} + + {body} + + ); +}; diff --git a/docs/src/components/home/text.tsx b/docs/src/components/home/text.tsx new file mode 100644 index 0000000..44a3505 --- /dev/null +++ b/docs/src/components/home/text.tsx @@ -0,0 +1,109 @@ +import React from 'react'; +import { Box, BoxProps, color } from '@stacks/ui'; +import { Text } from '@components/typography'; +import { getCapsizeStyles, getHeadingStyles } from '@components/mdx/typography'; + +export const H1: React.FC = ({ children, ...rest }) => ( + + + {children} + + +); + +export const H2: React.FC = ({ children, ...rest }) => ( + + {children} + +); +export const H3: React.FC = ({ children, ...rest }) => ( + + {children} + +); +export const BodyLarge: React.FC = ({ children, ...rest }) => ( + + {children} + +); + +export const SubHeading = ({ as, children, ...rest }: any) => ( + + {children} + +); +export const Body = ({ as, children, ...rest }: any) => ( + + {children} + +); diff --git a/docs/src/components/hover-image.tsx b/docs/src/components/hover-image.tsx new file mode 100644 index 0000000..c1c2f30 --- /dev/null +++ b/docs/src/components/hover-image.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { Box, BoxProps, Grid, space } from '@stacks/ui'; +import { transition } from '@common/utils'; +import { Img } from '@components/mdx/image'; + +const Image = ({ + src, + isHovered, + size, + ...rest +}: BoxProps & { src?: string; isHovered?: boolean }) => ( + + + +); +export const HoverImage: React.FC = React.memo( + ({ isHovered, src, ...props }) => ( + + + + + + ) +); diff --git a/docs/src/components/icons/_base.tsx b/docs/src/components/icons/_base.tsx new file mode 100644 index 0000000..01a1c5a --- /dev/null +++ b/docs/src/components/icons/_base.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Box, BoxProps } from '@stacks/ui'; +export type SvgProps = React.FC; + +export const BaseSvg: SvgProps = props => ( + +); diff --git a/docs/src/components/icons/accessible.tsx b/docs/src/components/icons/accessible.tsx new file mode 100644 index 0000000..bd6c29d --- /dev/null +++ b/docs/src/components/icons/accessible.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Box, BoxProps } from '@stacks/ui'; + +export const AccessibleIcon: React.FC = props => ( + + + + + + +); diff --git a/docs/src/components/icons/alert-circle.tsx b/docs/src/components/icons/alert-circle.tsx new file mode 100644 index 0000000..3ce06fc --- /dev/null +++ b/docs/src/components/icons/alert-circle.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const AlertCircleIcon: SvgProps = props => ( + + + + + + +); diff --git a/docs/src/components/icons/alert-triangle.tsx b/docs/src/components/icons/alert-triangle.tsx new file mode 100644 index 0000000..d10598f --- /dev/null +++ b/docs/src/components/icons/alert-triangle.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const AlertTriangleIcon: SvgProps = props => ( + + + + + +); diff --git a/docs/src/components/icons/apps.tsx b/docs/src/components/icons/apps.tsx new file mode 100644 index 0000000..eb53a7b --- /dev/null +++ b/docs/src/components/icons/apps.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const AppsIcon: SvgProps = props => ( + + + + + + + + +); diff --git a/docs/src/components/icons/arrow-left.tsx b/docs/src/components/icons/arrow-left.tsx new file mode 100644 index 0000000..e6aead5 --- /dev/null +++ b/docs/src/components/icons/arrow-left.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const ArrowLeftIcon: SvgProps = props => ( + + + + + + +); diff --git a/docs/src/components/icons/arrow-right.tsx b/docs/src/components/icons/arrow-right.tsx new file mode 100644 index 0000000..a09fc76 --- /dev/null +++ b/docs/src/components/icons/arrow-right.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const ArrowRightIcon: SvgProps = props => ( + + + + + + +); diff --git a/docs/src/components/icons/atom-alt.tsx b/docs/src/components/icons/atom-alt.tsx new file mode 100644 index 0000000..e01837c --- /dev/null +++ b/docs/src/components/icons/atom-alt.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const AtomAltIcon: SvgProps = props => ( + + + + + + + + + + +); diff --git a/docs/src/components/icons/atom.tsx b/docs/src/components/icons/atom.tsx new file mode 100644 index 0000000..a6fd5bf --- /dev/null +++ b/docs/src/components/icons/atom.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Box, BoxProps } from '@stacks/ui'; +export const AtomIcon: React.FC = props => ( + + + + + + +); diff --git a/docs/src/components/icons/blockstack-logo.tsx b/docs/src/components/icons/blockstack-logo.tsx new file mode 100644 index 0000000..e0f420c --- /dev/null +++ b/docs/src/components/icons/blockstack-logo.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Box, BoxProps } from '@stacks/ui'; + +export const BlockstackLogo = ({ size = '24px', ...props }: BoxProps) => ( + + + + + +); diff --git a/docs/src/components/icons/box.tsx b/docs/src/components/icons/box.tsx new file mode 100644 index 0000000..0d0c0e4 --- /dev/null +++ b/docs/src/components/icons/box.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const BoxIcon: SvgProps = props => ( + + + + + + + +); diff --git a/docs/src/components/icons/braces.tsx b/docs/src/components/icons/braces.tsx new file mode 100644 index 0000000..19fdf20 --- /dev/null +++ b/docs/src/components/icons/braces.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const BracesIcon: SvgProps = props => ( + + + + + +); diff --git a/docs/src/components/icons/check-circle.tsx b/docs/src/components/icons/check-circle.tsx new file mode 100644 index 0000000..cef5dd7 --- /dev/null +++ b/docs/src/components/icons/check-circle.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const CheckCircleIcon: SvgProps = props => ( + + + + + +); diff --git a/docs/src/components/icons/circle-check.tsx b/docs/src/components/icons/circle-check.tsx new file mode 100644 index 0000000..ddceff1 --- /dev/null +++ b/docs/src/components/icons/circle-check.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const CircleCheck: SvgProps = props => ( + + + + + +); diff --git a/docs/src/components/icons/clipboard-check.tsx b/docs/src/components/icons/clipboard-check.tsx new file mode 100644 index 0000000..c869374 --- /dev/null +++ b/docs/src/components/icons/clipboard-check.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const ClipboardCheckIcon: SvgProps = props => ( + + + + + + +); diff --git a/docs/src/components/icons/clipboard.tsx b/docs/src/components/icons/clipboard.tsx new file mode 100644 index 0000000..05380ea --- /dev/null +++ b/docs/src/components/icons/clipboard.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const ClipboardIcon: SvgProps = props => ( + + + + + +); diff --git a/docs/src/components/icons/clock.tsx b/docs/src/components/icons/clock.tsx new file mode 100644 index 0000000..20df493 --- /dev/null +++ b/docs/src/components/icons/clock.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const ClockIcon: SvgProps = props => ( + + + + + +); diff --git a/docs/src/components/icons/code.tsx b/docs/src/components/icons/code.tsx new file mode 100644 index 0000000..dfa46b7 --- /dev/null +++ b/docs/src/components/icons/code.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const CodeIcon: SvgProps = props => ( + + + + + + +); diff --git a/docs/src/components/icons/copy.tsx b/docs/src/components/icons/copy.tsx new file mode 100644 index 0000000..9ebbc6f --- /dev/null +++ b/docs/src/components/icons/copy.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const CopyIcon: SvgProps = props => ( + + + + + +); diff --git a/docs/src/components/icons/dark-mode.tsx b/docs/src/components/icons/dark-mode.tsx new file mode 100644 index 0000000..c86f7d2 --- /dev/null +++ b/docs/src/components/icons/dark-mode.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Box, BoxProps } from '@stacks/ui'; + +export const DarkModeIcon = (props: BoxProps) => ( + + + + + +); diff --git a/docs/src/components/icons/edit.tsx b/docs/src/components/icons/edit.tsx new file mode 100644 index 0000000..fb13abe --- /dev/null +++ b/docs/src/components/icons/edit.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const EditIcon: SvgProps = props => ( + + + + + + +); diff --git a/docs/src/components/icons/feedback.tsx b/docs/src/components/icons/feedback.tsx new file mode 100644 index 0000000..443d826 --- /dev/null +++ b/docs/src/components/icons/feedback.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { Box, BoxProps, transition } from '@stacks/ui'; +export type SvgProps = React.FC; + +export const SadIcon: SvgProps = ({ bg = '#E1E3E8', ...props }) => ( + + + + + + +); + +export const NeutralIcon: SvgProps = ({ bg = '#E1E3E8', ...props }) => ( + + + + + + +); + +export const HappyIcon: SvgProps = ({ bg = '#E1E3E8', ...props }) => ( + + + + + + +); diff --git a/docs/src/components/icons/gauge.tsx b/docs/src/components/icons/gauge.tsx new file mode 100644 index 0000000..80ab8af --- /dev/null +++ b/docs/src/components/icons/gauge.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const GaugeIcon: SvgProps = props => ( + + + + + + + +); diff --git a/docs/src/components/icons/info-circle.tsx b/docs/src/components/icons/info-circle.tsx new file mode 100644 index 0000000..5f7cc12 --- /dev/null +++ b/docs/src/components/icons/info-circle.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const InfoCircleIcon: SvgProps = props => ( + + + + + + +); diff --git a/docs/src/components/icons/layers-intersect.tsx b/docs/src/components/icons/layers-intersect.tsx new file mode 100644 index 0000000..67b1e1d --- /dev/null +++ b/docs/src/components/icons/layers-intersect.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const LayersIntersectIcon: SvgProps = props => ( + + + + + +); diff --git a/docs/src/components/icons/light-mode.tsx b/docs/src/components/icons/light-mode.tsx new file mode 100644 index 0000000..5b964ca --- /dev/null +++ b/docs/src/components/icons/light-mode.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Box, BoxProps } from '@stacks/ui'; + +export const LightModeIcon = (props: BoxProps) => ( + + + + + +); diff --git a/docs/src/components/icons/magnifying-glass.tsx b/docs/src/components/icons/magnifying-glass.tsx new file mode 100644 index 0000000..8e92e87 --- /dev/null +++ b/docs/src/components/icons/magnifying-glass.tsx @@ -0,0 +1,24 @@ +import React from 'react'; + +import { Box, BoxProps } from '@stacks/ui'; + +interface MagnifyingGlassProps extends BoxProps { + size?: number; +} + +export const MagnifyingGlass = ({ size = 16, ...props }: MagnifyingGlassProps) => ( + + + + + +); diff --git a/docs/src/components/icons/package.tsx b/docs/src/components/icons/package.tsx new file mode 100644 index 0000000..33cabb8 --- /dev/null +++ b/docs/src/components/icons/package.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Box, BoxProps } from '@stacks/ui'; +export const PackageIcon: React.FC = props => ( + + + + + + + + +); diff --git a/docs/src/components/icons/paint.tsx b/docs/src/components/icons/paint.tsx new file mode 100644 index 0000000..95abaca --- /dev/null +++ b/docs/src/components/icons/paint.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Box, BoxProps } from '@stacks/ui'; +export const PaintIcon: React.FC = props => ( + + + + + + +); diff --git a/docs/src/components/icons/palette.tsx b/docs/src/components/icons/palette.tsx new file mode 100644 index 0000000..e92475d --- /dev/null +++ b/docs/src/components/icons/palette.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Box, BoxProps } from '@stacks/ui'; +export const PaletteIcon: React.FC = props => ( + + + + + + + +); diff --git a/docs/src/components/icons/search.tsx b/docs/src/components/icons/search.tsx new file mode 100644 index 0000000..5e10e65 --- /dev/null +++ b/docs/src/components/icons/search.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const SearchIcon: SvgProps = props => ( + + + + + +); diff --git a/docs/src/components/icons/server.tsx b/docs/src/components/icons/server.tsx new file mode 100644 index 0000000..5b14932 --- /dev/null +++ b/docs/src/components/icons/server.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const ServerIcon: SvgProps = props => ( + + + + + + + +); diff --git a/docs/src/components/icons/sitemap.tsx b/docs/src/components/icons/sitemap.tsx new file mode 100644 index 0000000..ab56f0e --- /dev/null +++ b/docs/src/components/icons/sitemap.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const SitemapIcon: SvgProps = props => ( + + + + + + + + +); diff --git a/docs/src/components/icons/stack.tsx b/docs/src/components/icons/stack.tsx new file mode 100644 index 0000000..c43a8e6 --- /dev/null +++ b/docs/src/components/icons/stack.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const StackIcon: SvgProps = props => ( + + + + + + +); diff --git a/docs/src/components/icons/tools.tsx b/docs/src/components/icons/tools.tsx new file mode 100644 index 0000000..b608c18 --- /dev/null +++ b/docs/src/components/icons/tools.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const ToolsIcon: SvgProps = props => ( + + + + + + + + + +); diff --git a/docs/src/components/icons/world.tsx b/docs/src/components/icons/world.tsx new file mode 100644 index 0000000..9e83d12 --- /dev/null +++ b/docs/src/components/icons/world.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { BaseSvg, SvgProps } from '@components/icons/_base'; + +export const WorldIcon: SvgProps = props => ( + + + + + + + + +); diff --git a/docs/src/components/layouts/base-layout.tsx b/docs/src/components/layouts/base-layout.tsx new file mode 100644 index 0000000..de92a74 --- /dev/null +++ b/docs/src/components/layouts/base-layout.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { Flex } from '@stacks/ui'; +import { SideNav } from '../side-nav'; +import { Header } from '../header'; +import { Main } from '../main'; +import { Footer } from '../footer'; + +import { PAGE_WIDTH, SIDEBAR_WIDTH } from '@common/constants'; +import { useWatchActiveHeadingChange } from '@common/hooks/use-active-heading'; +import { useRouter } from 'next/router'; +import { MobileMenu } from '@components/mobile-menu'; + +const BaseLayout: React.FC<{ isHome?: boolean }> = ({ children }) => { + const router = useRouter(); + const isHome = router.pathname === '/'; + + useWatchActiveHeadingChange(); + return ( + + +
+ + + + +
+ + {children} + +
+