diff --git a/src/atoms/Breadcrumbs/Breadcrumbs.js b/src/atoms/Breadcrumbs/Breadcrumbs.js
new file mode 100644
index 00000000..96e16bba
--- /dev/null
+++ b/src/atoms/Breadcrumbs/Breadcrumbs.js
@@ -0,0 +1,52 @@
+// @flow
+/* eslint-disable react/jsx-key */
+import React, { Fragment } from 'react';
+
+import { createStyledTag, createTheme } from '../../utils';
+import type { BreadcrumbsRoutes, BreadcrumbsMatchPath } from './Breadcrumbs.types';
+import { getBreadcrumbs } from './Breadcrumbs.utils';
+import { BreadcrumbsItem } from './BreadcrumbsItem';
+import { BreadcrumbsDivider } from './BreadcrumbsDivider';
+
+type BreadcrumbsProps = {|
+ /* the location pathname */
+ pathname: string,
+ /* list of breadcrumbs routes */
+ routes: BreadcrumbsRoutes,
+ /* custom match path function */
+ matchPath?: BreadcrumbsMatchPath,
+ /* custom breadcrum's item tag */
+ itemTagName?: string | React$ComponentType<*>,
+|};
+
+const name = 'breadcrumbs';
+
+const theme = createTheme(name, {
+ modifiers: {
+ },
+ defaults: {
+ },
+});
+
+const BreadcrumbsTag = createStyledTag(name, {});
+
+const Breadcrumbs = ({ itemTagName, pathname, routes, matchPath, ...rest }: BreadcrumbsProps) => {
+ const breadcrumbs = getBreadcrumbs(pathname, routes, matchPath);
+
+ return (
+
+ {
+ React.Children.toArray(
+ breadcrumbs.map((item, index) => (
+
+
+ { index !== breadcrumbs.length - 1 && > }
+
+ )),
+ )
+ }
+
+ );
+};
+
+export { Breadcrumbs, theme };
diff --git a/src/atoms/Breadcrumbs/Breadcrumbs.stories.js b/src/atoms/Breadcrumbs/Breadcrumbs.stories.js
new file mode 100644
index 00000000..79b27ef5
--- /dev/null
+++ b/src/atoms/Breadcrumbs/Breadcrumbs.stories.js
@@ -0,0 +1,27 @@
+import React from 'react';
+
+const routes = [
+ { path: '/', label: 'Dashboard' },
+ { path: '/app', component: ({ param }) => param },
+ { path: '/app/settings', label: 'Settings' },
+ { path: '/app/settings/security', label: 'Security' },
+];
+
+export default (asStory) => {
+ asStory('ATOMS/Breadcrumbs', module, (story, { Breadcrumbs, Button, Link, Row }) => {
+ story
+ .add('with default modifiers', () => (
+
+ ));
+ story
+ .add('with custom tagName', () => (
+ }
+ />
+ ));
+ });
+};
diff --git a/src/atoms/Breadcrumbs/Breadcrumbs.types.js b/src/atoms/Breadcrumbs/Breadcrumbs.types.js
new file mode 100644
index 00000000..1357bbbe
--- /dev/null
+++ b/src/atoms/Breadcrumbs/Breadcrumbs.types.js
@@ -0,0 +1,8 @@
+export type BreadcrumbsRoutes = Array<{
+ path: string,
+ component?: React$ComponentType<*>,
+ label?: string,
+ matchOptions?: Object,
+}>;
+
+export type BreadcrumbsMatchPath = (path: string, options: Object) => boolean;
diff --git a/src/atoms/Breadcrumbs/Breadcrumbs.utils.js b/src/atoms/Breadcrumbs/Breadcrumbs.utils.js
new file mode 100644
index 00000000..49a67864
--- /dev/null
+++ b/src/atoms/Breadcrumbs/Breadcrumbs.utils.js
@@ -0,0 +1,37 @@
+// @flow
+import type { BreadcrumbsRoutes, BreadcrumbsMatchPath } from './Breadcrumbs.types';
+
+const getPaths = (pathname: string) => pathname.replace(/\/$/, '').split('/').reduce((result, path, index) => [
+ ...result,
+ index < 2 ? `/${path}` : `${result[result.length - 1]}/${path}`,
+], []);
+
+
+const matchPathDefault: BreadcrumbsMatchPath = (path: string, options = {}) => path === options.path;
+
+const getBreadcrumbs = (
+ pathname: string,
+ routes: BreadcrumbsRoutes,
+ matchPath: BreadcrumbsMatchPath = matchPathDefault,
+) => {
+ const paths = getPaths(pathname);
+
+ const breadcrumbs = paths.reduce((result, path) => {
+ const matchedRoute = routes.find(
+ (route) => matchPath ? matchPath(path, { path: route.path, ...(route.matchOptions || {}) }) : route.path === path,
+ );
+
+ if (matchedRoute) {
+ const match = matchPath(path, { path: matchedRoute.path, ...(matchedRoute.matchOptions || {}) });
+
+ result = [...result, { ...matchedRoute, originalPath: path, match }];
+ }
+
+ return result;
+ }, []);
+
+ return breadcrumbs;
+};
+
+
+export { getBreadcrumbs };
diff --git a/src/atoms/Breadcrumbs/BreadcrumbsDivider.js b/src/atoms/Breadcrumbs/BreadcrumbsDivider.js
new file mode 100644
index 00000000..8e121c5f
--- /dev/null
+++ b/src/atoms/Breadcrumbs/BreadcrumbsDivider.js
@@ -0,0 +1,10 @@
+// @flow
+import styled from 'react-emotion';
+
+const BreadcrumbsDivider = styled('span')({
+ padding: '0 1rem',
+ fontSize: '1.6rem',
+ fontFamily: 'Poppins',
+});
+
+export { BreadcrumbsDivider };
diff --git a/src/atoms/Breadcrumbs/BreadcrumbsItem.js b/src/atoms/Breadcrumbs/BreadcrumbsItem.js
new file mode 100644
index 00000000..29d456e5
--- /dev/null
+++ b/src/atoms/Breadcrumbs/BreadcrumbsItem.js
@@ -0,0 +1,25 @@
+// @flow
+import React from 'react';
+
+import { Link } from '../typography/Link';
+
+type BreadcrumbsItemProps = {|
+ tagName: string | React$ComponentType<*>,
+ to: string,
+ label?: string,
+ component?: React$ComponentType<*>,
+|};
+
+const BreadcrumbsItem = ({ tagName, to, label, component: Component, ...rest }: BreadcrumbsItemProps) => React.createElement(
+ tagName,
+ {
+ to,
+ },
+ Component ? : label,
+);
+
+BreadcrumbsItem.defaultProps = {
+ tagName: Link,
+};
+
+export { BreadcrumbsItem };
diff --git a/src/atoms/Breadcrumbs/index.js b/src/atoms/Breadcrumbs/index.js
new file mode 100644
index 00000000..ce977548
--- /dev/null
+++ b/src/atoms/Breadcrumbs/index.js
@@ -0,0 +1 @@
+export * from './Breadcrumbs';
diff --git a/src/atoms/atoms.js b/src/atoms/atoms.js
index 9b1b76c5..041ba26d 100644
--- a/src/atoms/atoms.js
+++ b/src/atoms/atoms.js
@@ -2,6 +2,7 @@
export { Avatar } from './Avatar';
export { Button } from './Button';
+export { Breadcrumbs } from './Breadcrumbs';
export { Card } from './Card';
export { Checkbox } from './dataEntry/Checkbox';
export { CheckboxField } from './dataEntry/CheckboxField';
diff --git a/src/atoms/theme.js b/src/atoms/theme.js
index 0a27de27..55380346 100644
--- a/src/atoms/theme.js
+++ b/src/atoms/theme.js
@@ -2,6 +2,7 @@
import { theme as avatarTheme } from './Avatar';
import { theme as buttonTheme } from './Button';
+import { theme as breadcrumbsTheme } from './Breadcrumbs';
import { theme as cardTheme } from './Card';
import { theme as checkboxTheme } from './dataEntry/Checkbox';
import { theme as dialogTheme } from './Dialog';
@@ -32,6 +33,7 @@ import { theme as textTheme } from './typography/Text';
export const theme = {
...avatarTheme,
...buttonTheme,
+ ...breadcrumbsTheme,
...cardTheme,
...checkboxTheme,
...dialogTheme,
diff --git a/storybook/__tests__/__snapshots__/storyshots.test.js.snap b/storybook/__tests__/__snapshots__/storyshots.test.js.snap
index a65551de..0e0bac5c 100644
--- a/storybook/__tests__/__snapshots__/storyshots.test.js.snap
+++ b/storybook/__tests__/__snapshots__/storyshots.test.js.snap
@@ -42,12 +42,52 @@ exports[`Storyshots ATOMS/Avatar with default modifiers 1`] = `
`;
-exports[`Storyshots ATOMS/Button with children 1`] = `
-.emotion-1 {
+exports[`Storyshots ATOMS/Breadcrumbs with custom tagName 1`] = `
+.emotion-8 {
margin: 2rem;
}
+.emotion-1 {
+ padding: 0 1rem;
+ font-size: 1.6rem;
+ font-family: Poppins;
+}
+
+.emotion-7 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-box-pack: start;
+ -webkit-justify-content: flex-start;
+ -ms-flex-pack: start;
+ justify-content: flex-start;
+ -webkit-align-content: flex-start;
+ -ms-flex-line-pack: start;
+ align-content: flex-start;
+ -webkit-align-items: flex-start;
+ -webkit-box-align: flex-start;
+ -ms-flex-align: flex-start;
+ align-items: flex-start;
+ cursor: inherit;
+}
+
+.emotion-7 > *:not(:last-child) {
+ margin-right: 1rem;
+}
+
.emotion-0 {
+ cursor: pointer;
+ font-family: Poppins;
+ font-weight: 400;
+ line-height: 1;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ font-size: 1.4rem;
+ color: #4DA1FF;
outline: none;
text-align: center;
-webkit-text-decoration: none;
@@ -79,6 +119,11 @@ exports[`Storyshots ATOMS/Button with children 1`] = `
border-radius: .5rem;
}
+.emotion-0:hover {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
.emotion-0:hover {
box-shadow: 0 1px 3px 0 rgba(50,50,93,.14),0 4px 6px 0 rgba(112,157,199,.08);
}
@@ -88,20 +133,140 @@ exports[`Storyshots ATOMS/Button with children 1`] = `
}
`;
-exports[`Storyshots ATOMS/Button with custom colors and variant 1`] = `
-.emotion-13 {
+exports[`Storyshots ATOMS/Breadcrumbs with default modifiers 1`] = `
+.emotion-8 {
+ margin: 2rem;
+}
+
+.emotion-0 {
+ cursor: pointer;
+ font-family: Poppins;
+ font-weight: 400;
+ line-height: 1;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ font-size: 1.4rem;
+ color: #4DA1FF;
+}
+
+.emotion-0:hover {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-1 {
+ padding: 0 1rem;
+ font-size: 1.6rem;
+ font-family: Poppins;
+}
+
+
+`;
+
+exports[`Storyshots ATOMS/Button with children 1`] = `
+.emotion-1 {
margin: 2rem;
}
@@ -145,6 +310,24 @@ exports[`Storyshots ATOMS/Button with custom colors and variant 1`] = `
margin-right: .5rem;
}
+
+
+
+`;
+
+exports[`Storyshots ATOMS/Button with custom colors and variant 1`] = `
+.emotion-13 {
+ margin: 2rem;
+}
+
.emotion-5 {
display: -webkit-box;
display: -webkit-flex;
@@ -171,6 +354,46 @@ exports[`Storyshots ATOMS/Button with custom colors and variant 1`] = `
margin-right: 1rem;
}
+.emotion-0 {
+ outline: none;
+ text-align: center;
+ -webkit-text-decoration: none;
+ text-decoration: none;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-pack: center;
+ -webkit-justify-content: center;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -webkit-align-items: center;
+ -webkit-box-align: center;
+ -ms-flex-align: center;
+ align-items: center;
+ cursor: pointer;
+ font-size: 1.4rem;
+ font-weight: 600;
+ -webkit-transition: all .15s ease-in-out;
+ transition: all .15s ease-in-out;
+ border-style: solid;
+ border-width: 1px;
+ border-color: #4DA1FF;
+ background-color: #4DA1FF;
+ color: rgba(255,255,255,1);
+ height: 4rem;
+ padding: 0 4rem;
+ border-radius: .5rem;
+}
+
+.emotion-0:hover {
+ box-shadow: 0 1px 3px 0 rgba(50,50,93,.14),0 4px 6px 0 rgba(112,157,199,.08);
+}
+
+.emotion-0 > *:not(:last-child) {
+ margin-right: .5rem;
+}
+
.emotion-12 {
display: -webkit-box;
display: -webkit-flex;
@@ -655,6 +878,32 @@ exports[`Storyshots ATOMS/Button with custom sizes 1`] = `
margin: 2rem;
}
+.emotion-3 {
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-direction: row;
+ -ms-flex-direction: row;
+ flex-direction: row;
+ -webkit-box-pack: start;
+ -webkit-justify-content: flex-start;
+ -ms-flex-pack: start;
+ justify-content: flex-start;
+ -webkit-align-content: flex-start;
+ -ms-flex-line-pack: start;
+ align-content: flex-start;
+ -webkit-align-items: flex-start;
+ -webkit-box-align: flex-start;
+ -ms-flex-align: flex-start;
+ align-items: flex-start;
+ cursor: inherit;
+}
+
+.emotion-3 > *:not(:last-child) {
+ margin-right: 1rem;
+}
+
.emotion-1 {
outline: none;
text-align: center;
@@ -695,32 +944,6 @@ exports[`Storyshots ATOMS/Button with custom sizes 1`] = `
margin-right: .5rem;
}
-.emotion-3 {
- display: -webkit-box;
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- -webkit-flex-direction: row;
- -ms-flex-direction: row;
- flex-direction: row;
- -webkit-box-pack: start;
- -webkit-justify-content: flex-start;
- -ms-flex-pack: start;
- justify-content: flex-start;
- -webkit-align-content: flex-start;
- -ms-flex-line-pack: start;
- align-content: flex-start;
- -webkit-align-items: flex-start;
- -webkit-box-align: flex-start;
- -ms-flex-align: flex-start;
- align-items: flex-start;
- cursor: inherit;
-}
-
-.emotion-3 > *:not(:last-child) {
- margin-right: 1rem;
-}
-
.emotion-0 {
outline: none;
text-align: center;