This commit is contained in:
Родитель
a49b6fd856
Коммит
937f5ab42f
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
@ -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
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 you’d 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 project’s 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. It’s 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 won’t 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;
|
||||||
|
`;
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче