diff --git a/.eslintignore b/.eslintignore
index 25a743b72..52079d084 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -6,4 +6,4 @@
**/public/**
**/public-prod/**
**/blueprints/**
-web/static/**
\ No newline at end of file
+web/static/**
diff --git a/.storybook/config.js b/.storybook/config.js
index f9260ac8d..b21eb1dca 100644
--- a/.storybook/config.js
+++ b/.storybook/config.js
@@ -13,4 +13,5 @@ configure(() => {
// Add regular story definitions (i.e. those using storiesOf() directly below)
require('../components/x-increment/storybook/index.jsx');
+ require('../components/x-follow-button/storybook/index.jsx');
}, module);
diff --git a/.storybook/register-components.js b/.storybook/register-components.js
index 06ad9288b..071883d9f 100644
--- a/.storybook/register-components.js
+++ b/.storybook/register-components.js
@@ -4,7 +4,7 @@ const components = [
require('../components/x-teaser/storybook'),
require('../components/x-styling-demo/stories'),
require('../components/x-gift-article/stories'),
- require('../components/x-podcast-launchers/stories'),
+ require('../components/x-podcast-launchers/stories')
];
module.exports = components;
diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js
index b419c439b..70520a4c8 100644
--- a/.storybook/webpack.config.js
+++ b/.storybook/webpack.config.js
@@ -2,6 +2,7 @@
// See https://storybook.js.org/configurations/custom-webpack-config/ for more info.
const path = require('path');
+const glob = require('glob');
const fs = require('fs');
const xBabelConfig = require('../packages/x-babel-config');
const xEngine = require('../packages/x-engine/src/webpack');
@@ -58,6 +59,29 @@ module.exports = ({ config }) => {
return name.includes('babel-preset-minify') === false;
});
+ config.module.rules.push({
+ test: /\.(scss|sass)$/,
+ use: [
+ {
+ loader: require.resolve('style-loader')
+ },
+ {
+ loader: require.resolve('css-loader'),
+ options: {
+ url: false,
+ import: false,
+ modules: true
+ }
+ },
+ {
+ loader: require.resolve('sass-loader'),
+ options: {
+ includePaths: glob.sync('./components/*/bower_components', { absolute: true }),
+ }
+ }
+ ]
+ });
+
// HACK: Ensure we only bundle one instance of React
config.resolve.alias.react = require.resolve('react');
diff --git a/__mocks__/styleMock.js b/__mocks__/styleMock.js
new file mode 100644
index 000000000..f053ebf79
--- /dev/null
+++ b/__mocks__/styleMock.js
@@ -0,0 +1 @@
+module.exports = {};
diff --git a/components/x-follow-button/.bowerrc b/components/x-follow-button/.bowerrc
new file mode 100644
index 000000000..59e9a5925
--- /dev/null
+++ b/components/x-follow-button/.bowerrc
@@ -0,0 +1,8 @@
+{
+ "registry": {
+ "search": [
+ "https://origami-bower-registry.ft.com",
+ "https://registry.bower.io"
+ ]
+ }
+}
diff --git a/components/x-follow-button/__tests__/x-follow-button.test.jsx b/components/x-follow-button/__tests__/x-follow-button.test.jsx
new file mode 100644
index 000000000..ae149fc84
--- /dev/null
+++ b/components/x-follow-button/__tests__/x-follow-button.test.jsx
@@ -0,0 +1,174 @@
+const { h } = require('@financial-times/x-engine');
+const { mount } = require('@financial-times/x-test-utils/enzyme');
+
+import { FollowButton } from '../src/FollowButton';
+
+describe('x-follow-button', () => {
+ describe('concept name', () => {
+ it('when conceptNameAsButtonText prop is true, and the topic name is provided, the button is named by this name', () => {
+ const subject = mount(
+
+ );
+ expect(subject.find('button').text()).toEqual('dummy concept name');
+ });
+
+ it('when conceptNameAsButtonText prop is false, the button has a default name', () => {
+ const subject = mount(
+
+ );
+ expect(subject.find('button').text()).toEqual('Add to myFT');
+ });
+
+ it('when conceptNameAsButtonText prop is true, and the topic name is not provided, the button has a default name', () => {
+ const subject = mount();
+ expect(subject.find('button').text()).toEqual('Add to myFT');
+ });
+
+ it('when conceptNameAsButtonText prop is not provided, the button has a default name', () => {
+ const subject = mount();
+ expect(subject.find('button').text()).toEqual('Add to myFT');
+ });
+ });
+ describe('conceptId prop', () => {
+ it('assigns conceptId prop to data-concept-id attribute of the button', async () => {
+ const subject = mount();
+ expect(subject.find('button').prop('data-concept-id')).toEqual('dummy-id');
+ });
+
+ it('assigns conceptId prop to data-concept-id attribute of the form', async () => {
+ const subject = mount();
+ expect(subject.find('form').prop('data-concept-id')).toEqual('dummy-id');
+ });
+ });
+
+ describe('form action', () => {
+ it('assigns follow-plus-digest-email put action if followPlusDigestEmail is true', async () => {
+ const subject = mount();
+ expect(subject.find('form').prop('action')).toEqual(
+ '/__myft/api/core/follow-plus-digest-email/dummy-id?method=put'
+ );
+ });
+
+ it('assigns followed/concept delete action if isFollowed is true', async () => {
+ const subject = mount();
+ expect(subject.find('form').prop('action')).toEqual(
+ '/__myft/api/core/followed/concept/dummy-id?method=delete'
+ );
+ });
+
+ it('assigns followed/concept put action if isFollowed and followPlusDigestEmail are not passed', async () => {
+ const subject = mount();
+ expect(subject.find('form').prop('action')).toEqual(
+ '/__myft/api/core/followed/concept/dummy-id?method=put'
+ );
+ });
+ });
+
+ describe('isFollowed', () => {
+ describe('when true', () => {
+ it('button text is "Added"', () => {
+ const subject = mount();
+ expect(subject.find('button').text()).toEqual('Added');
+ });
+
+ it('button aria-pressed is "true"', () => {
+ const subject = mount();
+ expect(subject.find('button').prop('aria-pressed')).toEqual('true');
+ });
+
+ it('button title is "Remove ConceptName from myFT"', () => {
+ const subject = mount();
+ expect(subject.find('button').prop('title')).toEqual('Remove ConceptName from myFT');
+ });
+
+ it('button aria-label is "Remove conceptName from myFT"', () => {
+ const subject = mount();
+ expect(subject.find('button').prop('aria-label')).toEqual('Remove ConceptName from myFT');
+ });
+ });
+
+ describe('when false', () => {
+ it('button text is "Add to myFT"', () => {
+ const subject = mount();
+ expect(subject.find('button').text()).toEqual('Add to myFT');
+ });
+
+ it('button aria-pressed is "false"', () => {
+ const subject = mount();
+ expect(subject.find('button').prop('aria-pressed')).toEqual('false');
+ });
+
+ it('button title is "Add ConceptName to myFT"', () => {
+ const subject = mount();
+ expect(subject.find('button').prop('title')).toEqual('Add ConceptName to myFT');
+ });
+
+ it('button aria-label is "Add ConceptName to myFT"', () => {
+ const subject = mount();
+ expect(subject.find('button').prop('aria-label')).toEqual('Add ConceptName to myFT');
+ });
+ });
+ });
+
+ describe('followPlusDigestEmail', () => {
+ describe('when true', () => {
+ it('form has data-myft-ui-variant property which is true', () => {
+ const subject = mount();
+ expect(subject.find('form').prop('data-myft-ui-variant')).toEqual(true);
+ });
+
+ it('button has data-trackable-context-messaging property which is add-to-myft-plus-digest-button', () => {
+ const subject = mount();
+ expect(subject.find('button').prop('data-trackable-context-messaging')).toEqual(
+ 'add-to-myft-plus-digest-button'
+ );
+ });
+ });
+
+ describe('when false', () => {
+ it('form has data-myft-ui-variant property which is true', () => {
+ const subject = mount();
+ expect(subject.find('form').prop('data-myft-ui-variant')).toEqual(undefined);
+ });
+
+ it('button has data-trackable-context-messaging property which is add-to-myft-plus-digest-button', () => {
+ const subject = mount();
+ expect(subject.find('button').prop('data-trackable-context-messaging')).toEqual(null);
+ });
+ });
+ });
+
+ describe('form properties', () => {
+ it('method = GET', () => {
+ const subject = mount();
+ expect(subject.find('form').prop('method')).toEqual('GET');
+ });
+ });
+
+ describe('button properties', () => {
+ it('data-trackable="follow"', () => {
+ const subject = mount();
+ expect(subject.find('button').prop('data-trackable')).toEqual('follow');
+ });
+
+ it('type="submit"', () => {
+ const subject = mount();
+ expect(subject.find('button').prop('type')).toEqual('submit');
+ });
+ });
+
+ describe('csrf token', () => {
+ it('if passed creates an invisible input field', () => {
+ const subject = mount();
+ expect(subject.find('input').prop('value')).toEqual('dummyToken');
+ expect(subject.find('input').prop('type')).toEqual('hidden');
+ expect(subject.find('input').prop('name')).toEqual('token');
+ expect(subject.find('input').prop('data-myft-csrf-token')).toEqual(true);
+ });
+
+ it('if not passed an invisible input field is not created', () => {
+ const subject = mount();
+ expect(subject.find('input')).toEqual({});
+ });
+ });
+});
diff --git a/components/x-follow-button/bower.json b/components/x-follow-button/bower.json
new file mode 100644
index 000000000..48cc79ee1
--- /dev/null
+++ b/components/x-follow-button/bower.json
@@ -0,0 +1,10 @@
+{
+ "name": "x-follow-button",
+ "private": true,
+ "main": "dist/FollowButton.es5.js",
+ "dependencies": {
+ "o-colors": "^5.0.3",
+ "o-icons": "^6.0.0",
+ "o-typography": "^6.1.0"
+ }
+}
diff --git a/components/x-follow-button/package.json b/components/x-follow-button/package.json
new file mode 100644
index 000000000..a873b6f42
--- /dev/null
+++ b/components/x-follow-button/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "@financial-times/x-follow-button",
+ "version": "1.0.0",
+ "description": "",
+ "main": "dist/FollowButton.cjs.js",
+ "style": "dist/FollowButton.css",
+ "browser": "dist/FollowButton.es5.js",
+ "module": "dist/FollowButton.esm.js",
+ "scripts": {
+ "prepare": "bower install && npm run build",
+ "build": "node rollup.js",
+ "start": "node rollup.js --watch"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@financial-times/x-rollup": "file:../../packages/x-rollup",
+ "@financial-times/x-test-utils": "file:../../packages/x-test-utils",
+ "rollup": "^0.57.1",
+ "bower": "^1.7.9",
+ "node-sass": "^4.9.2"
+ },
+ "peerDependencies": {
+ "@financial-times/x-engine": "file:../../packages/x-engine"
+ },
+ "dependencies": {
+ "@financial-times/x-engine": "file:../../packages/x-engine",
+ "classnames": "^2.2.6"
+ }
+}
diff --git a/components/x-follow-button/readme.md b/components/x-follow-button/readme.md
new file mode 100644
index 000000000..e1ce947ff
--- /dev/null
+++ b/components/x-follow-button/readme.md
@@ -0,0 +1,43 @@
+# x-follow-button
+
+This module provides a template for myFT follow topic button, and is intended to replace the legacy handlebars component in [n-myft-ui](https://github.com/Financial-Times/n-myft-ui/tree/master/components/follow-button).
+
+## Installation
+
+```bash
+npm install --save @financial-times/x-follow-button
+```
+
+## Props
+
+(Some of the properties don't influence the way button looks or acts, but can be used for e.g. client-side Javascript in the apps).
+
+Feature | Type | Required | Default value | Description
+----------------------------|---------|----------|----------------|---------------
+`conceptId` | String | yes | none | UUID of the concept
+`conceptName` | String | yes | none | Name of the concept
+`conceptNameAsButtonText` | Boolean | no | `false` | If true will use the concept name as the button text, otherwise will default to "Add to MyFT" or "Remove from MyFT" (depending on isFollowed prop).
+`isFollowed` | Boolean | no | `false` | Whether the concept is followed or not.
+`csrfToken` | String | no | none | CSRF token (will be included in a hidden form field).
+`variant` | String | no | `standard` | One of `standard`, `inverse`, `opinion` or `monochrome`. Other values will be ignored.
+`followPlusDigestEmail` | Boolean | no | `false` | Whether following the topic should also subscribe to the digest.
+
+## Client side behaviour
+
+For users with JavaScript enabled, the default form submit action is prevented, and a custom event (named 'x-follow-button') will be dispatched on the form element.
+
+This custom event will contain the following in its `detail` object:
+
+Property | Value
+-------------------|-----------------
+`action` | `add` or `remove`
+`actorType` | `user`
+`relationshipName` | `followed`
+`subjectType` | `concept`
+`subjectId` | the value of the `conceptId` prop
+`token` | the value of the `csrfToken` prop
+
+It is up to the consumer of this component to listen for the `x-follow-button` event, and use this data, along with the user's ID, and carry out the appropriate action.
+
+For example, if using `next-myft-client` to carry out the follow/unfollow action, n-myft-ui provides a x-button-interaction component for this:
+https://github.com/Financial-Times/n-myft-ui/blob/master/components/x-button-integration/index.js
diff --git a/components/x-follow-button/rollup.js b/components/x-follow-button/rollup.js
new file mode 100644
index 000000000..075823328
--- /dev/null
+++ b/components/x-follow-button/rollup.js
@@ -0,0 +1,4 @@
+const xRollup = require('@financial-times/x-rollup');
+const pkg = require('./package.json');
+
+xRollup({ input: './src/FollowButton.jsx', pkg });
diff --git a/components/x-follow-button/src/FollowButton.jsx b/components/x-follow-button/src/FollowButton.jsx
new file mode 100644
index 000000000..807e28a72
--- /dev/null
+++ b/components/x-follow-button/src/FollowButton.jsx
@@ -0,0 +1,81 @@
+import { h } from '@financial-times/x-engine';
+import classNames from 'classnames';
+import styles from './styles/main.scss';
+
+export const FollowButton = (props) => {
+ const {
+ conceptNameAsButtonText = false,
+ conceptId,
+ conceptName,
+ isFollowed,
+ csrfToken,
+ followPlusDigestEmail,
+ onSubmit,
+ variant
+ } = props;
+ const VARIANTS = ['standard', 'inverse', 'opinion', 'monochrome'];
+
+ const getFormAction = () => {
+ if (followPlusDigestEmail) {
+ return `/__myft/api/core/follow-plus-digest-email/${conceptId}?method=put`;
+ } else if (isFollowed) {
+ return `/__myft/api/core/followed/concept/${conceptId}?method=delete`;
+ } else {
+ return `/__myft/api/core/followed/concept/${conceptId}?method=put`;
+ }
+ };
+
+ const getButtonText = () => {
+ if (conceptNameAsButtonText && conceptName) {
+ return conceptName;
+ }
+
+ return isFollowed ? 'Added' : 'Add to myFT';
+ };
+
+ const getAccessibleText = () =>
+ isFollowed ? `Remove ${conceptName} from myFT` : `Add ${conceptName} to myFT`;
+
+ return (
+
+ );
+};
diff --git a/components/x-follow-button/src/styles/components/FollowButton.scss b/components/x-follow-button/src/styles/components/FollowButton.scss
new file mode 100644
index 000000000..3c98e9db9
--- /dev/null
+++ b/components/x-follow-button/src/styles/components/FollowButton.scss
@@ -0,0 +1,11 @@
+@import '../mixins/lozenge/main';
+
+.button {
+ @include myftLozenge($with-toggle-icon: true);
+}
+
+@each $theme in map-keys($myft-lozenge-themes) {
+ .button--#{$theme} {
+ @include myftLozengeTheme($theme, $with-toggle-icon: true);
+ }
+}
diff --git a/components/x-follow-button/src/styles/main.scss b/components/x-follow-button/src/styles/main.scss
new file mode 100644
index 000000000..529d088fa
--- /dev/null
+++ b/components/x-follow-button/src/styles/main.scss
@@ -0,0 +1,10 @@
+// TODO: update me to not need a system code
+$system-code:'github:Financial-Times/x-dash' !default;
+
+@import 'o-icons/main';
+@import 'o-colors/main';
+@import 'o-typography/main';
+
+@import './mixins/lozenge/main.scss';
+
+@import './components/FollowButton.scss';
diff --git a/components/x-follow-button/src/styles/mixins/lozenge/_themes.scss b/components/x-follow-button/src/styles/mixins/lozenge/_themes.scss
new file mode 100644
index 000000000..c144e52ee
--- /dev/null
+++ b/components/x-follow-button/src/styles/mixins/lozenge/_themes.scss
@@ -0,0 +1,42 @@
+$theme-map: null;
+
+$myft-lozenge-themes: (
+ standard: (
+ background: oColorsByName('claret'),
+ text: oColorsByName('white'),
+ highlight: oColorsByName('claret-50'),
+ pressed-highlight: rgba(oColorsByName('black'), 0.05),
+ disabled: rgba(oColorsByName('black'), 0.5)
+ ),
+ inverse: (
+ background: oColorsByName('white'),
+ text: oColorsByName('claret'),
+ highlight: rgba(white, 0.8),
+ pressed-highlight: rgba(white, 0.2),
+ disabled: rgba(oColorsByName('white'), 0.5)
+ ),
+ opinion: (
+ background: oColorsByName('oxford-40'),
+ text: oColorsByName('white'),
+ highlight: oColorsByName('oxford-30'),
+ pressed-highlight: rgba(oColorsByName('oxford-40'), 0.2),
+ disabled: rgba(oColorsByName('black'), 0.5)
+ ),
+ monochrome: (
+ background: oColorsByName('white'),
+ text: oColorsByName('black'),
+ highlight: oColorsByName('white-80'),
+ pressed-highlight: rgba(oColorsByName('white'), 0.2),
+ disabled: rgba(oColorsByName('white'), 0.5)
+ )
+);
+
+@function getThemeColor($key) {
+ @return map-get($theme-map, $key);
+}
+
+@mixin withTheme($theme) {
+ $theme-map: map-get($myft-lozenge-themes, $theme) !global;
+
+ @content;
+}
diff --git a/components/x-follow-button/src/styles/mixins/lozenge/_toggle-icon.scss b/components/x-follow-button/src/styles/mixins/lozenge/_toggle-icon.scss
new file mode 100644
index 000000000..302298c94
--- /dev/null
+++ b/components/x-follow-button/src/styles/mixins/lozenge/_toggle-icon.scss
@@ -0,0 +1,43 @@
+@mixin getIcon($name, $color) {
+ @include oIconsContent($icon-name: $name, $size: 10, $color: $color, $iconset-version: 1);
+ content: '';
+}
+
+@mixin plusIcon($color) {
+ @include getIcon('plus', $color);
+ background-size: 25px;
+ margin: 0 6px -1px 0;
+}
+
+@mixin tickIcon($color) {
+ @include getIcon('tick', $color);
+ background-size: 21px;
+}
+
+@mixin myftToggleIcon($theme: standard) {
+ @include withTheme($theme) {
+ &::before {
+ @include plusIcon(getThemeColor(background));
+ }
+
+ &[aria-pressed="true"] {
+ &::before {
+ @include tickIcon(getThemeColor(text));
+ }
+ }
+
+ &[disabled],
+ &[disabled]:hover {
+ background: transparent;
+
+ &::before {
+ @include plusIcon(getThemeColor(disabled));
+ opacity: 0.5;
+ }
+
+ &[aria-pressed="true"]::before {
+ @include tickIcon(getThemeColor(disabled));
+ }
+ }
+ }
+}
diff --git a/components/x-follow-button/src/styles/mixins/lozenge/main.scss b/components/x-follow-button/src/styles/mixins/lozenge/main.scss
new file mode 100644
index 000000000..c0a0d854a
--- /dev/null
+++ b/components/x-follow-button/src/styles/mixins/lozenge/main.scss
@@ -0,0 +1,64 @@
+@import './themes';
+@import './toggle-icon';
+
+@mixin myftLozengeTheme($theme: standard, $with-toggle-icon: false) {
+ @if $with-toggle-icon != false {
+ @include myftToggleIcon($theme);
+ }
+
+ @include withTheme($theme) {
+ background-color: transparent;
+ border: 1px solid getThemeColor(background);
+ color: getThemeColor(background);
+
+ &:hover,
+ &:focus {
+ background-color: getThemeColor(pressed-highlight);
+ border-color: getThemeColor(background);
+ color: getThemeColor(background);
+ }
+
+ &[aria-pressed="true"] {
+ background-color: getThemeColor(background);
+ border: 1px solid getThemeColor(background);
+ color: getThemeColor(text);
+
+ &:hover,
+ &:focus {
+ background-color: getThemeColor(highlight);
+ border-color: getThemeColor(highlight);
+ color: getThemeColor(text);
+ }
+ }
+
+ &[disabled]:hover,
+ &[disabled] {
+ background: transparent;
+ border: 1px solid getThemeColor(disabled);
+ color: getThemeColor(disabled);
+ }
+ }
+}
+
+@mixin myftLozenge($theme: standard, $with-toggle-icon: false) {
+ @include myftLozengeTheme($theme, $with-toggle-icon);
+ @include oTypographySans($scale: -1, $weight: 'semibold');
+
+ border-radius: 100px; // Number that will be larger than any possible height, so that works for all possible button sizes
+ box-sizing: content-box;
+ display: block;
+ font-size: 14px;
+ margin: 6px 4px 6px 2px;
+ max-width: 200px;
+ outline-offset: 2px;
+ overflow: hidden;
+ padding: 5px 12px;
+ text-align: left;
+ text-overflow: ellipsis;
+ transition: border-color, background-color 0.5s ease;
+ white-space: nowrap;
+
+ &:focus {
+ outline: none;
+ }
+}
diff --git a/components/x-follow-button/storybook/index.jsx b/components/x-follow-button/storybook/index.jsx
new file mode 100644
index 000000000..419b73185
--- /dev/null
+++ b/components/x-follow-button/storybook/index.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+
+import { storiesOf } from '@storybook/react';
+import { withKnobs, text, boolean, select } from '@storybook/addon-knobs';
+
+import { FollowButton } from '../src/FollowButton';
+
+const defaultProps = {
+ isFollowed: false,
+ variant: 'standard',
+ conceptNameAsButtonText: false,
+ conceptId: '00000-0000-00000-00000',
+ conceptName: 'UK politics & policy',
+ followPlusDigestEmail: true,
+ csrfToken: 'testTokenValue'
+};
+
+const toggleConceptNameAsButtonText = () =>
+ boolean('conceptNameAsButtonText', defaultProps.conceptNameAsButtonText);
+const toggleIsFollowed = () => boolean('isFollowed', defaultProps.isFollowed);
+const toggleConceptName = () => text('Topic name', defaultProps.conceptName);
+const toggleFollowPlusDigestEmail = () =>
+ boolean('followPlusDigestEmail', defaultProps.followPlusDigestEmail);
+const toggleVariant = () =>
+ select('variant', ['standard', 'inverse', 'opinion', 'monochrome'], defaultProps.variant);
+
+storiesOf('x-follow-button', module)
+ .addDecorator(withKnobs)
+ .add('Follow Button', () => {
+ const knobs = {
+ conceptNameAsButtonText: toggleConceptNameAsButtonText(),
+ isFollowed: toggleIsFollowed(),
+ conceptName: toggleConceptName(),
+ followPlusDigestEmail: toggleFollowPlusDigestEmail(),
+ variant: toggleVariant()
+ };
+
+ return ;
+ });
diff --git a/jest.config.js b/jest.config.js
index 1008f00bc..52ac7f513 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -3,6 +3,9 @@ module.exports = {
testMatch: ['**/__tests__/**/*.test.js?(x)'],
testPathIgnorePatterns: ['/node_modules/', '/bower_components/'],
transform: {
- '^.+\\.jsx?$': './packages/x-babel-config/jest',
+ '^.+\\.jsx?$': './packages/x-babel-config/jest'
},
+ moduleNameMapper: {
+ '^[./a-zA-Z0-9$_-]+\\.scss$': '/__mocks__/styleMock.js'
+ }
};
diff --git a/web/gatsby-config.js b/web/gatsby-config.js
index d75338486..26e347579 100644
--- a/web/gatsby-config.js
+++ b/web/gatsby-config.js
@@ -13,14 +13,14 @@ module.exports = {
options: {
name: 'docs',
path: './src/data'
- },
+ }
},
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'docs',
path: '../docs'
- },
+ }
},
{
resolve: 'gatsby-source-filesystem',
@@ -30,14 +30,14 @@ module.exports = {
// Don't attempt to load any Storybook or source files, as these may
// contain syntax and/or features we cannot parse.
ignore: [/stories/, /storybook/, /src/]
- },
+ }
},
{
resolve: 'gatsby-source-filesystem',
options: {
name: 'packages',
path: '../packages'
- },
+ }
},
// Handles markdown files (creates "MarkdownRemark" nodes)
{