This commit is contained in:
Родитель
a49b6fd856
Коммит
937f5ab42f
|
@ -28,6 +28,10 @@ lib-cov
|
|||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# gatsby files
|
||||
.cache/
|
||||
public
|
||||
|
||||
# nyc test coverage
|
||||
.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
|
||||
|
||||
- [Button](/Components/Button)
|
||||
- [Link](/Components/Link)
|
||||
- [Separator](/Components/Separator)
|
||||
- [Text](/Components/Text)
|
||||
- [Button](/windows/components/button)
|
||||
- [Link](/windows/components/link)
|
||||
- [Separator](/windows/components/separator)
|
||||
- [Text](/windows/components/text)
|
||||
|
||||
## Utilities
|
||||
|
||||
- [FocusTrapZone](/Utilities/FocusTrapZone)
|
||||
- [Pressable](/Utilities/Pressable)
|
||||
- [Stack](/Utilities/Stack)
|
||||
- [FocusTrapZone](/windows/components/utilities/focustrapzone)
|
||||
- [Pressable](/windows/components/utilities/pressable)
|
||||
- [Stack](/windows/components/utilities/stack)
|
||||
|
||||
## 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",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"name": "fluent-website",
|
||||
"version": "0.0.1",
|
||||
"description": "Fluent website content",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/microsoft/fluent-site"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"start": "next start",
|
||||
"build": "next build && echo"
|
||||
"clean": "gatsby clean",
|
||||
"build": "gatsby build --prefix-paths",
|
||||
"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": {
|
||||
"@mdx-js/loader": "^1.5.5",
|
||||
"@mdx-js/mdx": "^1.5.5",
|
||||
"@next/mdx": "^9.2.1",
|
||||
"next": "^9.2.1"
|
||||
"gatsby-plugin-typescript": "^2.1.26",
|
||||
"typescript": "^3.5.1",
|
||||
"gatsby": "^2.19.27",
|
||||
"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;
|
||||
`;
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче