This commit is contained in:
Micah Godbolt 2020-04-06 16:55:32 -07:00
Родитель a49b6fd856
Коммит 937f5ab42f
125 изменённых файлов: 14332 добавлений и 1464 удалений

4
.gitignore поставляемый
Просмотреть файл

@ -28,6 +28,10 @@ lib-cov
# Coverage directory used by tools like istanbul # Coverage directory used by tools like istanbul
coverage coverage
# gatsby files
.cache/
public
# nyc test coverage # nyc test coverage
.nyc_output .nyc_output

106
docs/README.md Normal file
Просмотреть файл

@ -0,0 +1,106 @@
# Fluent Website Content
__Key Concepts:__
1. [Folder path === URL](#adding-new-content)
2. [Left hand navigation defined per folder/folder tree](#creating-navigation)
3. [Built with MD, MDX or TSX](#supported-page-formats)
4. [Host your content anywhere](#hosting-your-content)
## Adding new content
Files in the `docs` folders are built to a page with the same URL as the relative directory. `index` files will be rendered as the folder's root page.
`docs/components/button.mdx` will be built to `example.com/components/button.html`. `docs/styles/index.tsx` will be built to `example.com/styles/index.html`
## Creating navigation
The vertical navigation of each page is written in a `toc.yml` file that includes `name`, `link` and any children `items`.
- `name` is the link text
- `link` is the full url to the page
- `items` is an array of name/link pairs and can be further nested
```yml
- name: Components
items:
- name: Button
link: components/button
- name: Toggle
link: components/toggle
```
### Unique navigation for sub controls
Often you'll want a subsection of the site to have its own navigation. The navigation of each page is based off of the closest `toc.yml` file to the page.
```md
docs/
styles.mdx
toc.yml
components/
button.mdx
toc.yml
foo/
bar.mdx
```
The `styles` page will have the navigation from `docs/toc.yml` and `button` page will use the navigation found in `docs/components/toc.yml`.
`docs/foo` does not contain a `toc.yml` so `docs/toc.yml` will be used for `bar.mdx`.
## Supported page formats
The Fluid UI Site supports multiple page formats.
### MDX
[MDX](https://mdxjs.com/) is a superset of markdown that adds the power of JSX to the file.
This means you can import JSX directly into your markdown content.
#### Importing JSX into MDX
```md
import {Button} from 'office-ui-fabric-react'
## This is a Fabric button
<Button primary={true}> Click Me </Button>
```
#### Importing MD into MDX
Another great feature of MDX is the ability to import other MD or MDX files into a single file.
This is a great way to split content out into multiple files and combine/reuse it.
```md
import Stuff from './somestuff.md'
Hello, this is my <Stuff />
```
### TSX Files
TSX files can be used when you need complete control over the page contents. No assumptions will be made about the page contents, styles or meta information (other than URL).
#### Leveraging site templates
Unless your page is meant to be a standalone app, we recommend using the built in `PageTemplate` to render the default page shell.
```tsx
import React from 'react';
import PageTemplate from 'gatsby-theme-fluent-site/src/templates/PageTemplate'
import
export default () => {
return <PageTemplate>Page Content</PageTemplate>
}
```
## Hosting your content
Gatsby can source pages from multiple locations. Content added to this repo under `docs/ios` could easily be moved to another repo under `fluentui-docs/ios` and produce the exact same page content. This workflow is not yet fully implemented, but it is a core tenent and fully supported by our tech choices.

2
docs/content/toc.yml Normal file
Просмотреть файл

@ -0,0 +1,2 @@
- name: Windows
link: /windows

Просмотреть файл

@ -0,0 +1,4 @@
---
title: Components
---

Просмотреть файл

@ -0,0 +1,20 @@
- name: Components
items:
- name: Button
link: windows/components/button
- name: Link
link: windows/components/link
- name: RadioGroup
link: windows/components/radiogroup
- name: Separator
link: windows/components/separator
- name: Text
link: windows/components/text
- name: Utilities
items:
- name: FocusTrapZone
link: windows/components/utilities/focustrapzone
- name: Pressable
link: windows/components/utilities/pressable
- name: Stack
link: windows/components/utilities/stack

Просмотреть файл

Просмотреть файл

@ -0,0 +1,3 @@
---
title: Experiences
---

Просмотреть файл

@ -0,0 +1,2 @@
- name: Example
link: windows

Просмотреть файл

@ -0,0 +1,5 @@
---
title: Get started
---
#

Просмотреть файл

@ -0,0 +1,2 @@
- name: Example
link: windows

Просмотреть файл

@ -15,16 +15,16 @@ You can import React components in an `.mdx` file. Like this:
## Components ## Components
- [Button](/Components/Button) - [Button](/windows/components/button)
- [Link](/Components/Link) - [Link](/windows/components/link)
- [Separator](/Components/Separator) - [Separator](/windows/components/separator)
- [Text](/Components/Text) - [Text](/windows/components/text)
## Utilities ## Utilities
- [FocusTrapZone](/Utilities/FocusTrapZone) - [FocusTrapZone](/windows/components/utilities/focustrapzone)
- [Pressable](/Utilities/Pressable) - [Pressable](/windows/components/utilities/pressable)
- [Stack](/Utilities/Stack) - [Stack](/windows/components/utilities/stack)
## Contributing Docs ## Contributing Docs

Просмотреть файл

@ -0,0 +1,5 @@
---
title: Styles
---
styles

Просмотреть файл

@ -0,0 +1,2 @@
- name: Example
link: windows

16
docs/gatsby-config.js Normal file
Просмотреть файл

@ -0,0 +1,16 @@
module.exports = {
siteMetadata: {
siteURL: 'https://fluentui.z5.web.core.windows.net/',
},
plugins: [
`gatsby-plugin-typescript`,
`gatsby-plugin-sharp`,
'gatsby-transformer-sharp',
{
resolve: `gatsby-theme-fluent-site`,
options: {
contentPath: `./content`,
},
},
],
};

Просмотреть файл

@ -1,8 +0,0 @@
const withMDX = require('@next/mdx')({
extension: /\.mdx?$/
});
module.exports = withMDX({
pageExtensions: ['js', 'jsx', 'md', 'mdx']
});

Просмотреть файл

@ -1,16 +1,30 @@
{ {
"name": "fluentui-docs", "name": "fluent-website",
"version": "0.1.0", "version": "0.0.1",
"private": true, "description": "Fluent website content",
"repository": {
"type": "git",
"url": "https://github.com/microsoft/fluent-site"
},
"license": "MIT",
"scripts": { "scripts": {
"dev": "next", "clean": "gatsby clean",
"start": "next start", "build": "gatsby build --prefix-paths",
"build": "next build && echo" "develop": "gatsby clean && gatsby develop --port 3000",
"serve": "gatsby serve",
"start": "npm run develop"
},
"devDependencies": {
"gatsby-theme-fluent-site": "^0.1.1",
"gatsby-plugin-sharp": "^2.3.13",
"gatsby-transformer-sharp": "^2.3.12"
}, },
"dependencies": { "dependencies": {
"@mdx-js/loader": "^1.5.5", "gatsby-plugin-typescript": "^2.1.26",
"@mdx-js/mdx": "^1.5.5", "typescript": "^3.5.1",
"@next/mdx": "^9.2.1", "gatsby": "^2.19.27",
"next": "^9.2.1" "gatsby-plugin-emotion": "^4.1.23",
"react": "^16.13.0",
"react-dom": "^16.13.0"
} }
} }

18
docs/tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,18 @@
{
"include": ["./src/**/*"],
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"lib": ["dom", "es2017"],
// "allowJs": true,
// "checkJs": true,
"jsx": "react",
"strict": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noEmit": true,
"skipLibCheck": true,
"noImplicitAny": false
}
}

30
packages/docs/gatsby-plugin-docs-creator/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,30 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
decls
dist

Просмотреть файл

@ -0,0 +1,34 @@
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
*.un~
yarn.lock
src
flow-typed
coverage
decls
examples

Просмотреть файл

@ -0,0 +1,104 @@
# gatsby-plugin-docs-creator
Gatsby plugin that automatically creates pages from React components in specified directories. Gatsby
includes this plugin automatically in all sites for creating pages from components in `src/pages`.
You may include another instance of this plugin if you'd like to create additional "pages" directories.
With this plugin, _any_ file that lives in the specified pages folder (e.g. the default `src/pages`) or subfolders will be expected to export a React Component to generate a Page. The following files are automatically excluded:
- `template-*`
- `__tests__/*`
- `*.test.jsx?`
- `*.spec.jsx?`
- `*.d.tsx?`
- `*.json`
- `*.yaml`
- `_*`
- `.*`
To exclude custom patterns, see [Ignoring Specific Files](#ignoring-specific-files)
## Install
`npm install --save gatsby-plugin-docs-creator`
## How to use
```javascript
// gatsby-config.js
module.exports = {
plugins: [
// You can have multiple instances of this plugin
// to create pages from React components in different directories.
//
// The following sets up the pattern of having multiple
// "pages" directories in your project
{
resolve: `gatsby-plugin-docs-creator`,
options: {
path: `${__dirname}/src/account/pages`,
},
},
{
resolve: `gatsby-plugin-docs-creator`,
options: {
path: `${__dirname}/src/settings/pages`,
},
},
],
}
```
### Ignoring Specific Files
#### Shorthand
```javascript
// The following example will disable the `/blog` index page
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-docs-creator`,
options: {
path: `${__dirname}/src/indexes/pages`,
ignore: [`blog.(js|ts)?(x)`],
// See pattern syntax recognized by micromatch
// https://www.npmjs.com/package/micromatch#matching-features
},
},
],
}
```
**NOTE**: The above code snippet will only stop the creation of the `/blog` page, which is defined as a React component.
This plugin does not affect programmatically generated pages from the [createPagesAPI](https://www.gatsbyjs.org/docs/node-apis/#createPages).
#### Ignore Options
```javascript
// The following example will ignore pages using case-insensitive matching
// gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-docs-creator`,
options: {
path: `${__dirname}/src/examples/pages`,
ignore: {
// Example: Ignore `file.example.js`, `dir/s/file.example.tsx`
patterns: [`**/*.example.(js|ts)?(x)`],
// Example: Match both `file.example.js` and `file.EXAMPLE.js`
options: { nocase: true },
// See all available micromatch options
// https://www.npmjs.com/package/micromatch#optionsnocase
},
},
},
],
}
```

Просмотреть файл

@ -0,0 +1 @@
module.exports = require('./dist/gatsby-node')

Просмотреть файл

@ -0,0 +1,50 @@
{
"name": "gatsby-plugin-docs-creator",
"version": "2.1.40",
"description": "Gatsby plugin that automatically creates pages from React components in specified directories with additional docs related data",
"main": "dist/gatsby-node.js",
"scripts": {
"build": "tsc",
"start": "yarn watch",
"watch": "tsc -w --preserveWatchOutput",
"prepare": "cross-env NODE_ENV=production npm run build"
},
"keywords": [
"gatsby",
"gatsby-plugin"
],
"author": "Micah Godbolt <mgodbolt@microsoft.com>",
"contributors": [
"Steven Natera <tektekpush@gmail.com> (https://twitter.com/stevennatera)",
"Kyle Mathews <mathews.kyle@gmail.com>"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/microsoft/fluent-site.git",
"directory": "packages/gatsby-plugin-docs-creator"
},
"dependencies": {
"@types/bluebird": "^3.5.29",
"@types/js-yaml": "^3.12.2",
"@types/lodash": "^4.14.149",
"@types/node": "^13.7.4",
"bluebird": "^3.7.2",
"fs-exists-cached": "^1.0.0",
"gatsby-page-utils": "^0.0.39",
"glob": "^7.1.6",
"js-yaml": "^3.13.1",
"lodash": "^4.17.15",
"micromatch": "^3.1.10"
},
"devDependencies": {
"cross-env": "^5.2.1",
"tslib": "^1.10.0"
},
"peerDependencies": {
"gatsby": "^2.0.0"
},
"engines": {
"node": ">=8.0.0"
}
}

Просмотреть файл

@ -0,0 +1,136 @@
import _ from 'lodash';
import yaml from 'js-yaml';
import { createPath, validatePath, ignorePath, watchDirectory } from 'gatsby-page-utils';
const BBPromise = require('bluebird');
const existsSync = require(`fs-exists-cached`).sync;
const systemPath = require(`path`);
const { readFileSync } = require(`fs`);
const globCB = require(`glob`);
const glob = BBPromise.promisify(globCB);
// Path creator.
// Auto-create pages.
// algorithm is glob /pages directory for js/jsx/cjsx files *not*
// underscored. Then create url w/ our path algorithm *unless* user
// takes control of that page component in gatsby-node.
export const createPagesStatefully = async ({ store, actions, reporter }, { path: pagesPath, pathCheck = true, ignore }, doneCb) => {
const { createPage, deletePage } = actions;
const program = store.getState().program;
const exts = program.extensions.map(e => `${e.slice(1)}`).join(`,`);
if (!pagesPath) {
reporter.panic(
`
"path" is a required option for gatsby-plugin-page-creator
See docs here - https://www.gatsbyjs.org/plugins/gatsby-plugin-page-creator/
`
);
}
// Validate that the path exists.
if (pathCheck && !existsSync(pagesPath)) {
reporter.panic(
`
The path passed to gatsby-plugin-page-creator does not exist on your file system:
${pagesPath}
Please pick a path to an existing directory.
`
);
}
const findNearestFile = (matchPath: string, filePaths: string[]): string | undefined => {
if (filePaths === undefined) {
return undefined;
}
const matchParts = matchPath.split('/');
do {
const match = filePaths.find(filePath => {
const fileRelPath = systemPath.dirname(filePath);
const matchRelPath = systemPath.dirname(matchParts.join('/'));
return fileRelPath === matchRelPath;
});
if (match !== undefined) {
return match;
} else matchParts.splice(-1, 1);
} while (matchParts.length > 0);
return undefined;
};
const pagesDirectory = systemPath.resolve(process.cwd(), pagesPath);
const pagesGlob = `**/*.{${exts}}`;
const tocGlob = '**/toc.yml';
// Get initial list of files.
let files = await glob(pagesGlob, { cwd: pagesPath });
const tocs = await glob(tocGlob, { cwd: pagesPath });
files.forEach(file => {
const tocPath = findNearestFile(file, tocs);
_createPage(file, pagesDirectory, createPage, ignore, tocPath);
});
watchDirectory(
pagesPath,
pagesGlob,
addedPath => {
if (!_.includes(files, addedPath)) {
const tocPath = findNearestFile(addedPath, tocs);
_createPage(addedPath, pagesDirectory, createPage, ignore, tocPath);
files.push(addedPath);
}
},
removedPath => {
// Delete the page for the now deleted component.
const componentPath = systemPath.join(pagesDirectory, removedPath);
store.getState().pages.forEach(page => {
if (page.component === componentPath) {
deletePage({
path: createPath(removedPath),
component: componentPath
});
}
});
files = files.filter(f => f !== removedPath);
}
).then(() => doneCb());
};
const _createPage = (filePath, pagesDirectory, createPage, ignore, tocPath) => {
// Filter out special components that shouldn't be made into
// pages.
if (!validatePath(filePath)) {
return;
}
// Filter out anything matching the given ignore patterns and options
if (ignorePath(filePath, ignore)) {
return;
}
let toc = undefined;
if (tocPath !== undefined) {
try {
toc = yaml.safeLoad(readFileSync(systemPath.join(pagesDirectory, tocPath), 'utf8'));
} catch (e) {
console.log(e);
}
}
// Create page object
const createdPath = createPath(filePath);
const page = {
path: createdPath,
component: systemPath.join(pagesDirectory, filePath),
context: {
toc: toc,
rootPath: filePath.substring(0, filePath.indexOf('/'))
}
};
// Add page
createPage(page);
};

Просмотреть файл

@ -0,0 +1,23 @@
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "dist",
"target": "es5",
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"importHelpers": true,
"noUnusedLocals": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true,
"noImplicitAny": false,
"moduleResolution": "node",
"preserveConstEnums": true,
"lib": ["es5", "dom"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
},
"include": ["src"]
}

Просмотреть файл

@ -0,0 +1,12 @@
{
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"semi": "off"
}
}

Просмотреть файл

@ -0,0 +1,23 @@
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
module.exports = {
stories: ['../src/**/*.story.tsx'],
addons: ['@storybook/addon-actions', '@storybook/addon-links'],
webpackFinal: async config => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
loader: require.resolve('babel-loader'),
options: {
presets: [['react-app', { flow: false, typescript: true }]],
},
})
config.plugins.push(
new MonacoWebpackPlugin({
languages: ['typescript'],
})
)
config.resolve.extensions.push('.ts', '.tsx')
return config
},
}

Просмотреть файл

@ -0,0 +1,96 @@
# Application Insights README
Fluent website telementry information
**Table of Contents**
<!-- TOC -->
- [Application Insights README](#application-insights-readme)
- [Description](#description)
- [Usage](#usage)
- [IMPORTANT! Build notes](#important-build-notes)
- [NPM Packages](#npm-packages)
- [Resources](#resources)
<!-- /TOC -->
## Description
The Application Insights interface has be designed as a client side only react component. It hooks into the equivelent of
onComponentDidMount event (via hooks) and runs once per instantiation.
## Usage
*PageView*
```typescript
import { usePageViewTelemetry } from '../components/ApplicationInsights'
...
let pathName = "Home" // if pathName is null, it will use the window.location.pathName value
const [pageView, setPageView] = usePageViewTelemetry({ name: props.path })
// since the call is made immediately, you can just do the following if you are not updating the value
usePageViewTelemetry({ name: props.path })
```
*EventView*
```jsx
import { useEventTelemetry } from '../components/ApplicationInsights'
...
const [myEvent, invokeMyEventTelemetry] = useEventTelemetry({ name: 'MyEvent' })
const buttonClick = () => {
invokeMyEventTelemetry();
}
return (
<button onClick={buttonClick}>Click Here!</button>
)
```
*EventView With Name/Value Property*
```jsx
import { useEventTelemetry } from '../components/ApplicationInsights'
...
const [myEvent, invokeMyEventTelemetry] = useEventTelemetry({ name: 'MyEvent' })
const sendEvent = (buttonId:number) => {
myEvent.properties = myEvent.properties ? myEvent.properties : []
myEvent.properties["Button_Clicked"] = buttonId
// this call will send the update and send the telementry data
invokeMyEventTelemetry(myEvent)
}
return (
<button onClick={() => {sendEvent(1)}}>Button 1</button>
<button onClick={() => {sendEvent(2)}}>Button 2</button>
)
```
## IMPORTANT! Build notes
*NOTE
For production builds you need to set GATBSY_APPLICATIONINSIGHTS_KEY to the value of the production key *prior* to
a production build. This can be done as an evironment variable or in the .env.production file under src/website.
The key is retrieved from the Application Insights app on https://portal.azure.com
For development/test builds, modify the .env.developement file.
## NPM Packages
NPM package(s):
@microsoft/applicationinsights-web
@microsoft/applicationinsights-react-js
## Resources
[Azure Portal Resource](https://ms.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/9ccbac18-03d3-485b-a43e-87dc09014817/resourcegroups/OXOSharedRG/providers/microsoft.insights/components/FluentUI-Website/overview)
[Javascript NPM Setup](https://docs.microsoft.com/en-us/azure/azure-monitor/app/javascript#npm-based-setup)
[Application Insights React](https://github.com/microsoft/ApplicationInsights-JS/blob/17ef50442f73fd02a758fbd74134933d92607ecf/extensions/applicationinsights-react-js/README.md)

Просмотреть файл

@ -0,0 +1,24 @@
{
"name": "gatsby-starter-uifabric-doc",
"entries": [
{
"date": "Sat, 27 Jul 2019 05:29:12 GMT",
"tag": "gatsby-starter-uifabric-doc_v0.1.1",
"version": "0.1.1",
"comments": {
"patch": [
{
"comment": "initial publish",
"author": "kchau@microsoft.com",
"commit": "d5ff88bc7ddf21d5e9035f4a95503df50709f55c"
},
{
"comment": "initial release",
"author": "kchau@microsoft.com",
"commit": "e9f0dccdd68a5890a3a063b79314d3ea446a95da"
}
]
}
}
]
}

Просмотреть файл

@ -0,0 +1,11 @@
# Change Log - gatsby-starter-uifabric-doc
This log was last generated on Sat, 27 Jul 2019 05:29:12 GMT and should not be manually modified.
## 0.1.1
Sat, 27 Jul 2019 05:29:12 GMT
### Patches
- initial publish (kchau@microsoft.com)
,- initial release (kchau@microsoft.com)

Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 gatsbyjs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Просмотреть файл

@ -0,0 +1,86 @@
## 🚀 Quick start
1. **Start developing.**
```sh
yarn
yarn start
```
1. **Open the source code and start editing!**
Your site is now running at `http://localhost:3000`!
_Note: You'll also see a second link: _`http://localhost:3000/___graphql`_. This is a tool you can use to experiment with querying your data. Learn more about using this tool in the [Gatsby tutorial](https://www.gatsbyjs.org/tutorial/part-five/#introducing-graphiql)._
1. Working with [NetlifyCMS](https://www.netlifycms.org/)
NetlifyCMS is a React application that sites on top of the markdown content in git and provides a user friendly interface for creating, editing, and reviewing proposed content changes.
To develop new collections and work within NetlifyCMS's `static/admin/config.yml` file, you can now run a local server and allow the CMS to create and edit your local files
```sh
npx netlify-cms-proxy-server
# while server is running, in a seperate terminal run
yarn start
```
Now when you navigate to `http://localhost:3000/admin/` you will be allowed to log in without authentication, and any file change will only change your local data. No git involved.
## 🧐 What's inside?
A quick look at the top-level files and directories you'll see in a the Website package.
.
├── src
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── gatsby-ssr.js
├── LICENSE
├── package-lock.json
├── package.json
└── README.md
2. **`/src`**: This directory will contain all of the code related to what you will see on the front-end of your site (what you see in the browser) such as your site header or a page template. `src` is a convention for “source code”.
5. **`gatsby-browser.js`**: This file is where Gatsby expects to find any usage of the [Gatsby browser APIs](https://www.gatsbyjs.org/docs/browser-apis/) (if any). These allow customization/extension of default Gatsby settings affecting the browser.
6. **`gatsby-config.js`**: This is the main configuration file for a Gatsby site. This is where you can specify information about your site (metadata) like the site title and description, which Gatsby plugins youd like to include, etc. (Check out the [config docs](https://www.gatsbyjs.org/docs/gatsby-config/) for more detail).
7. **`gatsby-node.js`**: This file is where Gatsby expects to find any usage of the [Gatsby Node APIs](https://www.gatsbyjs.org/docs/node-apis/) (if any). These allow customization/extension of default Gatsby settings affecting pieces of the site build process.
8. **`gatsby-ssr.js`**: This file is where Gatsby expects to find any usage of the [Gatsby server-side rendering APIs](https://www.gatsbyjs.org/docs/ssr-apis/) (if any). These allow customization of default Gatsby settings affecting server-side rendering.
9. **`LICENSE`**: Gatsby is licensed under the MIT license.
11. **`package.json`**: A manifest file for Node.js projects, which includes things like metadata (the projects name, author, etc). This manifest is how npm knows which packages to install for your project.
12. **`README.md`**: A text file containing useful reference information about your project.
## 🎓 Learning Gatsby
Looking for more guidance? Full documentation for Gatsby lives [on the website](https://www.gatsbyjs.org/). Here are some places to start:
- **For most developers, we recommend starting with our [in-depth tutorial for creating a site with Gatsby](https://www.gatsbyjs.org/tutorial/).** It starts with zero assumptions about your level of ability and walks through every step of the process.
- **To dive straight into code samples, head [to our documentation](https://www.gatsbyjs.org/docs/).** In particular, check out the _Guides_, _API Reference_, and _Advanced Tutorials_ sections in the sidebar.
## 💫 Deploy
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/gatsbyjs/gatsby-starter-hello-world)
<!-- AUTO-GENERATED-CONTENT:END -->
## Developing Components
Check out [README](./src/components/CONTRIBUTING.md) in the components directory.
## Storybook
A playground for developing components in isolation [README](./src/components/STORYBOOK.md)

Просмотреть файл

@ -0,0 +1,145 @@
require('dotenv').config({
path: `.env.${process.env.NODE_ENV}`,
})
module.exports = themeOptions => {
const { contentPath, pathPrefix } = themeOptions
return {
pathPrefix: pathPrefix || '',
siteMetadata: {
title: 'Microsoft Design - Fluent',
description:
'Fluent brings the fundamentals of principled design, innovation in technology, and customer needs together as one. Its a collective approach to creating simplicity and coherence through a shared, open design system across platforms.',
siteURL: 'https://fluentui.z5.web.core.windows.net/',
headerLinks: [
{
name: 'Fundamentals',
link: '/fundamentals',
headerOnly: true,
},
{
name: 'Web',
link: '/web',
},
{
name: 'Windows',
link: '/windows',
},
{
name: 'iOS',
link: '/ios',
},
{
name: 'Android',
link: '/android',
},
{
name: 'Mac',
link: '/mac',
},
],
topLinks: [
{ name: 'Get started', link: 'get-started' },
{ name: 'Styles & Theming', link: 'styles' },
{ name: 'Experiences', link: 'experiences' },
{ name: 'Components', link: 'components' },
],
footerLinks: [
{
name: 'Resources',
link: '/resources',
ariaLabel: 'This link will take you to the Resources page',
},
{
name: "What's new",
link: '/whatsnew',
ariaLabel: "This link will take you to the What's new page",
},
{
name: 'GitHub',
link: 'https://github.com/microsoft/fluent-site',
target: '_blank',
ariaLabel: 'This link will take you to the Microsoft Fluent UI GitHub site in a new window.',
},
{
name: 'Privacy & cookies',
link: 'https://privacy.microsoft.com/en-us/privacystatement',
ariaLabel: 'This link will take you to the Microsoft privacy statement.',
},
],
homePageData: {
news: [
{
title: 'Lorem ipsum dolor sit amet, consectet adipiscing elit. Vivamus ut max velit, ut iaculis est. Nullam tincidunt.',
link: '#',
},
{
title: 'Lorem ipsum dolor sit amet, consectet adipiscing elit. Vivamus ut max velit, ut iaculis est. Nullam tincidunt.',
link: '#',
},
{
title: 'Lorem ipsum dolor sit amet, consectet adipiscing elit. Vivamus ut max velit, ut iaculis est. Nullam tincidunt.',
link: '#',
},
{
title: 'Lorem ipsum dolor sit amet, consectet adipiscing elit. Vivamus ut max velit, ut iaculis est. Nullam tincidunt.',
link: '#',
},
],
},
},
plugins: [
`gatsby-plugin-emotion`,
`gatsby-transformer-yaml`,
`gatsby-plugin-react-helmet`,
`gatsby-plugin-typescript`,
'gatsby-plugin-sharp',
'gatsby-transformer-sharp',
`gatsby-plugin-offline`,
{
resolve: `gatsby-plugin-netlify-cms`,
options: {
modulePath: `${__dirname}/src/cms/cms.js`,
},
},
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `Fabric Website 2.0`,
short_name: `fabricwebsite`,
start_url: `/`,
background_color: `#f7f0eb`,
theme_color: `#a2466c`,
display: `standalone`,
},
},
{
resolve: `gatsby-plugin-mdx`,
options: {
defaultLayouts: {
default: require.resolve('./src/templates/MDXTemplate.tsx'),
},
gatsbyRemarkPlugins: [
{
resolve: `gatsby-remark-images`,
options: {
maxWidth: 400,
withWebp: true,
tracedSVG: true,
linkImagesToOriginal: false,
},
},
{
resolve: `gatsby-remark-copy-linked-files`,
},
],
},
},
{
resolve: `gatsby-plugin-docs-creator`,
options: {
path: contentPath,
},
},
],
}
}

Просмотреть файл

@ -0,0 +1,29 @@
const path = require('path')
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
exports.onCreatePage = ({ page, actions }) => {
const { createPage, deletePage } = actions
}
exports.createPages = async ({ actions, graphql }) => {
const { createPage } = actions
}
exports.onCreateWebpackConfig = ({ stage, actions }) => {
if (stage.startsWith('develop')) {
actions.setWebpackConfig({
resolve: {
alias: {
'react-dom': '@hot-loader/react-dom',
},
},
})
}
actions.setWebpackConfig({
plugins: [
new MonacoWebpackPlugin({
languages: ['typescript'],
}),
],
})
}

Просмотреть файл

@ -0,0 +1,20 @@
import { Stylesheet, InjectionMode } from '@uifabric/merge-styles'
import { renderStatic } from '@uifabric/merge-styles/lib/server'
import { renderToString } from 'react-dom/server'
import React from 'react'
const config = require('./gatsby-config')
export const replaceRenderer = ({ bodyComponent, replaceBodyHTMLString, setHeadComponents }) => {
const { html, css } = renderStatic(() => {
return renderToString(bodyComponent)
})
replaceBodyHTMLString(html)
setHeadComponents([<style dangerouslySetInnerHTML={{ __html: css }} />])
}
export const onRenderBody = ({ pathname, setHeadComponents }) => {
setHeadComponents([<link rel="canonical" href={`${config.siteMetadata ? config.siteMetadata.siteUrl : '/'}${pathname}`} />])
}

Просмотреть файл

@ -0,0 +1,79 @@
{
"name": "gatsby-theme-fluent-site",
"version": "0.1.1",
"main": "index.js",
"description": "A Fluent theme for GatsbyJS",
"repository": {
"type": "git",
"url": "https://github.com/microsoft/fluent-site"
},
"license": "MIT",
"scripts": {
"clean": "gatsby clean",
"build": "echo 'building'",
"start": "echo 'starting'",
"test": "echo \"Write tests! -> https://gatsby.app/unit-testing\"",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
},
"devDependencies": {
"@babel/core": "^7.8.3",
"@mdx-js/mdx": "^1.1.0",
"@mdx-js/react": "^1.0.27",
"@storybook/addon-actions": "^5.3.4",
"@storybook/addon-links": "^5.3.4",
"@storybook/addons": "^5.3.4",
"@storybook/react": "^5.3.4",
"@types/graphql": "^14.2.0",
"@types/node": "^11.13.13",
"@types/react": "^16.8.19",
"@types/react-dom": "^16.8.4",
"@types/react-helmet": "^5.0.8",
"babel-loader": "^8.0.6",
"gatsby": "^2.20.10",
"gatsby-image": "^2.2.38",
"gatsby-link": "^2.2.2",
"gatsby-plugin-docs-creator": "^2.1.40",
"gatsby-plugin-manifest": "^2.2.20",
"gatsby-plugin-mdx": "^1.0.67",
"gatsby-plugin-netlify-cms": "^4.2.2",
"gatsby-plugin-offline": "^3.0.32",
"gatsby-plugin-react-helmet": "^3.1.21",
"gatsby-plugin-sharp": "^2.3.13",
"gatsby-plugin-typescript": "^2.1.26",
"gatsby-remark-copy-linked-files": "^2.1.36",
"gatsby-remark-images": "^3.1.42",
"gatsby-remark-prismjs": "^3.3.30",
"gatsby-source-filesystem": "^2.1.46",
"gatsby-source-git": "^1.0.2",
"gatsby-transformer-remark": "^2.6.48",
"gatsby-transformer-sharp": "^2.3.12",
"gatsby-transformer-yaml": "^2.2.24",
"monaco-editor-webpack-plugin": "^1.9.0",
"netlify-cms-app": "^2.12.2",
"office-ui-fabric-react": "^7.92.0",
"prism-react-renderer": "^1.0.1",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-helmet": "^5.2.1",
"react-live": "^2.1.2",
"react-monaco-editor": "^0.34.0",
"remark-parse": "^7.0.0",
"remark-react": "^6.0.0",
"unified": "^8.3.2"
},
"dependencies": {
"@emotion/core": "^10.0.27",
"@emotion/styled": "^10.0.27",
"@hot-loader/react-dom": "^16.12.0",
"@loadable/component": "^5.12.0",
"@microsoft/applicationinsights-react-js": "^2.4.4",
"@microsoft/applicationinsights-web": "^2.4.4",
"@uifabric/api-docs": "^7.2.13",
"@uifabric/example-app-base": "^7.11.16",
"fuse.js": "^3.4.6",
"gatsby-plugin-emotion": "^4.1.22",
"monaco-editor": "^0.20.0",
"typescript": "^3.5.1"
}
}

Просмотреть файл

@ -0,0 +1,6 @@
/**
* The default export of `netlify-cms-app` is an object with all of the Netlify CMS
* extension registration methods, such as `registerWidget` and
* `registerPreviewTemplate`.
*/
import CMS from 'netlify-cms-app'

Просмотреть файл

@ -0,0 +1,65 @@
import React, { useEffect } from 'react'
import { ApplicationInsights } from '@microsoft/applicationinsights-web'
import styled from '@emotion/styled'
declare global {
interface Window {
appInsights: ApplicationInsights
}
}
const StyledAppInsights = styled.div`
display: none;
`
export interface IAppInsightsPageViewProps {
path: string | null
}
export interface IAppInsightsEventViewProps {
eventName: string
eventPropertyName?: string
eventPropertyValue?: any
}
const AppInsightsLoadable = (props: IAppInsightsPageViewProps | IAppInsightsEventViewProps) => {
useEffect(() => {
if (window.appInsights === undefined) {
/* TODO: The key needs to be injected for production vs development */
window.appInsights = new ApplicationInsights({
config: {
instrumentationKey: `${process.env.GATSBY_APPLICATIONINSIGHTS_KEY}`,
enableAutoRouteTracking: true,
/* ...Other Configuration Options... */
},
})
window.appInsights.loadAppInsights()
}
let eventProps = props as IAppInsightsEventViewProps
if (eventProps.eventName !== undefined) {
if (eventProps.eventPropertyName !== undefined) {
window.appInsights.trackEvent({
name: eventProps.eventName,
properties: [eventProps.eventPropertyName] = eventProps.eventPropertyValue,
})
} else {
window.appInsights.trackEvent({ name: eventProps.eventName })
}
} else {
let pageViewProps = props as IAppInsightsPageViewProps
let path = pageViewProps.path ? pageViewProps.path : window.location.pathname
if (path === '/') {
path = document.title
} else {
path = path.replace(/^\//, '')
}
if (path) {
window.appInsights.trackPageView({ name: path })
}
}
}, [])
return <StyledAppInsights />
}
export default AppInsightsLoadable

Просмотреть файл

@ -0,0 +1,36 @@
import { ApplicationInsights, IEventTelemetry } from '@microsoft/applicationinsights-web'
import React from 'react'
interface AppInsightsWrapper {
appInsights: ApplicationInsights
}
declare global {
interface Window {
appInsights: ApplicationInsights
}
}
/**
* Returns an instance of the Application Insights object, undefined, or false(during SSR)
**/
export default function InitAppInsights(): ApplicationInsights | undefined {
const windowGlobal = (typeof window !== 'undefined' && window) as Window
const key = `${process.env.GATSBY_APPLICATIONINSIGHTS_KEY}`
if (windowGlobal && key !== undefined && key !== '') {
if (windowGlobal.appInsights === undefined) {
/* TODO: The key needs to be injected for production vs development */
windowGlobal.appInsights = new ApplicationInsights({
config: {
instrumentationKey: key,
enableAutoRouteTracking: true,
/* ...Other Configuration Options... */
},
})
windowGlobal.appInsights.loadAppInsights()
}
return windowGlobal.appInsights
}
return undefined
}

Просмотреть файл

@ -0,0 +1,3 @@
import useEventTelemetry from './useEventTelemetry'
import usePageViewTelemetry from './usePageViewTelemetry'
export { useEventTelemetry, usePageViewTelemetry }

Просмотреть файл

@ -0,0 +1,33 @@
import { useState, useEffect, Dispatch, SetStateAction } from 'react'
import { IEventTelemetry } from '@microsoft/applicationinsights-web'
import InitAppInsights from './InitAppInsights'
//#region IEventTelemetry Hook
/**
* Returns a stateful value of a IEventTelemetry object and a function to invoke the telemetry call
* @param {IEventTelemetry} data IEventTelemetry Object
* @returns [IEventTelemetry, Function to invoke the call]
**/
export default function usePageViewTelemetry(
data: IEventTelemetry | undefined
): [IEventTelemetry, Dispatch<SetStateAction<IEventTelemetry | undefined>>] {
data = data || ({} as IEventTelemetry)
const [telementryData, setTelementryData] = useState(data)
const invoke = (newData: IEventTelemetry | undefined) => {
if (newData !== undefined) {
setTelementryData(newData)
}
newData = newData || telementryData
if (newData !== undefined) {
let appInsights = InitAppInsights()
if (appInsights) {
appInsights.trackEvent(newData)
}
}
}
return [telementryData, invoke]
}
//#endregion

Просмотреть файл

@ -0,0 +1,41 @@
import { useState, useEffect, Dispatch, SetStateAction } from 'react'
import { IPageViewTelemetry } from '@microsoft/applicationinsights-web'
import InitAppInsights from './InitAppInsights'
//#region usePageViewTelemetry Hook
/**
* Returns a stateful value of a IPageViewTelemetry object and a function to update the telemetry value
* @param {IPageViewTelemetry} data IPageViewTelemetry Object
* @returns [IPageViewTelemetry, Function to update]
**/
export default function usePageViewTelemetry(
data: IPageViewTelemetry | undefined
): [IPageViewTelemetry | undefined, Dispatch<SetStateAction<IPageViewTelemetry | undefined>>] {
// Send the data immediately (OnComponentMount) if instantiated with data
const [telementryData, setTelementryData] = useState(data)
useEffect(() => {
if (typeof window !== undefined && window && telementryData !== undefined) {
let appInsights = InitAppInsights()
if (appInsights !== undefined) {
let path = telementryData.name
if (path === undefined) {
path = window.location.pathname
if (path === '/') {
path = document.title
} else {
path = path.replace(/^\//, '')
}
telementryData.name = path
setTelementryData(telementryData)
}
appInsights.trackPageView(telementryData)
}
}
}, [])
return [telementryData, setTelementryData]
}
//#endregion

Просмотреть файл

@ -0,0 +1,38 @@
# A WIP guide to contributing components
> Just gathering some thoughts and notes. These are not final, may change with additional clarity, and are open for discussion. - Scott
## Notes for components:
1. The UI for the docs site should be built using Fluent UI. Seems like table stakes.
2. Initially, we will need to scaffold out the site using components built with JSX tags.
3. Some level of run-time theming will need to be supported.
4. Theming should leverage the same Fluent UI base styles.
- Possible themes include high contrast, dark mode, etc.
## Component questions / thoughts:
1. Can we use the Fluent UI components directly in code or do we need a proxy component to make global changes easier?
> I am leaning on using the Fluent UI components directly since we will be custom-building a version of it for the docs site. - Scott
2. Does overall site theming affect the theme that is shown on any given component?
3. Is global / local theming a setting somewhere?
4. How are design-time customizations exposed for use in the site?
---
## Notes for CSS variables:
- There seems to be a logical divide between theme styles that are static and dynamic.
- Some properties need to be changed at runtime. This includes color themes, density, and others.
- Some properties only need to be changed at design time. This includes most other aspects of the design system: ramps (type, spacing, color), elevation, etc.
- Fallback values will need to provided in a way that allows legacy browser to style things correctly.
- Fallbacks can use the `var(--bg-color, white)` syntax.
- Or they can be made more robust w/ graceful degredation by declaring legacy properties alongside variable properties.
- When leveraging CSS variables, media queries are used to change the value of custom properties.
- This separates out the styling blocks from the layout logic.
- It might make sense to use special casing for global CSS variables, e.g. `--PAGE-BG-COLOR` to more easily differentiate them from local variables.
## CSS variables questions / thoughts:
1. How do we reconcile runtime styling and design time styling?
- The site will need some flexibility that the compiled library wont need. For example, the theme editor with need to change design-time properties on the fly, but implementations of Fluent UI will not introduce these aspects as runtime properties.

Просмотреть файл

@ -0,0 +1,13 @@
import * as React from 'react'
import styled from '@emotion/styled'
export const PageInnerContent = props => {
return <StyledContent>{props.children}</StyledContent>
}
const StyledContent = styled.main`
border-left: 1px solid #eee;
padding: 40px;
overflow-y: auto;
flex-grow: 1;
`

Просмотреть файл

@ -0,0 +1 @@
export { PageInnerContent } from './PageInnerContent'

Просмотреть файл

@ -0,0 +1,37 @@
import * as React from 'react'
import { useStaticQuery, graphql } from 'gatsby'
import styled from '@emotion/styled'
import { FooterMenuItems } from './FooterMenuItems'
export const Footer = props => {
const data = useStaticQuery(graphql`
query FooterMenuQuery {
site {
siteMetadata {
footerLinks {
name
link
target
ariaLabel
}
}
}
}
`)
const {
site: { siteMetadata },
} = data
return (
<StyledFooter>
<FooterMenuItems {...siteMetadata} />
</StyledFooter>
)
}
const StyledFooter = styled.div`
display: flex;
margin: 0 auto;
padding: 40px 40px 40px 20px;
border-top: 1px solid #eee;
`

Просмотреть файл

@ -0,0 +1,62 @@
import * as React from 'react'
import styled from '@emotion/styled'
import { Link } from 'gatsby'
export const FooterMenuItems = props => {
return (
<StyledMenuItems>
{props.footerLinks.map((link, idx) => (
<MenuItem {...link} key={'footerItem_' + idx} />
))}
</StyledMenuItems>
)
}
const MenuItem = props => {
return (
<StyledMenuItem key={props.name}>
{props.link.startsWith('http') ? (
<a
href={props.link}
target={props.target !== undefined ? props.target : undefined}
{...{ 'aria-label': props.ariaLabel ? props.ariaLabel : undefined }}
>
{props.name}
</a>
) : (
<Link
activeClassName="Link-IsActive"
to={props.link}
target={props.target ? props.target : undefined}
{...{ 'aria-label': props.ariaLabel ? props.ariaLabel : undefined }}
>
{props.name}
</Link>
)}
</StyledMenuItem>
)
}
const StyledMenuItems = styled.ul`
display: flex;
padding: 0;
`
const StyledMenuItem = styled.li`
list-style: none;
margin: auto 10px;
a {
color: #000;
opacity: 0.7;
padding: 0 2px;
text-decoration: none;
&.Link-IsActive {
border-bottom: 3px solid #000;
padding-bottom: 20px;
font-weight: 600;
opacity: 1;
}
}
`

Просмотреть файл

@ -0,0 +1 @@
export { Footer } from './Footer'

Просмотреть файл

@ -0,0 +1,85 @@
import * as React from 'react'
import styled from '@emotion/styled'
import { useStaticQuery, graphql, Link } from 'gatsby'
import { HeaderMenuItems } from '.'
import { Search } from '../Search'
import { SubNav } from './SubNav'
import { usePageContext } from '../Provider'
export const Header = props => {
const pageContext = usePageContext()
const data = useStaticQuery(graphql`
query MainMenuQuery {
site {
siteMetadata {
title
headerLinks {
name
link
}
topLinks {
link
name
}
}
}
}
`)
const {
pathContext: { rootPath },
location: { pathname },
} = pageContext
const {
site: { siteMetadata },
} = data
const topLinks = siteMetadata.topLinks.map(item => {
return {
name: item.name,
link: '/' + rootPath + '/' + item.link,
}
})
return (
<StyledHeader>
<Nav>
<Logo to="/">
<img src={require('gatsby-theme-fluent-site/static/images/microsoft.svg')} alt="Microsoft Logo" />
<p>Fluent</p>
</Logo>
<HeaderMenuItems {...siteMetadata} />
<Search />
</Nav>
{rootPath && rootPath !== 'fundamentals' && <SubNav topLinks={topLinks} />}
</StyledHeader>
)
}
const StyledHeader = styled.div``
const Nav = styled.header`
width: 100%;
padding: 20px 40px;
display: flex;
justify-content: space-between;
border-bottom: 1px solid #eee;
`
const Logo = styled(Link)`
display: flex;
color: #000;
text-decoration: none;
img {
width: 22px;
}
p {
margin: 12px;
font-weight: 600;
}
`

Просмотреть файл

@ -0,0 +1,63 @@
import * as React from 'react'
import styled from '@emotion/styled'
import { Link } from 'gatsby'
const MenuItem = props => {
return (
<StyledMenuItem key={props.name}>
<Link activeClassName="Link-IsActive" to={props.link}>
{props.name}
</Link>
</StyledMenuItem>
)
}
export const HeaderMenuItems = props => {
return (
<StyledMenuItems>
{props.headerLinks.map((link, i) => (
<MenuItem key={i} {...link} />
))}
</StyledMenuItems>
)
}
const StyledMenuItems = styled.ul`
display: flex;
padding: 0;
`
const StyledMenuItem = styled.li`
list-style: none;
margin: auto 10px;
&:first-of-type {
border-right: 1px solid #eee;
padding-right: 20px;
}
a {
color: #666;
padding: 0 2px;
font-weight: 500;
text-decoration: none;
&.Link-IsActive {
color: #000;
font-weight: 500;
position: relative;
&:after {
content: '';
position: absolute;
bottom: -35px;
left: calc(50% - 10px);
width: 0;
height: 0;
border-style: solid;
border-width: 0 10px 10px 10px;
border-color: transparent transparent #eeeeee transparent;
}
}
}
`

Просмотреть файл

@ -0,0 +1,54 @@
import * as React from 'react'
import styled from '@emotion/styled'
import { Link } from 'gatsby'
export const SubNav = props => {
const menuItems = props.topLinks.map((item, index) => {
return (
<li key={index}>
<Link partiallyActive={true} activeClassName="Link-IsActive" to={item.link}>
{item.name}
</Link>
</li>
)
})
return (
<StyledSubNav>
<StyledList>{menuItems}</StyledList>
</StyledSubNav>
)
}
const StyledSubNav = styled.div`
background-color: #eee;
margin: 0px;
p {
margin: 0;
padding: 40px;
opacity: 0.4;
}
`
const StyledList = styled.ul`
display: flex;
padding: 30px 30px 30px 20px;
max-width: 600px;
list-style: none;
li {
list-style: none;
margin: 0 20px;
a {
color: #666;
font-weight: 500;
text-decoration: none;
&.Link-IsActive {
color: #000;
}
}
}
`

Просмотреть файл

@ -0,0 +1,2 @@
export { Header } from './Header'
export { HeaderMenuItems } from './HeaderMenuItems'

Просмотреть файл

@ -0,0 +1,21 @@
import * as React from 'react'
import Highlight, { defaultProps } from 'prism-react-renderer'
import darkTheme from 'prism-react-renderer/themes/nightOwl'
export const HighlightHOC = p => {
return (
<Highlight {...defaultProps} theme={darkTheme} code={p.children} language="jsx">
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<div className={className} style={style}>
{tokens.map((line, i) => (
<div {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</div>
))}
</div>
)}
</Highlight>
)
}

Просмотреть файл

@ -0,0 +1,21 @@
import * as React from 'react'
import Highlight, { defaultProps } from 'prism-react-renderer'
import darkTheme from 'prism-react-renderer/themes/nightOwl'
export const HighlightInlineHOC = p => {
return (
<Highlight {...defaultProps} theme={darkTheme} code={p.children} language="jsx">
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<code className={className} style={style}>
{tokens.map((line, i) => (
<span {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span {...getTokenProps({ token, key })} />
))}
</span>
))}
</code>
)}
</Highlight>
)
}

Просмотреть файл

@ -0,0 +1,2 @@
export { HighlightInlineHOC as HighlightInline } from './HighlightInline'
export { HighlightHOC as Highlight } from './Highlight'

Просмотреть файл

@ -0,0 +1,42 @@
import * as React from 'react'
import { css } from '@emotion/core'
interface NYIProps {
children?: React.ReactNode
}
export const NYI = (props: NYIProps) => (
<>
<abbr
css={css`
display: inline-block;
background-color: lightyellow;
padding: 1px 8px 1px 8px;
border: 1px solid orange;
border-radius: 2px;
color: black;
font-size: 11px;
font-weight: 600;
text-decoration: none;
cursor: default;
user-select: none;
`}
title="Not Yet Implemented"
>
NYI
</abbr>
{props.children && (
<span
css={css`
color: gray;
font-style: italic;
`}
>
{props.children}
</span>
)}
</>
)

Просмотреть файл

@ -0,0 +1,28 @@
import * as React from 'react'
import { css } from '@emotion/core'
interface PlaceholderProps {
children?: React.ReactNode
}
export const Placeholder = (props: PlaceholderProps) => (
<div
css={css`
display: flex;
position: relative;
height: 360px;
color: #808080;
background-color: #f3f2f1;
`}
>
<div
css={css`
display: block;
margin: auto;
`}
>
{props.children}
</div>
</div>
)

Просмотреть файл

@ -0,0 +1,2 @@
export { NYI } from './NYI'
export { Placeholder } from './Placeholder'

Просмотреть файл

@ -0,0 +1,14 @@
import * as React from 'react'
import styled from '@emotion/styled'
export const Persona = () => {
return <StyledPersona>{/* Beep boop */}</StyledPersona>
}
const StyledPersona = styled.div`
width: 32px;
height: 32px;
margin: 8px;
background: #ccc;
border-radius: 100%;
`

Просмотреть файл

Просмотреть файл

@ -0,0 +1,120 @@
import * as React from 'react'
import { IFrameRenderer } from './IFrameRenderer'
import { usePlayground, IExample } from './context'
export const ExamplePreview = ({ example }: { example: IExample }) => {
const [Component, error] = useTranspiledComponent(example.source)
const [playground] = usePlayground()
return (
<IFrameRenderer>
{ctx => {
if (error) return null // TODO: better UX
if (!Component) return null
return (
<ExampleRenderer>
<Component document={ctx.document} theme={playground.currentTheme} rtl={playground.rtl} />
</ExampleRenderer>
)
}}
</IFrameRenderer>
)
}
const ExampleRenderer = ({ children }) => {
const [playground] = usePlayground()
const style: any = {
direction: playground.rtl ? 'rtl' : 'ltr',
zoom: playground.zoomLevel,
}
if (playground.resolution !== 'Responsive') {
style.width = playground.resolution
}
return (
<div
style={{
maxWidth: '100%',
maxHeight: '100%',
overflow: 'auto',
}}
>
<div style={style}>{children}</div>
</div>
)
}
/**
* Transpiles source code into a React component. Expects the source code to
* expose the component as a default export.
*
* @example
* ```tsx
* const Component = useTranspiledComponent(`
* import * as React from "react"
*
* export default () => {
* return <h1>Hello World</h1>
* }
* `)
*
* // Component = () => React.createElement("h1", null, "Hello World")
* ```
*/
function useTranspiledComponent(source: string): [React.ComponentType<any> | undefined, Error | undefined] {
const ts = useLazyTypeScript()
// TypeScript has not loaded, do nothing.
if (!ts) return [undefined, undefined]
// TODO: proper import handling... Consider TS VFS.
source = source.replace('import * as React from "react"', '')
source = source.replace('export default', 'return')
try {
source = ts.transpileModule(source, {
compilerOptions: {
module: 'none',
jsx: 'react',
},
}).outputText
// TODO: should this be sandboxed in some way?
window.React = React // TODO: properly bind React into component scope.
const Component = new Function(source)()
return [
// Dumb wrapper around user-defined component to handle errors/undefined return value.
// TODO: proper UX for errors
props => {
try {
return Component(props) || null
} catch (e) {
return null
}
},
undefined,
]
} catch (e) {
return [undefined, e]
}
}
/**
* Lazy loads TypeScript for in-browser transpilation
*/
function useLazyTypeScript() {
const [ts, setTS] = React.useState()
React.useEffect(() => {
let cancelled = false
import('typescript').then(res => {
if (!cancelled) {
setTS(res.default)
}
})
return () => {
cancelled = true
}
}, [])
return ts
}

Просмотреть файл

@ -0,0 +1,217 @@
import * as React from 'react'
import styled from '@emotion/styled'
import CodeEditor from './plugins/CodeEditor'
import ThemeEditor from './plugins/ThemeEditor'
import { usePlayground } from './context'
const PLUGINS = [CodeEditor, ThemeEditor]
export const Footer = () => {
return (
<StyledFooter>
<StyledFooterBanner>
<PluginList />
<StyledGrid>
<RTLToggle />
<ThemeSelector />
</StyledGrid>
</StyledFooterBanner>
<ActivePluginOutlet />
</StyledFooter>
)
}
const PluginList = () => {
const [playground, dispatch] = usePlayground()
return (
<StyledPluginList>
{PLUGINS.map(plugin => {
const active = plugin === playground.currentPlugin
return (
<li
key={plugin.label}
data-is-active={active}
onClick={() => {
dispatch({ type: 'TOGGLE_PLUGIN', payload: plugin })
}}
>
{plugin.icon}
{plugin.label}
</li>
)
})}
</StyledPluginList>
)
}
const RTLToggle = () => {
const [playground, dispatch] = usePlayground()
return (
<StyledInputLabel htmlFor="rtl">
<span className="label">RTL</span>
<StyledToggleSlider>
<input
id="rtl"
type="checkbox"
checked={playground.rtl}
onChange={() => {
dispatch({ type: 'TOGGLE_RTL' })
}}
/>
<span className="slider" />
</StyledToggleSlider>
</StyledInputLabel>
)
}
const ThemeSelector = () => {
const [playground, dispatch] = usePlayground()
return (
<StyledInputLabel htmlFor="theme">
<span className="label">Theme</span>
<StyledSelect
id="theme"
value={playground.currentTheme}
onChange={e => {
dispatch({ type: 'CHANGE_THEME', payload: e.target.value })
}}
>
{playground.themes.map(theme => {
return (
<option key={theme} value={theme}>
{theme}
</option>
)
})}
</StyledSelect>
</StyledInputLabel>
)
}
const ActivePluginOutlet = () => {
const [playground] = usePlayground()
const plugin = playground.currentPlugin
if (!plugin) return null
return <StyledPluginOutlet>{plugin.render()}</StyledPluginOutlet>
}
const StyledFooter = styled.footer``
const StyledFooterBanner = styled.div`
display: flex;
justify-content: space-between;
padding: 0.75rem 1rem;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
`
const StyledGrid = styled.div`
display: grid;
grid-auto-flow: column;
grid-auto-columns: min-content;
align-items: center;
grid-gap: 1rem;
`
const StyledSelect = styled.select`
padding: 0;
line-height: inherit;
color: inherit;
`
const StyledPluginList = styled.ol`
list-style: none;
padding: 0;
display: grid;
grid-auto-flow: column;
grid-auto-columns: min-content;
align-items: center;
grid-gap: 1rem;
> li {
display: flex;
position: relative;
align-items: center;
white-space: nowrap;
border-bottom: 2px solid transparent;
svg {
margin-right: 0.5rem;
}
&:hover,
&:focus,
&[data-is-active='true'] {
cursor: pointer;
color: rgba(62, 66, 192, 1);
&::after {
content: '';
position: absolute;
width: 100%;
bottom: -19px;
left: 0;
height: 2px;
background: rgba(62, 66, 192, 1);
box-shadow: inset 0 0 1px 0 rgb(62, 66, 192), 0 0 1px 0 rgba(62, 66, 192, 0.1), 0 0 15px 0 rgba(128, 131, 216, 0.4),
0 2px 6px 0 rgba(111, 115, 247, 0.5), 0 2px 2px 0 rgba(104, 68, 207, 0.2);
}
}
}
`
const StyledPluginOutlet = styled.div`
padding: 1rem;
`
const StyledInputLabel = styled.label`
display: flex;
align-items: center;
cursor: pointer;
> .label {
margin-right: 0.5rem;
}
`
const StyledToggleSlider = styled.span`
position: relative;
height: 18px;
width: 40px;
input {
display: none;
&:checked + .slider {
background: #66bb6a;
}
&:checked + .slider::before {
transform: translateX(21px);
}
}
.slider {
position: absolute;
background-color: #ccc;
bottom: 0;
left: 0;
right: 0;
top: 0;
transition: 200ms;
border-radius: 1rem;
&::before {
background-color: #fff;
bottom: 0px;
content: '';
height: 16px;
width: 16px;
left: 1px;
position: absolute;
transition: 200ms;
border: 1px solid #bbb;
border-radius: 100%;
}
}
`

Просмотреть файл

@ -0,0 +1,102 @@
import * as React from 'react'
import * as ReactDOM from 'react-dom'
/**
* Renders React children into an iframe.
*
* TODO: convert to hooks.
*/
export class IFrameRenderer extends React.Component {
node: any
_setInitialContent = false
_mounted = false
_rendered = false
componentDidMount() {
this._mounted = true
const doc = this.getDoc()
if (doc && doc.readyState === 'complete') {
this.forceUpdate()
} else {
this.node.addEventListener('load', this.handleLoad)
}
}
componentWillUnmount() {
this._mounted = false
this.node.removeEventListener('load', this.handleLoad)
}
getDoc() {
return this.node && this.node.contentDocument
}
getMountTarget() {
const doc = this.getDoc()
return doc.getElementById('frame-root')
}
handleLoad = () => {
this.forceUpdate()
}
renderIFrameContents() {
if (!this._mounted) {
return null
}
const doc = this.getDoc()
if (!doc) {
return null
}
if (!this._setInitialContent) {
const styles = `
html, body, #frame-root {
margin: 0;
height: 100vh;
width: 100%;
}
#frame-root {
display: flex;
align-items: center;
justify-content: center;
}
`
doc.write(`<!DOCTYPE html><html><head><style>${styles}</style></head><body><div id="frame-root"></div></body></html>`)
doc.close()
this._setInitialContent = true
}
const mountTarget = this.getMountTarget()
const win = doc.defaultView || doc.parentView
// Do not allow elements to be focused within the iframe
win.HTMLElement.prototype.focus = () => {}
const ctx = { window: win, document: doc }
const content = this.props.children(ctx) || null
return ReactDOM.createPortal(content, mountTarget)
}
render() {
const { children, ...rest } = this.props
return (
<iframe
title="Example Renderer"
seamless
ref={node => (this.node = node)}
style={{
height: '100%',
width: '100%',
border: 'none',
}}
{...rest}
>
{this.renderIFrameContents()}
</iframe>
)
}
}

Просмотреть файл

@ -0,0 +1,89 @@
import React from 'react'
import { Playground } from '.'
import { Global, css } from '@emotion/core'
export default {
title: 'Playground',
component: Playground,
}
// TODO: should share this with the main app and other stories.
const GlobalStyles = () => (
<Global
styles={css`
body {
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol';
}
`}
/>
)
export const ToStorybook = () => {
const examples = [
{
title: 'Hello World',
description: 'Description for the first example',
source: `import * as React from "react"
export default () => <h1>Hello World!</h1>`,
},
{
title: 'Goodbye World',
description: 'Description for the second example',
source: `import * as React from "react"
export default () => <h1>Goodbye World?</h1>`,
},
{
title: 'RTL Example',
description: 'Description for the third example',
source: `import * as React from "react"
export default () => (
<div style={{
display: "grid",
gridAutoFlow: "column",
gridAutoColumns: "min-content",
gridGap: "1rem",
}}>
<button style={{ padding: "1rem", fontWeight: 800, background: "red", color: "#fff" }}>Cancel</button>
<button style={{ padding: "1rem", fontWeight: 800, background: "blue", color: "#fff" }}>Confirm</button>
</div>
)`,
},
{
title: 'Theme Example',
description: 'Description for the third example',
source: `import * as React from "react"
export default ({ theme }) => {
const style = {
padding: "1rem"
}
switch (theme) {
case "Dark":
style.background = "#333"
style.color = "#eee"
break
case "High Contrast":
style.background = "#000"
style.color = "#fff"
break
}
return <h1 style={style}>Current theme: {theme}</h1>
}`,
},
]
const themes = ['Light', 'Dark', 'High Contrast']
return (
<>
<GlobalStyles />
<Playground themes={themes} examples={examples} />
</>
)
}
ToStorybook.story = {
name: 'Basic',
}

Просмотреть файл

@ -0,0 +1,66 @@
import * as React from 'react'
import styled from '@emotion/styled'
import { PlaygroundProvider, IPlayground, IExample } from './context'
import { Sidebar } from './Sidebar'
import { Viewport } from './Viewport'
import { Footer } from './Footer'
function handleAction(state: IPlayground, action: any): IPlayground {
switch (action.type) {
case 'TOGGLE_RTL':
return { ...state, rtl: !state.rtl }
case 'CHANGE_THEME':
return { ...state, currentTheme: action.payload }
case 'CHANGE_EXAMPLE':
return { ...state, currentExample: action.payload }
case 'CHANGE_RESOLUTION':
return { ...state, resolution: action.payload }
case 'CHANGE_ZOOM_LEVEL':
return { ...state, zoomLevel: action.payload }
case 'CHANGE_CURRENT_EXAMPLE_SOURCE':
return { ...state, currentExample: { ...state.currentExample, source: action.payload } }
case 'TOGGLE_PLUGIN':
return { ...state, currentPlugin: state.currentPlugin === action.payload ? null : action.payload }
default:
console.warn('Missing handler for action: %s', action.type)
return state
}
}
export const Playground = ({ examples = [], themes = [] }: { examples: IExample[]; themes: string[] }) => {
// TODO: ensure state stays in sync with changes to examples/themes props.
const playground = React.useReducer(handleAction, {
examples,
themes,
rtl: false,
zoomLevel: 1,
resolution: 'Responsive',
currentExample: examples[0],
currentTheme: themes[0],
currentPlugin: null,
})
return (
<PlaygroundProvider value={playground}>
<StyledPlayground>
<StyledPlaygroundBody>
<Viewport />
<Sidebar />
</StyledPlaygroundBody>
<Footer />
</StyledPlayground>
</PlaygroundProvider>
)
}
const StyledPlayground = styled.div`
border: 1px solid #eee;
border-radius: 3px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
`
const StyledPlaygroundBody = styled.div`
position: relative;
display: flex;
overflow: hidden;
`

Просмотреть файл

@ -0,0 +1,129 @@
import * as React from 'react'
import styled from '@emotion/styled'
import { usePlayground } from './context'
export const Sidebar = () => {
const [show, setShow] = React.useState(true)
return (
<>
<StyledSidebar show={show}>
<ExampleList />
</StyledSidebar>
<ToggleSidebar onToggle={() => setShow(!show)} />
</>
)
}
const ExampleList = () => {
const [playground, dispatch] = usePlayground()
return (
<SidebarPanel header="Examples">
<StyledExampleList>
{playground.examples.map(example => {
const active = example.title === playground.currentExample.title
// TODO: should probably render an actual button or anchor inside of li
return (
<StyledExampleListItem
key={example.title}
active={active}
onClick={() => {
dispatch({ type: 'CHANGE_EXAMPLE', payload: example })
}}
>
{example.title}
</StyledExampleListItem>
)
})}
</StyledExampleList>
</SidebarPanel>
)
}
const ToggleSidebar = ({ onToggle }) => {
return (
<StyledToggleButton title="Toggle sidebar" onClick={onToggle}>
<HamburgerIcon />
</StyledToggleButton>
)
}
const StyledExampleList = styled.ul`
margin: 0;
padding: 0;
list-style: none;
`
const StyledExampleListItem = styled.li<{ active: boolean }>`
padding: 0.5rem;
cursor: pointer;
border: 1px solid transparent;
border-bottom-color: #eee;
font-size: 0.9rem;
&:hover,
&:focus {
border-color: rgb(62, 66, 192);
}
${props =>
props.active && {
background: 'rgba(62, 66, 192, 0.035)',
color: 'rgba(62, 66, 192, 1)',
borderColor: 'rgba(62, 66, 192, 1)',
}}
`
const StyledSidebar = styled.aside<{ show: boolean }>`
display: flex;
flex-direction: column;
width: 225px;
margin-left: ${props => (props.show ? 0 : '-226px')};
border-left: 1px solid #eee;
background: #fff;
transition: margin 150ms ease 0s;
`
export const SidebarPanel = ({ header, children }: { children: React.ReactNode; header: React.ReactNode }) => {
const [show, setShow] = React.useState(true)
return (
<StyledSidebarPanel>
<StyledSidebarPanelHeader onClick={() => setShow(!show)}>{header}</StyledSidebarPanelHeader>
{show && <StyledSidebarPanelContent>{children}</StyledSidebarPanelContent>}
</StyledSidebarPanel>
)
}
const StyledSidebarPanel = styled.section``
const StyledSidebarPanelHeader = styled.header`
padding: 0.75rem;
border-bottom: 1px solid #eee;
cursor: pointer;
font-weight: 600;
`
const StyledSidebarPanelContent = styled.div`
background: rgb(249, 249, 249);
`
const HamburgerIcon = () => (
<span role="img" aria-hidden="true">
<svg role="presentation" focusable="false" height="16" width="16" viewBox="8 8 16 16">
<path d="M22.49 10.47c0 .14-.05.25-.14.35s-.21.14-.35.14H9c-.14 0-.25-.05-.35-.14s-.14-.21-.14-.35.05-.25.14-.35.21-.14.35-.14h13c.14 0 .25.05.35.14s.14.21.14.35zm0 5c0 .14-.05.25-.14.35s-.21.14-.35.14H9c-.14 0-.25-.05-.35-.14s-.14-.21-.14-.35.05-.25.14-.35.21-.14.35-.14h13c.14 0 .25.05.35.14s.14.21.14.35zm0 5c0 .14-.05.25-.14.35s-.21.14-.35.14H9c-.14 0-.25-.05-.35-.14s-.14-.21-.14-.35.05-.25.14-.35.21-.14.35-.14h13c.14 0 .25.05.35.14s.14.21.14.35z"></path>
<path d="M9 11h13c.6 0 1-.4 1-1s-.4-1-1-1H9c-.6 0-1 .4-1 1s.4 1 1 1zm13 8H9c-.6 0-1 .4-1 1s.4 1 1 1h13c.6 0 1-.4 1-1s-.4-1-1-1zm0-5H9c-.6 0-1 .4-1 1s.4 1 1 1h13c.6 0 1-.4 1-1s-.4-1-1-1z"></path>
</svg>
</span>
)
const StyledToggleButton = styled.button`
position: absolute;
top: 0;
right: 0;
margin: 15px 15px 0 0;
padding: 0;
background: none;
border: none;
outline: none;
cursor: pointer;
z-index: 2;
`

Просмотреть файл

@ -0,0 +1,116 @@
import * as React from 'react'
import styled from '@emotion/styled'
import { usePlayground } from './context'
import { ExamplePreview } from './ExamplePreview'
const RESOLUTIONS = [
{ label: 'Responsive', value: 'Responsive' },
{ label: '320', value: 320 },
{ label: '640', value: 640 },
{ label: '1024', value: 1024 },
]
const ZOOM_LEVELS = [
{ label: '25%', value: 0.25 },
{ label: '50%', value: 0.5 },
{ label: '75%', value: 0.75 },
{ label: '100%', value: 1 },
{ label: '200%', value: 2 },
{ label: '300%', value: 3 },
]
export const Viewport = () => {
const [playground, dispatch] = usePlayground()
return (
<StyledViewport>
<Grid style={{ zIndex: 1, padding: '1rem' }}>
<Select
label="Resolution"
options={RESOLUTIONS}
value={playground.resolution}
onChange={(value: any) => dispatch({ type: 'CHANGE_RESOLUTION', payload: value })}
/>
<Select
label="Zoom Level"
options={ZOOM_LEVELS}
value={playground.zoomLevel}
onChange={(value: any) => dispatch({ type: 'CHANGE_ZOOM_LEVEL', payload: value })}
/>
</Grid>
<StyledExamplePreviewContainer>
<ExamplePreview example={playground.currentExample} />
</StyledExamplePreviewContainer>
{playground.currentExample && (
<div style={{ position: 'relative', padding: '1rem' }}>
<StyledExampleTitle>{playground.currentExample.title}</StyledExampleTitle>
<StyledExampleDescription>{playground.currentExample.description}</StyledExampleDescription>
</div>
)}
</StyledViewport>
)
}
const Select = ({ label, options, value, onChange }) => {
const selected = options.find(opt => opt.value === value)
return (
<label htmlFor={label} style={{ display: 'flex', alignItems: 'center' }}>
<StyledSelect
id={label}
value={selected.label}
onChange={e => {
const option = options.find(opt => opt.label === e.target.value)!
onChange(option.value)
}}
>
{options.map(opt => {
return (
<option key={opt.label} value={opt.label}>
{opt.label}
</option>
)
})}
</StyledSelect>
</label>
)
}
const StyledViewport = styled.div`
position: relative;
display: flex;
flex: 1 1 0%;
flex-direction: column;
justify-content: space-between;
min-height: 375px;
background: rgb(249, 249, 249);
`
const StyledSelect = styled.select`
padding: 0;
line-height: inherit;
color: inherit;
`
// NOTE: this must take up 100% of the viewport height, since the example
// may render with a custom background. In that case, that background should
// appear behind the viewport's header and footer.
const StyledExamplePreviewContainer = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
`
const StyledExampleTitle = styled.h3`
margin: 0;
`
const StyledExampleDescription = styled.p`
margin: 0.5rem 0;
`
const Grid = styled.div`
display: grid;
grid-auto-flow: column;
grid-auto-columns: min-content;
align-items: center;
grid-gap: 7.5px;
`

Просмотреть файл

@ -0,0 +1,24 @@
import * as React from 'react'
const PlaygroundContext = React.createContext<IPlaygroundContext>(null as any)
PlaygroundContext.displayName = 'PlaygroundContext'
export const PlaygroundProvider = PlaygroundContext.Provider
export const usePlayground = () => React.useContext(PlaygroundContext)
export type IPlaygroundContext = [IPlayground, React.Dispatch<any>]
export interface IPlayground {
examples: IExample[]
themes: string[]
currentTheme: string
currentExample: IExample
currentPlugin: any
rtl: boolean
zoomLevel: number
resolution: number | 'Responsive'
}
export interface IExample {
title: string
description: string
source: string
}

Просмотреть файл

@ -0,0 +1 @@
export { Playground } from './Playground'

Просмотреть файл

@ -0,0 +1,63 @@
import * as React from 'react'
import styled from '@emotion/styled'
import { usePlayground } from '../context'
// TODO: monaco is throwing errors on source change. Dig into
// react-monaco-editor; potentially fork (it's a very slim wrapper)
const CodeEditor = () => {
const MonacoEditor = useLazyMonacoEditor()
const [playground, dispatch] = usePlayground()
if (!MonacoEditor) {
return <span>Loading...</span>
}
const example = playground.currentExample
return (
<MonacoEditor
language="typescript"
value={example.source}
height={300}
onChange={(value: string) => {
dispatch({ type: 'CHANGE_CURRENT_EXAMPLE_SOURCE', payload: value })
}}
/>
)
}
function useLazyMonacoEditor() {
const [monaco, setMonaco] = React.useState()
React.useEffect(() => {
let cancelled = false
Promise.all([import('monaco-editor'), import('react-monaco-editor')]).then(([monaco, MonacoEditor]) => {
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
jsx: monaco.languages.typescript.JsxEmit.React,
})
if (!cancelled) {
setMonaco(() => MonacoEditor.default)
}
})
return () => {
cancelled = true
}
}, [])
return monaco
}
const StyledSVG = styled.svg`
fill: currentColor;
height: 16px;
width: 16px;
`
export default {
label: 'Code Editor',
icon: (
<StyledSVG role="presentation" focusable="false" viewBox="8 8 16 16">
<path d="M20 20.5a.993.993 0 0 1-.65-.241.997.997 0 0 1-.108-1.409L21.683 16l-2.441-2.849a.999.999 0 1 1 1.517-1.302l3 3.5a1 1 0 0 1 0 1.301l-3 3.5a.993.993 0 0 1-.759.35zM12 20.5a.995.995 0 0 1-.76-.35l-3-3.5a1 1 0 0 1 0-1.301l3-3.5a1 1 0 0 1 1.518 1.302L10.317 16l2.442 2.85A1 1 0 0 1 12 20.5zM14.251 23.5a.5.5 0 0 1-.482-.638l4-14a.499.499 0 1 1 .961.274l-4 14a.498.498 0 0 1-.479.364z"></path>
</StyledSVG>
),
render(props: any) {
return <CodeEditor {...props} />
},
}

Просмотреть файл

@ -0,0 +1,24 @@
import * as React from 'react'
import styled from '@emotion/styled'
const ThemeEditor = () => {
return <p>Placeholder</p>
}
const StyledSVG = styled.svg`
fill: currentColor;
height: 16px;
width: 16px;
`
export default {
label: 'Theme Editor',
icon: (
<StyledSVG role="presentation" focusable="false" viewBox="8 8 16 16">
<path d="M23.5,9C23.2239,9,23,9.2236,23,9.5v2c0,0.2759-0.2244,0.5-0.5,0.5H20h-8H9.5C9.2244,12,9,11.7759,9,11.5v-2 C9,9.2236,8.7761,9,8.5,9S8,9.2236,8,9.5v2C8,12.3271,8.6729,13,9.5,13H11v3.5c0,0.8271,0.6729,1.5,1.5,1.5H13v5.5 c0,0.1802,0.0969,0.3462,0.2537,0.4351C13.3301,23.9785,13.415,24,13.5,24c0.0891,0,0.1782-0.0239,0.2573-0.0713l5-3 C18.908,20.8384,19,20.6758,19,20.5V18h0.5c0.8271,0,1.5-0.6729,1.5-1.5V13h1.5c0.8271,0,1.5-0.6729,1.5-1.5v-2 C24,9.2236,23.7761,9,23.5,9z M20,16.5c0,0.2759-0.2244,0.5-0.5,0.5h-7c-0.2756,0-0.5-0.2241-0.5-0.5V13h8V16.5z"></path>
</StyledSVG>
),
render() {
return <ThemeEditor />
},
}

Просмотреть файл

@ -0,0 +1,4 @@
import * as React from 'react'
export const PageContext = React.createContext<any>({})
export const usePageContext = () => React.useContext(PageContext)

Просмотреть файл

@ -0,0 +1,63 @@
import * as React from 'react'
import { MDXProvider } from '@mdx-js/react'
import { Highlight, HighlightInline } from '../Highlight'
import { Global, css } from '@emotion/core'
export const Provider = props => (
<MDXProvider
components={{
code: Highlight,
inlineCode: HighlightInline,
}}
>
<Global
styles={css`
*,
*::before,
*::after {
box-sizing: border-box;
}
html,
body,
#gatsby-focus-wrapper,
#___gatsby {
height: 100%;
min-height: 100%;
font-family: 'Segoe UI';
}
body,
ul[class],
ol[class],
li,
figure,
figcaption,
blockquote,
dl,
dd {
margin: 0;
}
input,
button,
textarea,
select {
font: inherit;
}
img {
max-width: 100%;
display: block;
}
body {
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol';
background: #fff;
}
h1,
h2,
h3 {
font-weight: 600;
}
`}
/>
{props.children}
</MDXProvider>
)

Просмотреть файл

@ -0,0 +1 @@
export * from './PageContext'

Просмотреть файл

@ -0,0 +1,38 @@
# Storybook
Building isolated, reusable components is one of our project goals. If you are starting work on a new feature or want to isolate a component for development / debugging, it is recommended to use [Storybook][storybook].
## Running Storybook
```sh
yarn storybook
```
## Adding a story
To add a story to Storybook, place a `<Component>.story.tsx` (Example: Button.story.tsx) file in your component directory and `yarn storybook` from the command line.
## Writing a story
You can reference the [Storybook documentation][storybookdocs] for an introduction on “Writing Stories”.
You can find examples of stories in this repository by searching for `.story.tsx` files.
```jsx
import React from 'react'
import { BasicPlayground } from '.'
export default {
title: 'Playground',
component: Playground,
}
export const ToStorybook = () => <BasicPlayground />
ToStorybook.story = {
name: 'Basic Playground',
}
```
[storybook]: https://storybook.js.org/
[storybookdocs]: https://storybook.js.org/basics/writing-stories/

Просмотреть файл

@ -0,0 +1,303 @@
import * as React from 'react'
import styled from '@emotion/styled'
import Fuse from 'fuse.js'
import { useKeyPress } from '../../hooks'
const dummyData = [
{
title: "Old Man's War",
author: {
firstName: 'John',
lastName: 'Scalzi',
},
},
{
title: 'The Lock Artist',
author: {
firstName: 'Steve',
lastName: 'Hamilton',
},
},
{
title: 'HTML5',
author: {
firstName: 'Remy',
lastName: 'Sharp',
},
},
{
title: 'Right Ho Jeeves',
author: {
firstName: 'P.D',
lastName: 'Woodhouse',
},
},
{
title: 'The Code of the Wooster',
author: {
firstName: 'P.D',
lastName: 'Woodhouse',
},
},
{
title: 'Thank You Jeeves',
author: {
firstName: 'P.D',
lastName: 'Woodhouse',
},
},
{
title: 'The DaVinci Code',
author: {
firstName: 'Dan',
lastName: 'Brown',
},
},
{
title: 'Angels & Demons',
author: {
firstName: 'Dan',
lastName: 'Brown',
},
},
{
title: 'The Silmarillion',
author: {
firstName: 'J.R.R',
lastName: 'Tolkien',
},
},
{
title: 'Syrup',
author: {
firstName: 'Max',
lastName: 'Barry',
},
},
{
title: 'The Lost Symbol',
author: {
firstName: 'Dan',
lastName: 'Brown',
},
},
{
title: 'The Book of Lies',
author: {
firstName: 'Brad',
lastName: 'Meltzer',
},
},
{
title: 'Lamb',
author: {
firstName: 'Christopher',
lastName: 'Moore',
},
},
{
title: 'Fool',
author: {
firstName: 'Christopher',
lastName: 'Moore',
},
},
{
title: 'Incompetence',
author: {
firstName: 'Rob',
lastName: 'Grant',
},
},
{
title: 'Fat',
author: {
firstName: 'Rob',
lastName: 'Grant',
},
},
{
title: 'Colony',
author: {
firstName: 'Rob',
lastName: 'Grant',
},
},
{
title: 'Backwards, Red Dwarf',
author: {
firstName: 'Rob',
lastName: 'Grant',
},
},
{
title: 'The Grand Design',
author: {
firstName: 'Stephen',
lastName: 'Hawking',
},
},
{
title: 'The Book of Samson',
author: {
firstName: 'David',
lastName: 'Maine',
},
},
{
title: 'The Preservationist',
author: {
firstName: 'David',
lastName: 'Maine',
},
},
{
title: 'Fallen',
author: {
firstName: 'David',
lastName: 'Maine',
},
},
{
title: 'Monster 1959',
author: {
firstName: 'David',
lastName: 'Maine',
},
},
]
const options = {
shouldSort: true,
threshold: 0.6,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ['title', 'author.firstName'],
}
const fuse = new Fuse(dummyData, options)
export const Search = () => {
const [query, setQuery] = React.useState('')
const [results, setResults] = React.useState()
const [isEmpty, setEmpty] = React.useState(true)
const [isFocused, setFocus] = React.useState(false)
const inputRef = React.useRef<any>()
const handleChange = event => {
const value = event.target.value
setQuery(value)
const results = fuse.search(value)
setResults(results)
results && results.length > 0 ? setEmpty(false) : setEmpty(true)
}
const handleOnFocus = () => {
setFocus(true)
}
const handleOnBlur = () => {
setFocus(false)
}
const invokeSearch = useKeyPress('/')
React.useEffect(() => {
invokeSearch && inputRef.current.focus()
}, [invokeSearch])
return (
<StyledSearch>
<Input
ref={inputRef}
placeholder={isFocused ? '' : 'Search the docs ("/" to focus)'}
value={query}
onChange={handleChange}
onBlur={handleOnBlur}
onFocus={handleOnFocus}
/>
{isFocused && (
<PopOver>
{isEmpty ? (
<ResultsViews>
{query.length > 0 ? (
<div className="ResultsViews-Empty">Empty state</div>
) : (
<div className="ResultsViews-ZeroQuery">Zero query</div>
)}
</ResultsViews>
) : (
<Results>
<ResultsList>
{results.map((result: any) => (
<ResultsItem>{result.title}</ResultsItem>
))}
</ResultsList>
</Results>
)}
</PopOver>
)}
</StyledSearch>
)
}
const StyledSearch = styled.div`
position: relative;
width: 260px;
`
const Input = styled.input`
padding: 10px;
margin: 8px 0px 0px;
height: 32px;
width: 100%;
border: 0px none;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.05);
-moz-appearance: none;
overflow: hidden;
font-size: 14px;
`
const PopOver = styled.div`
position: absolute;
top: 60px;
left: 0px;
padding: 20px;
height: 300px;
width: 100%;
overflow: scroll;
background-color: #fff;
border: 1px solid #eee;
border-radius: 5px;
box-shadow: 0 20px 30px rgba(100, 100, 100, 0.2);
`
const Results = styled.div`
display: flex;
`
const ResultsList = styled.ul`
margin: 0px;
padding: 0px;
`
const ResultsItem = styled.li`
padding: 0px;
margin: 0px 0px 12px;
list-style: none;
`
const ResultsViews = styled.div`
display: flex;
opacity: 0.5;
.EmptyState-None {
margin: auto;
}
`

Просмотреть файл

@ -0,0 +1 @@
export { Search } from './Search'

Просмотреть файл

@ -0,0 +1,64 @@
import * as React from 'react';
import styled from '@emotion/styled';
import { Link } from 'gatsby';
import { usePageContext } from '../Provider';
const Menu = props => {
const { items, level = 0 } = props;
const menuItems = items.map((item, index) => {
if (!item.link && !item.items) {
return;
}
if (item.link && item.link.charAt(0) !== '/') {
item.link = '/' + item.link;
}
return (
<li key={index} style={{ marginLeft: 16 * level + 'px', marginBottom: 4 }}>
{item.link ? <Link to={item.link}>{item.name}</Link> : <span> {item.name}</span>}
{item.items && <Menu items={item.items} level={level + 1} />}
</li>
);
});
return <StyledList>{menuItems}</StyledList>;
};
export const Sidebar = props => {
const {
pathContext: { toc = [] }
} = usePageContext();
return (
<StyledSidebar>
<Menu items={toc} />
</StyledSidebar>
);
};
const HeaderHeight = 69;
const FooterHeight = 162;
const StyledSidebar = styled.div`
width: 300px;
padding: 40px;
background-color: rgba(0, 0, 0, 0.01);
height: calc(100vh - ${HeaderHeight}px - ${FooterHeight}px);
flex: none;
`;
const StyledList = styled.ul`
margin: 0px;
padding: 0px;
li {
margin: 0px 0px 20px;
padding: 0px;
list-style: none;
a {
color: #323130;
font-weight: 500;
text-decoration: none;
}
}
`;

Просмотреть файл

Просмотреть файл

@ -0,0 +1 @@
export { Sidebar } from './Sidebar'

Просмотреть файл

@ -0,0 +1,71 @@
import * as React from 'react'
import styled from '@emotion/styled'
import { useRootStyles } from './useRootStyles'
type SliderChangeEvent = React.ChangeEvent<HTMLInputElement>
const StyleTest: React.FC = () => {
const [hue, setHue] = React.useState(0)
const [saturation, setSaturation] = React.useState(50)
const [lightness, setLightness] = React.useState(50)
useRootStyles({ '--hue': hue, '--saturation': saturation, '--lightness': lightness })
const handleHueChange = (event: SliderChangeEvent) => setHue(parseInt(event.target.value, 10))
const handleSaturationChange = (event: SliderChangeEvent) => setSaturation(parseInt(event.target.value, 10))
const handleLightnessChange = (event: SliderChangeEvent) => setLightness(parseInt(event.target.value, 10))
return (
<StoryLayout>
<Swatch />
<fieldset>
<Label htmlFor="hue">Hue</Label>
<Slider name="hue" type="range" min={0} max={360} value={hue} onChange={handleHueChange} />
<Label htmlFor="saturation">Saturation</Label>
<Slider name="saturation" type="range" min={0} max={100} value={saturation} onChange={handleSaturationChange} />
<Label htmlFor="lightness">Lightness</Label>
<Slider name="lightness" type="range" min={0} max={100} value={lightness} onChange={handleLightnessChange} />
</fieldset>
</StoryLayout>
)
}
const Swatch = styled.div`
background-color: hsl(var(--hue), calc(var(--saturation) * 1%), calc(var(--lightness) * 1%));
width: 80px;
height: 80px;
`
const StoryLayout = styled.div`
display: flex;
height: 100vh;
width: 100vw;
align-items: center;
justify-content: center;
fieldset {
width: 300px;
border: none;
}
`
const Label = styled.label`
text-transform: uppercase;
font-family: sans-serif;
font-size: 14px;
color: hsl(0, 0%, 50%);
display: inline-block;
width: 120px;
text-align: right;
padding-right: 8px;
`
const Slider = styled.input`
display: inline-block;
width: 140px;
`
export default { title: 'StyleTest', component: StyleTest }
export const ToStorybook = () => <StyleTest />
ToStorybook.story = { name: 'Basic' }

Просмотреть файл

@ -0,0 +1,32 @@
import * as React from 'react'
import styled from '@emotion/styled'
export interface ExampleProps {
children: React.ReactNode
bad: true | undefined
}
export const Example = (props: ExampleProps) => (
<StyledExample {...props}>
<div>{props.children}</div>
</StyledExample>
)
const StyledExample = styled.div<ExampleProps>`
display: flex;
position: relative;
min-width: 304px;
min-height: 96px;
border-radius: 3px;
padding: 1em;
color: #808080;
background-color: ${props => (props.bad ? '#fcedee' : '#f2f2f2')};
user-select: none;
& > * {
display: block;
margin: auto;
}
`

Просмотреть файл

@ -0,0 +1,112 @@
import * as React from 'react'
import { css } from '@emotion/core'
import styled from '@emotion/styled'
import { ExampleProps } from './Example'
interface UsageProps {
children: React.ReactNode
}
export const Usage = (props: UsageProps) => {
if (!props.children || !(typeof props.children === 'object') || !(1 in (props.children as any)))
throw new Error('At least two children are required in <Usage>.')
const children = props.children as React.ReactNode[]
// TODO: Make this work outside of the MDXRenderer.
const firstExampleIndex = children.findIndex((child: any) => child.props && child.props.mdxType === 'Example')
if (firstExampleIndex < 0) throw new Error('At least one <Example> is required in <Usage>.')
const examples = [...injectHeaders(children.slice(firstExampleIndex))]
return (
<StyledUsage
css={css`
grid-template-rows: repeat(${examples.length + 1}, auto);
`}
>
<Description>{children.slice(0, firstExampleIndex)}</Description>
{examples}
</StyledUsage>
)
}
const injectHeaders = function*(examples: React.ReactNode[]): Generator<React.ReactNode> {
let lastExampleWasGood: boolean | null = null
const count = examples.length
for (let i = 0; i < count; i++) {
const example = examples[i]
const exampleIsGood = !((example as any)?.props as ExampleProps)?.bad
if (exampleIsGood && lastExampleWasGood !== true) yield (<LikeThis key={i}>Like this</LikeThis>)
else if (!exampleIsGood && lastExampleWasGood !== false) yield (<NotThis key={i}>Not this</NotThis>)
yield example
lastExampleWasGood = exampleIsGood
}
}
const StyledUsage = styled.div`
position: relative;
margin: 1em 0 1em 0;
display: grid;
grid-template-columns: [description] 1fr [examples] auto;
column-gap: 48px;
row-gap: 4px;
`
const headerHeight = '24px'
const Description = styled.div`
grid-column: description;
grid-row: 1 / -1;
margin-top: ${headerHeight};
p:first-of-type {
margin-top: 0;
}
p:last-of-type {
margin-bottom: 0;
}
`
const Header = css`
height: ${headerHeight};
padding-top: 6px;
border-style: solid;
border-width: 0 0 1px 0;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
font-variant-caps: titling-caps;
user-select: none;
`
const LikeThis = styled.div`
${Header}
border-color: #13a40e;
color: #13a40e;
&::before {
content: '\\2713';
margin-right: 0.5em;
}
`
const NotThis = styled.div`
${Header}
border-color: #e73550;
color: #e73550;
&::before {
content: '\\2715';
margin-right: 0.5em;
}
`

Просмотреть файл

@ -0,0 +1,2 @@
export { Example } from './Example'
export { Usage } from './Usage'

Просмотреть файл

@ -0,0 +1,22 @@
import { useLayoutEffect } from 'react'
type RootStyles =
// each style can be documented
| '--hue'
// which helps with maintenance
| '--saturation'
// but this could also get a little nutty
| '--lightness'
type StyleObject = { [key in RootStyles]: string | number }
/*
* Custom React hook to update CSS variables on the document node
*/
export function useRootStyles(styles: StyleObject) {
useLayoutEffect(
() =>
Object.keys(styles).forEach((styleKey: string) => document.documentElement.style.setProperty(styleKey, styles[styleKey] as string)),
[styles]
)
}

Просмотреть файл

@ -0,0 +1 @@
export { useKeyPress } from './useKeyPress'

Просмотреть файл

@ -0,0 +1,21 @@
import * as React from 'react'
export const useKeyPress = targetKey => {
const [keyPressed, setKeyPressed] = React.useState(false)
function downHandler({ key }) {
if (key === targetKey) {
setKeyPressed(true)
}
}
React.useEffect(() => {
window.addEventListener('keydown', downHandler)
// cleanup
return () => {
window.removeEventListener('keydown', downHandler)
}
}, [])
return keyPressed
}

Просмотреть файл

@ -0,0 +1,10 @@
import unified from 'unified'
import parse from 'remark-parse'
import remark2react from 'remark-react'
export default function(md: string) {
return unified()
.use(parse as any)
.use(remark2react)
.processSync(md).contents
}

Просмотреть файл

@ -0,0 +1,10 @@
import { ISidebarItem } from '.'
export interface IPageTemplateProps {
sidebarItems?: {
name: string
items?: ISidebarItem[]
}
children?: React.ReactNode
path?: string
}

Просмотреть файл

@ -0,0 +1,5 @@
export interface ISidebarItem {
name: string
link?: string
items?: ISidebarItem[]
}

Просмотреть файл

@ -0,0 +1,20 @@
import * as React from 'react'
import styled from '@emotion/styled'
import PageTemplate from './PageTemplate'
export default props => {
return (
<PageTemplate {...props}>
{props.pathContext.frontmatter.titleCategory && <TitleCategory>{props.pathContext.frontmatter.titleCategory}</TitleCategory>}
<h1>{props.pathContext.frontmatter.title}</h1>
{props.children}
</PageTemplate>
)
}
const TitleCategory = styled.div`
margin: -1em 0 -1em 0;
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
`

Просмотреть файл

@ -0,0 +1,46 @@
import * as React from 'react'
import { Helmet } from 'react-helmet'
import { useStaticQuery, graphql } from 'gatsby'
import { Provider } from '../components/Provider/Provider'
import { IPageTemplateProps } from '.'
export default (props: IPageTemplateProps) => {
const data = useStaticQuery(graphql`
{
site {
siteMetadata {
title
description
}
}
}
`)
return (
<Provider>
<Helmet>
<meta charSet="utf-8" />
<meta name="Description" content={data.site.siteMetadata.description} />
<title>{data.site.siteMetadata.title}</title>
<link rel="shortcut icon" href="favicons/favicon.ico" />
<link rel="icon" sizes="16x16 32x32 64x64" href="favicons/favicon.ico" />
<link rel="icon" type="image/png" sizes="196x196" href="favicons/favicon-192.png" />
<link rel="icon" type="image/png" sizes="160x160" href="favicons/favicon-160.png" />
<link rel="icon" type="image/png" sizes="96x96" href="favicons/favicon-96.png" />
<link rel="icon" type="image/png" sizes="64x64" href="favicons/favicon-64.png" />
<link rel="icon" type="image/png" sizes="32x32" href="favicons/favicon-32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="favicons/favicon-16.png" />
<link rel="apple-touch-icon" href="favicons/favicon-57.png" />
<link rel="apple-touch-icon" sizes="114x114" href="favicons/favicon-114.png" />
<link rel="apple-touch-icon" sizes="72x72" href="favicons/favicon-72.png" />
<link rel="apple-touch-icon" sizes="144x144" href="favicons/favicon-144.png" />
<link rel="apple-touch-icon" sizes="60x60" href="favicons/favicon-60.png" />
<link rel="apple-touch-icon" sizes="120x120" href="favicons/favicon-120.png" />
<link rel="apple-touch-icon" sizes="76x76" href="favicons/favicon-76.png" />
<link rel="apple-touch-icon" sizes="152x152" href="favicons/favicon-152.png" />
<link rel="apple-touch-icon" sizes="180x180" href="favicons/favicon-180.png" />
</Helmet>
{props.children}
</Provider>
)
}

Просмотреть файл

@ -0,0 +1,37 @@
import * as React from 'react';
import PageShell from './PageShell';
import { Sidebar } from '../components/Sidebar';
import { PageInnerContent } from '../components/Content';
import styled from '@emotion/styled';
import { usePageViewTelemetry } from '../components/ApplicationInsights';
import { Header } from '../components/Header';
import { PageContext } from '../components/Provider';
import { Footer } from '../components/Footer';
export default props => {
usePageViewTelemetry({ name: props.path });
const sidebarItems = props.pathContext !== undefined ? props.pathContext.toc : undefined;
return (
<PageContext.Provider value={props}>
<PageShell>
<Header />
<Canvas>
{sidebarItems && <Sidebar items={sidebarItems} />}
<PageInnerContent>{props.children}</PageInnerContent>
</Canvas>
<Footer />
</PageShell>
</PageContext.Provider>
);
};
const HeaderHeight = 69;
const FooterHeight = 162;
const Canvas = styled.div`
display: flex;
min-height: calc(100vh - ${HeaderHeight}px - ${FooterHeight}px);
max-height: fit-content;
overflow: hidden;
`;

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше