Repository: uber-web/baseui
Pull request #960 (2019-02-26 20:27:46)

docs: update the installation page
- `styletron-react-core` is a legacy version of `styletron-react`, should not be installed
- `styletron-standard` is a dep of `styletron-react`, no need to install separately
- adding links to styletron.js.org

**DON'T merge until styletron.js.org goes alive**


Patch:
diff --git a/documentation-site/pages/components/link.mdx b/documentation-site/pages/components/link.mdx
new file mode 100644
index 000000000..fc8ee2154
--- /dev/null
+++ b/documentation-site/pages/components/link.mdx
@@ -0,0 +1,29 @@
+<!--
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+-->
+
+import Example from '../../components/example';
+import API from '../../components/api';
+import Layout from '../../components/layout';
+
+import Basic from 'examples/link/basic.js';
+
+export default Layout;
+
+# Link
+
+A simple styled anchor element.
+
+## Examples
+
+<Example title="basic" path="examples/link/basic.js">
+  <Basic />
+</Example>
+
+<API
+  heading="Link API"
+  api={require('!!@uber-web-ui/extract-react-types-loader!../../../src/link/index.js')}
+/>
diff --git a/documentation-site/pages/components/progress-bar.mdx b/documentation-site/pages/components/progress-bar.mdx
index 91db4978a..7399ee293 100644
--- a/documentation-site/pages/components/progress-bar.mdx
+++ b/documentation-site/pages/components/progress-bar.mdx
@@ -10,6 +10,7 @@ import API from '../../components/api';
 import Layout from '../../components/layout';
 
 import Basic from 'examples/progress-bar/basic.js';
+import Negative from 'examples/progress-bar/negative.js';
 import WithLabel from 'examples/progress-bar/with-label.js';
 import CustomLabel from 'examples/progress-bar/custom-label.js';
 import SuccessValue from 'examples/progress-bar/success-value.js';
@@ -35,6 +36,13 @@ Indicates a wait time for deterministic actions- either within an experience flo
   <Basic />
 </Example>
 
+<Example
+  title="Progress bar with Negative Color"
+  path="examples/progress-bar/negative.js"
+>
+  <Negative />
+</Example>
+
 <Example
   title="Progress bar with Visible Label"
   path="examples/progress-bar/with-label.js"
diff --git a/documentation-site/pages/components/spinner.mdx b/documentation-site/pages/components/spinner.mdx
index 8b6b5fd81..414bf511a 100644
--- a/documentation-site/pages/components/spinner.mdx
+++ b/documentation-site/pages/components/spinner.mdx
@@ -10,6 +10,7 @@ import API from '../../components/api';
 import Layout from '../../components/layout';
 
 import SpinnerBasic from 'examples/spinner/basic.js';
+import SpinnerNegative from 'examples/spinner/negative.js';
 import SpinnerWithSize from 'examples/spinner/with-size.js';
 import SpinnerWithOverrides from 'examples/spinner/with-overrides.js';
 
@@ -33,6 +34,13 @@ Spinners provide a visual cue that an action is processing awaiting a course of
   <SpinnerBasic />
 </Example>
 
+<Example
+  title="Spinner with Negative Color"
+  path="examples/spinner/negative.js"
+>
+  <SpinnerNegative />
+</Example>
+
 <Example
   title="Spinner with different size"
   path="examples/spinner/with-size.js"
diff --git a/documentation-site/pages/components/table.mdx b/documentation-site/pages/components/table.mdx
index fa4f10400..5a6d6a376 100644
--- a/documentation-site/pages/components/table.mdx
+++ b/documentation-site/pages/components/table.mdx
@@ -11,9 +11,11 @@ import Layout from '../../components/layout';
 
 import Basic from 'examples/table/basic.js';
 import Cells from 'examples/table/cells.js';
+import Pagination from 'examples/table/pagination.js';
 import FixedWidthColumn from 'examples/table/fixed-width-column.js';
 import VerticalScroll from 'examples/table/vertical-scroll.js';
 import HorizontalScroll from 'examples/table/horizontal-scroll.js';
+import Sortable from 'examples/table/sortable.js';
 import Filter from 'examples/table/filter.js';
 import Graph from 'examples/table/graph.js';
 import Virtual from 'examples/table/virtual.js';
@@ -35,6 +37,14 @@ Display larger amounts of information in a scannable UI.
   <Cells />
 </Example>
 
+<Example title="Table with pagination" path="examples/table/pagination.js">
+  <Pagination />
+</Example>
+
+<Example title="Table with sorting by a column" path="examples/table/sortable.js">
+  <Sortable />
+</Example>
+
 <Example
   title="Table with fixed width column"
   path="examples/table/fixed-width-column.js"
diff --git a/documentation-site/pages/getting-started/installation.mdx b/documentation-site/pages/getting-started/installation.mdx
index 239d57eca..73a1247fb 100644
--- a/documentation-site/pages/getting-started/installation.mdx
+++ b/documentation-site/pages/getting-started/installation.mdx
@@ -11,23 +11,25 @@ export default Layout;
 
 # Installing Base UI
 
-Base UI is available on [npm](https://npmjs.com/package/baseui).
+Base UI is available on [npm](https://npmjs.com/package/baseui). This single package contains all Base UI components.
 
 ```bash
 # using yarn
-yarn add baseui styletron-react styletron-react-core styletron-standard styletron-engine-atomic
+yarn add baseui styletron-engine-atomic styletron-react
 
 # using npm
-npm install baseui styletron-react styletron-react-core styletron-standard styletron-engine-atomic
+npm install baseui styletron-engine-atomic styletron-react
 ```
 
-[Styletron](https://styletron.js.org/) is a toolkit for component-oriented styling, part of the CSS in JS family. Base UI uses
-Styletron as a peer dependency to style all the elements, so you need to add them as dependencies too.
+[Styletron](https://styletron.js.org/) is a toolkit for component-oriented styling, part of the CSS in JS family. Base UI uses Styletron as a peer dependency to style all the elements, so you need to add them as dependencies too.
+
+**The next step is to setup Styletron in your application.** There are [detailed guides](https://styletron.js.org/getting-started/) for Fusion.js, Next.js, Gatsby and general React applications.
 
 ## With Fusion.js
 
-If you are building a web application on top of [Fusion.js](https://fusionjs.com/), all you have to install is the
-`fusion-plugin-styletron-react` plugin, and register the plugin.
+If you are building a web application on top of [Fusion.js](https://fusionjs.com/), you have to install `fusion-plugin-styletron-react` and `styletron-react`, and register the `fusion-plugin-styletron-react` plugin. It might be installed already if you bootstrapped your Fusion application.
+
+### Adding dependencies
 
 ```bash
 # using yarn
@@ -37,24 +39,24 @@ yarn add baseui fusion-plugin-styletron-react styletron-react
 npm install baseui fusion-plugin-styletron-react styletron-react
 ```
 
+### Registering the plugin
+
 ```javascript
 // src/main.js
 import App from 'fusion-react';
 import Styletron from 'fusion-plugin-styletron-react';
-
 import root from './components/root';
 
 export default () => {
   const app = new App(root);
-
   app.register(Styletron);
-
   return app;
-}
+};
 ```
 
-For more information, check out the documentation site for
-[fusion-plugin-styletron-react](https://fusionjs.com/api/fusion-plugin-styletron-react).
+For more information, check out the documentation site for [fusion-plugin-styletron-react](https://fusionjs.com/api/fusion-plugin-styletron-react).
+
+Do you want to learn more about Styletron? Go to [styletron.js.org](https://styletron.js.org/).
 
 # Next step
 
diff --git a/documentation-site/routes.js b/documentation-site/routes.js
index ed6d9239c..d96a3a63e 100644
--- a/documentation-site/routes.js
+++ b/documentation-site/routes.js
@@ -107,6 +107,10 @@ const routes = [
             text: 'Header navigation',
             path: '/components/header-navigation',
           },
+          {
+            text: 'Link',
+            path: '/components/link',
+          },
           {
             text: 'Pagination',
             path: '/components/pagination',
diff --git a/documentation-site/static/examples/link/basic.js b/documentation-site/static/examples/link/basic.js
new file mode 100644
index 000000000..70eac61e1
--- /dev/null
+++ b/documentation-site/static/examples/link/basic.js
@@ -0,0 +1,6 @@
+import React from 'react';
+import {StyledLink} from 'baseui/link';
+
+export default () => (
+  <StyledLink href="https://baseui.design">Link to baseui.design</StyledLink>
+);
diff --git a/documentation-site/static/examples/progress-bar/negative.js b/documentation-site/static/examples/progress-bar/negative.js
new file mode 100644
index 000000000..bc5d396bf
--- /dev/null
+++ b/documentation-site/static/examples/progress-bar/negative.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import {ProgressBar} from 'baseui/progress-bar';
+
+const SUCCESS_VALUE = 100;
+
+export default class Basic extends React.Component {
+  state = {value: 0};
+
+  componentDidMount() {
+    setInterval(() => {
+      if (this.state.value < SUCCESS_VALUE) {
+        this.setState({value: this.state.value + 10});
+      } else {
+        this.setState({value: 0});
+      }
+    }, 1000);
+  }
+
+  render() {
+    return (
+      <ProgressBar
+        value={this.state.value}
+        successValue={SUCCESS_VALUE}
+        overrides={{
+          BarProgress: {
+            style: ({$theme}) => ({backgroundColor: $theme.colors.negative}),
+          },
+        }}
+      />
+    );
+  }
+}
diff --git a/documentation-site/static/examples/spinner/negative.js b/documentation-site/static/examples/spinner/negative.js
new file mode 100644
index 000000000..98ac7089a
--- /dev/null
+++ b/documentation-site/static/examples/spinner/negative.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import {Spinner} from 'baseui/spinner';
+
+export default () => (
+  <Spinner
+    overrides={{
+      ActivePath: {style: ({$theme}) => ({fill: $theme.colors.negative})},
+    }}
+  />
+);
diff --git a/documentation-site/static/examples/table/cells.js b/documentation-site/static/examples/table/cells.js
index 2bd7f2241..fc9c6384d 100644
--- a/documentation-site/static/examples/table/cells.js
+++ b/documentation-site/static/examples/table/cells.js
@@ -8,7 +8,6 @@ import Search from 'baseui/icon/search';
 import Plus from 'baseui/icon/plus';
 import Delete from 'baseui/icon/delete';
 import Overflow from 'baseui/icon/overflow';
-import {StatefulPanel} from 'baseui/accordion';
 import {
   StyledTable,
   StyledHead,
@@ -36,59 +35,6 @@ const StyledLargeText = styled(StyledCell, {
   alignItems: 'center',
 });
 
-const ExpandableCellHead = styled(StyledHeadCell, {
-  paddingTop: '0',
-  paddingBottom: '0',
-  paddingLeft: '0',
-  paddingRight: '0',
-  minWidth: '180px',
-});
-
-const ExpandableCell = styled(StyledCell, {
-  paddingTop: '0',
-  paddingBottom: '0',
-  paddingLeft: '0',
-  paddingRight: '0',
-  minWidth: '180px',
-});
-
-const panelOverrides = {
-  Header: {
-    style: ({$theme: {colors, sizing, typography}, $expanded}) => ({
-      ...typography.font300,
-      color: 'inherit',
-      borderBottom: $expanded
-        ? `1px solid ${colors.mono500}`
-        : '1px solid transparent',
-      paddingTop: sizing.scale300,
-      paddingBottom: sizing.scale300,
-      paddingLeft: sizing.scale1000,
-      paddingRight: sizing.scale600,
-      outline: 'none',
-      ':hover': {
-        color: 'inherit',
-      },
-    }),
-  },
-  Content: {
-    style: ({$theme: {colors, sizing}, $expanded}) => ({
-      backgroundColor: colors.mono200,
-      color: colors.mono800,
-      paddingTop: $expanded ? sizing.scale300 : 0,
-      paddingBottom: $expanded ? sizing.scale600 : 0,
-      paddingLeft: sizing.scale1000,
-      paddingRight: sizing.scale600,
-    }),
-  },
-  ToggleIcon: {
-    style: ({$theme: {colors}}) => ({
-      color: colors.mono600,
-      position: 'absolute',
-      left: '12px',
-    }),
-  },
-};
-
 const DATA = [
   [
     'Marlyn',
@@ -264,9 +210,6 @@ export default () => (
         <StyledHeadCell>Role</StyledHeadCell>
         <StyledHeadCell>Delta</StyledHeadCell>
         <StyledHeadCell>Amount</StyledHeadCell>
-        <ExpandableCellHead>
-          <StyledHeadCell>Expandable</StyledHeadCell>
-        </ExpandableCellHead>
         <StyledHeadCell>Actions</StyledHeadCell>
       </StyledHead>
       <StyledBody $width="1000px">
@@ -297,12 +240,6 @@ export default () => (
               </Block>
             </StyledLargeText>
 
-            <ExpandableCell>
-              <StatefulPanel title={row[5].title} overrides={panelOverrides}>
-                {row[5].data}
-              </StatefulPanel>
-            </ExpandableCell>
-
             <StyledCell>
               <StyledAction>
                 <Search />
diff --git a/documentation-site/static/examples/table/pagination.js b/documentation-site/static/examples/table/pagination.js
new file mode 100644
index 000000000..013c1ab0f
--- /dev/null
+++ b/documentation-site/static/examples/table/pagination.js
@@ -0,0 +1,98 @@
+import React from 'react';
+import {Block} from 'baseui/block';
+import {Button, KIND} from 'baseui/button';
+import TriangleDown from 'baseui/icon/triangle-down';
+import {StatefulMenu} from 'baseui/menu';
+import {Pagination} from 'baseui/pagination';
+import {StatefulPopover, PLACEMENT} from 'baseui/popover';
+import {Table} from 'baseui/table';
+
+class PaginatedTable extends React.Component<any, any> {
+  state = {page: 1, limit: 12};
+
+  handlePageChange = nextPage => {
+    if (nextPage < 1) {
+      return;
+    }
+    if (nextPage > Math.ceil(this.props.data.length / this.state.limit)) {
+      return;
+    }
+    this.setState({page: nextPage});
+  };
+
+  handleLimitChange = (nextLimit: number) => {
+    const nextPageNum = Math.ceil(this.props.data.length / nextLimit);
+    if (nextPageNum < this.state.page) {
+      this.setState({limit: nextLimit, page: nextPageNum});
+    } else {
+      this.setState({limit: nextLimit});
+    }
+  };
+
+  window = () => {
+    const min = (this.state.page - 1) * this.state.limit;
+    return this.props.data.slice(min, min + this.state.limit);
+  };
+
+  render() {
+    return (
+      <>
+        <Block
+          display="flex"
+          justifyContent="space-between"
+          paddingTop="scale600"
+          paddingBottom="scale600"
+        >
+          <Block font="font700">Table Example</Block>
+          <Button>
+            <Block paddingLeft="scale1200" paddingRight="scale1200">
+              Action
+            </Block>
+          </Button>
+        </Block>
+        <Block height="500px">
+          <Table columns={this.props.columns} data={this.window()} />
+        </Block>
+        <Block
+          paddingTop="scale600"
+          paddingRight="scale800"
+          paddingBottom="scale600"
+          paddingLeft="scale800"
+          display="flex"
+          justifyContent="space-between"
+        >
+          <StatefulPopover
+            content={({close}) => (
+              <StatefulMenu
+                items={[...new Array(100)].map((_, i) => ({label: i + 1}))}
+                onItemSelect={({item}) => {
+                  this.handleLimitChange(item.label);
+                  close();
+                }}
+                overrides={{List: {style: {height: '150px', width: '100px'}}}}
+              />
+            )}
+            placement={PLACEMENT.bottom}
+          >
+            <Button kind={KIND.tertiary} endEnhancer={TriangleDown}>
+              {`${this.state.limit} Rows`}
+            </Button>
+          </StatefulPopover>
+
+          <Pagination
+            currentPage={this.state.page}
+            numPages={Math.ceil(this.props.data.length / this.state.limit)}
+            onPageChange={({nextPage}) => this.handlePageChange(nextPage)}
+          />
+        </Block>
+      </>
+    );
+  }
+}
+
+const COLUMNS = [...new Array(5)].map(() => 'Label');
+const DATA = [...new Array(45)].map((_, i) =>
+  [...new Array(5)].map(() => `row: ${i + 1}`),
+);
+
+export default () => <PaginatedTable columns={COLUMNS} data={DATA} />;
diff --git a/documentation-site/static/examples/table/sortable.js b/documentation-site/static/examples/table/sortable.js
new file mode 100644
index 000000000..6c26be333
--- /dev/null
+++ b/documentation-site/static/examples/table/sortable.js
@@ -0,0 +1,119 @@
+import React from 'react';
+
+import {
+  StyledTable,
+  StyledHead,
+  StyledBody,
+  StyledRow,
+  StyledCell,
+  SortableHeadCell,
+} from 'baseui/table';
+
+const DATA = [
+  ['Marlyn', 10],
+  ['Luther', 15],
+  ['Kiera', 13],
+  ['Edna', 20],
+  ['Soraya', 18],
+  ['Dorris', 32],
+  ['Astrid', 26],
+  ['Wendie', 17],
+  ['Marna', 11],
+  ['Malka', 14],
+  ['Jospeh', 10],
+  ['Roselee', 12],
+  ['Justine', 35],
+  ['Marlon', 30],
+  ['Mellissa', 32],
+  ['Fausto', 21],
+  ['Alfredia', 22],
+  ['Abel', 18],
+  ['Winford', 19],
+  ['Neil', 27],
+];
+
+export default class SortableTable extends React.Component {
+  state = {nameDirection: null, ageDirection: null};
+
+  handleSort = (title, prevDirection) => {
+    let nextDirection = null;
+    if (prevDirection === 'ASC') {
+      nextDirection = 'DESC';
+    }
+    if (prevDirection === 'DESC') {
+      nextDirection = null;
+    }
+    if (prevDirection === null) {
+      nextDirection = 'ASC';
+    }
+
+    if (title === 'name') {
+      this.setState({nameDirection: nextDirection, ageDirection: null});
+      return;
+    }
+
+    if (title === 'age') {
+      this.setState({nameDirection: null, ageDirection: nextDirection});
+      return;
+    }
+  };
+
+  getSortedData = () => {
+    if (this.state.nameDirection) {
+      const sorted = DATA.slice(0).sort((a, b) => a[0].localeCompare(b[0]));
+
+      if (this.state.nameDirection === 'ASC') {
+        return sorted;
+      }
+
+      if (this.state.nameDirection === 'DESC') {
+        return sorted.reverse();
+      }
+    }
+
+    if (this.state.ageDirection) {
+      const sorted = DATA.slice(0).sort((a, b) => a[1] - b[1]);
+      if (this.state.ageDirection === 'ASC') {
+        return sorted;
+      }
+
+      if (this.state.ageDirection === 'DESC') {
+        return sorted.reverse();
+      }
+    }
+
+    return DATA;
+  };
+
+  render() {
+    return (
+      <div style={{height: '500px', width: '400px'}}>
+        <StyledTable>
+          <StyledHead>
+            <SortableHeadCell
+              title="Name"
+              direction={this.state.nameDirection}
+              onSort={() => this.handleSort('name', this.state.nameDirection)}
+            >
+              <button onClick={() => console.log('filter')}>f</button>
+            </SortableHeadCell>
+            <SortableHeadCell
+              title="Age"
+              direction={this.state.ageDirection}
+              onSort={() => this.handleSort('age', this.state.ageDirection)}
+            />
+          </StyledHead>
+          <StyledBody>
+            {this.getSortedData().map((row, index) => (
+              <StyledRow key={index}>
+                {row.map((cell, cellIndex) => (
+                  <StyledCell key={cellIndex}>{cell}</StyledCell>
+                ))}
+              </StyledRow>
+            ))}
+          </StyledBody>
+        </StyledTable>
+      </div>
+    );
+  }
+}
diff --git a/package.json b/package.json
index 14783edd5..84cfd1751 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "baseui",
-  "version": "6.2.0",
+  "version": "6.3.0",
   "description": "React implementation of uber design components platform.",
   "keywords": [
     "react",
diff --git a/src/avatar/styled-components.js b/src/avatar/styled-components.js
index ac1110e5b..c43c927fa 100644
--- a/src/avatar/styled-components.js
+++ b/src/avatar/styled-components.js
@@ -43,7 +43,7 @@ export const Root = styled('div', (props: StylePropsT) => {
   const themedSize = getSize(props);
 
   return {
-    backgroundColor: props.$theme.colors.primary,
+    backgroundColor: $didImageFailToLoad ? props.$theme.colors.primary : null,
     borderRadius: '50%',
     boxSizing: 'border-box',
     display: 'inline-block',
diff --git a/src/spinner/index.js b/src/spinner/index.js
index 1e8709c76..bf00c8a05 100644
--- a/src/spinner/index.js
+++ b/src/spinner/index.js
@@ -7,6 +7,10 @@ LICENSE file in the root directory of this source tree.
 // @flow
 export {default as Spinner} from './spinner.js';
 // Styled elements
-export {Svg as StyledSvg} from './styled-components.js';
+export {
+  Svg as StyledSvg,
+  StyledTrackPath,
+  StyledActivePath,
+} from './styled-components.js';
 // Flow
 export * from './types.js';
diff --git a/src/spinner/spinner.js b/src/spinner/spinner.js
index d95429e65..905c4876c 100644
--- a/src/spinner/spinner.js
+++ b/src/spinner/spinner.js
@@ -6,10 +6,15 @@ LICENSE file in the root directory of this source tree.
 */
 // @flow
 import * as React from 'react';
-import {mergeOverrides} from '../helpers/overrides.js';
-import {Spinner as SpinnerIcon} from '../icon/index.js';
-import {Svg as StyledSvg} from './styled-components.js';
 
+import {mergeOverrides, getOverrides} from '../helpers/overrides.js';
+import {Icon} from '../icon/index.js';
+
+import {
+  Svg as StyledSvg,
+  StyledActivePath,
+  StyledTrackPath,
+} from './styled-components.js';
 import type {SpinnerPropsT} from './types.js';
 
 class Spinner extends React.Component<SpinnerPropsT> {
@@ -21,14 +26,37 @@ class Spinner extends React.Component<SpinnerPropsT> {
   };
 
   render() {
-    const overrides = mergeOverrides({Svg: StyledSvg}, this.props.overrides);
+    const {overrides = {}} = this.props;
+    const mergedOverrides = mergeOverrides({Svg: StyledSvg}, overrides);
+    const [TrackPath, trackPathProps] = getOverrides(
+      overrides.TrackPath,
+      StyledTrackPath,
+    );
+    const [ActivePath, activePathProps] = getOverrides(
+      overrides.ActivePath,
+      StyledActivePath,
+    );
+
     return (
-      <SpinnerIcon
-        role="progressbar"
+      <Icon
+        title="Spinner"
         viewBox="3 3 18 18"
         {...this.props}
-        overrides={overrides}
-      />
+        overrides={mergedOverrides}
+      >
+        <TrackPath
+          fillRule="evenodd"
+          clipRule="evenodd"
+          d="M12 5C8.13401 5 5 8.13401 5 12C5 15.866 8.13401 19 12 19C15.866 19 19 15.866 19 12C19 8.13401 15.866 5 12 5ZM3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12Z"
+          {...trackPathProps}
+        />
+        <ActivePath
+          fillRule="evenodd"
+          clipRule="evenodd"
+          d="M16.9497 7.05015C14.2161 4.31648 9.78392 4.31648 7.05025 7.05015C6.65973 7.44067 6.02656 7.44067 5.63604 7.05015C5.24551 6.65962 5.24551 6.02646 5.63604 5.63593C9.15076 2.12121 14.8492 2.12121 18.364 5.63593C18.7545 6.02646 18.7545 6.65962 18.364 7.05015C17.9734 7.44067 17.3403 7.44067 16.9497 7.05015Z"
+          {...activePathProps}
+        />
+      </Icon>
     );
   }
 }
diff --git a/src/spinner/styled-components.js b/src/spinner/styled-components.js
index 1e9879304..79b3429cf 100644
--- a/src/spinner/styled-components.js
+++ b/src/spinner/styled-components.js
@@ -32,3 +32,11 @@ export const Svg = styled('svg', props => {
     animationTimingFunction: 'linear',
   };
 });
+
+export const StyledTrackPath = styled('path', props => ({
+  fill: props.$theme.colors.mono400,
+}));
+
+export const StyledActivePath = styled('path', props => ({
+  fill: props.$color || props.$theme.colors.primary400,
+}));
diff --git a/src/spinner/types.js b/src/spinner/types.js
index b88e81f54..d485f413b 100644
--- a/src/spinner/types.js
+++ b/src/spinner/types.js
@@ -5,4 +5,19 @@ This source code is licensed under the MIT license found in the
 LICENSE file in the root directory of this source tree.
 */
 // @flow
-export type {IconPropsT as SpinnerPropsT} from '../icon/types.js';
+
+import type {OverrideT} from '../helpers/overrides.js';
+
+export type SpinnerPropsT = {
+  /** Size of element, will be passed to the svg width/height style. Can also be a value included in */
+  size?: number | string,
+  /** Color of icon, will be used as svg fill */
+  color?: string,
+  /** Allows you to set the SVG `<title>` label, which is used for accessibility */
+  title?: string,
+  overrides?: {
+    Svg?: OverrideT<*>,
+    ActivePath?: OverrideT<*>,
+    TrackPath?: OverrideT<*>,
+  },
+};
diff --git a/src/table/index.js b/src/table/index.js
index 0d2e454d9..f0657bcdb 100644
--- a/src/table/index.js
+++ b/src/table/index.js
@@ -5,6 +5,7 @@ This source code is licensed under the MIT license found in the
 LICENSE file in the root directory of this source tree.
 */
 // @flow
+export {default as SortableHeadCell} from './sortable-head-cell.js';
 export {default as Table} from './table.js';
 export {default as Filter} from './filter.js';
 // Styled elements
diff --git a/src/table/sortable-head-cell.js b/src/table/sortable-head-cell.js
new file mode 100644
index 000000000..5e5e0973c
--- /dev/null
+++ b/src/table/sortable-head-cell.js
@@ -0,0 +1,58 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import * as React from 'react';
+
+import {getOverrides} from '../helpers/overrides.js';
+import TriangleDown from '../icon/triangle-down.js';
+import TriangleUp from '../icon/triangle-up.js';
+
+import {StyledHeadCell, StyledSortableLabel} from './styled-components.js';
+import type {HeadCellPropsT} from './types.js';
+
+function Direction(props: {direction: 'ASC' | 'DESC' | null}) {
+  if (props.direction === 'ASC') {
+    return <TriangleDown />;
+  }
+
+  if (props.direction === 'DESC') {
+    return <TriangleUp />;
+  }
+
+  return null;
+}
+
+export default function SortableHeadCell(props: HeadCellPropsT) {
+  const {overrides = {}} = props;
+
+  const [HeadCell, headCellProps] = getOverrides(
+    overrides.HeadCell,
+    StyledHeadCell,
+  );
+  const [SortableLabel, sortableLabelProps] = getOverrides(
+    overrides.SortableLabel,
+    StyledSortableLabel,
+  );
+
+  return (
+    <HeadCell role="columnheader" {...headCellProps}>
+      <SortableLabel
+        aria-label={`sorts table by ${props.title} column`}
+        disabled={props.disabled}
+        onClick={() => {
+          props.onSort && props.onSort();
+        }}
+        {...sortableLabelProps}
+      >
+        <Direction direction={props.direction} />
+        {props.title}
+      </SortableLabel>
+      {props.children}
+    </HeadCell>
+  );
+}
diff --git a/src/table/styled-components.js b/src/table/styled-components.js
index 36ad901e2..4b209a150 100644
--- a/src/table/styled-components.js
+++ b/src/table/styled-components.js
@@ -58,6 +58,22 @@ export const StyledHeadCell = styled('div', ({$theme}: SharedStylePropsT) => {
   };
 });
 
+export const StyledSortableLabel = styled('button', ({$theme}) => {
+  return {
+    ...$theme.typography.font350,
+    alignItems: 'center',
+    border: 'none',
+    display: 'flex',
+    padding: 0,
+    ':hover:enabled': {
+      cursor: 'pointer',
+    },
+    ':disabled': {
+      color: $theme.colors.mono500,
+    },
+  };
+});
+
 export const StyledBody = styled('div', ({$width}: HorizontalStyleProps) => {
   return {
     width: $width ? $width : '100%',
diff --git a/src/table/types.js b/src/table/types.js
index d48db3a5e..1fe6f80df 100644
--- a/src/table/types.js
+++ b/src/table/types.js
@@ -11,6 +11,23 @@ import * as React from 'react';
 import type {OverrideT} from '../helpers/overrides.js';
 import type {ThemeT} from '../styles/types.js';
 
+export type HeadCellPropsT = {|
+  /** Additional content to fill head cell. Most likely used for a filter button. */
+  children?: React.Node,
+  /** Visually indicates sort direction. Displays a chevron next to column title. */
+  direction: 'ASC' | 'DESC' | null,
+  /** Disabled click-to-sort. Renders column title in muted color. */
+  disabled?: boolean,
+  /** Callback for when column title is clicked to trigger sort. */
+  onSort?: () => mixed,
+  overrides?: {
+    HeadCell: OverrideT<*>,
+    SortableLabel: OverrideT<*>,
+  },
+  /** Column title. */
+  title: string,
+|};
+
 export type TablePropsT = {|
   /** Table columns. Data passed to each header cell. */
   columns: Array<string | React.Node>,


Test patch:
diff --git a/src/avatar/__tests__/__snapshots__/styled-components.test.js.snap b/src/avatar/__tests__/__snapshots__/styled-components.test.js.snap
index 0a14107de..776583620 100644
--- a/src/avatar/__tests__/__snapshots__/styled-components.test.js.snap
+++ b/src/avatar/__tests__/__snapshots__/styled-components.test.js.snap
@@ -23,7 +23,7 @@ Object {
 
 exports[`Avatar styled components default properties: StyledRoot has expected default styles 1`] = `
 Object {
-  "backgroundColor": "$theme.colors.primary",
+  "backgroundColor": null,
   "borderRadius": "50%",
   "boxSizing": "border-box",
   "display": "inline-block",
@@ -44,7 +44,7 @@ Object {
 
 exports[`Avatar styled components non-default size: StyledRoot has expected styles with non-default size 1`] = `
 Object {
-  "backgroundColor": "$theme.colors.primary",
+  "backgroundColor": null,
   "borderRadius": "50%",
   "boxSizing": "border-box",
   "display": "inline-block",
diff --git a/src/progress-bar/__tests__/progressbar-negative.scenario.js b/src/progress-bar/__tests__/progressbar-negative.scenario.js
new file mode 100644
index 000000000..ee533f773
--- /dev/null
+++ b/src/progress-bar/__tests__/progressbar-negative.scenario.js
@@ -0,0 +1,24 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+
+import {ProgressBar} from '../index.js';
+
+export const name = 'progress-bar-negative';
+
+export const component = () => (
+  <ProgressBar
+    value={20}
+    overrides={{
+      BarProgress: {
+        style: ({$theme}) => ({backgroundColor: $theme.colors.negative}),
+      },
+    }}
+  />
+);
diff --git a/src/spinner/__tests__/__snapshots__/styled-components.test.js.snap b/src/spinner/__tests__/__snapshots__/styled-components.test.js.snap
index a3c7b8b19..5cf0930eb 100644
--- a/src/spinner/__tests__/__snapshots__/styled-components.test.js.snap
+++ b/src/spinner/__tests__/__snapshots__/styled-components.test.js.snap
@@ -1,5 +1,11 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`Spinner styled components StyledActivePath - basic render: StyledSvg has correct fill value 1`] = `
+Object {
+  "fill": "$theme.colors.primary400",
+}
+`;
+
 exports[`Spinner styled components StyledSvg - basic render: StyledSvg has correct default styles 1`] = `
 Object {
   "animationDuration": "$theme.animation.timing700",
@@ -65,3 +71,9 @@ Object {
   "width": "$theme.sizing.scale600",
 }
 `;
+
+exports[`Spinner styled components StyledTrackPath - basic render: StyledSvg has correct fill value 1`] = `
+Object {
+  "fill": "$theme.colors.mono400",
+}
+`;
diff --git a/src/spinner/__tests__/component.test.js b/src/spinner/__tests__/component.test.js
index 524e0ce4e..f2d681ce1 100644
--- a/src/spinner/__tests__/component.test.js
+++ b/src/spinner/__tests__/component.test.js
@@ -9,21 +9,19 @@ LICENSE file in the root directory of this source tree.
 import React from 'react';
 import {mount} from 'enzyme';
 import {Spinner} from '../index.js';
-import {Spinner as SpinnerIcon} from '../../icon/index.js';
+import {Icon} from '../../icon/index.js';
 
 describe('Spinner', () => {
   test('renders spinner icon', () => {
     let renderedIcon;
     const spinner = mount(<Spinner />);
 
-    // renderedRoot = wrapper.find(StyledSvg).first();
-    renderedIcon = spinner.find(SpinnerIcon).first();
+    renderedIcon = spinner.find(Icon).first();
     expect(renderedIcon).toExist();
 
-    // pass new size value set to 56
     spinner.setProps({size: 56});
 
-    renderedIcon = spinner.find(SpinnerIcon).first();
+    renderedIcon = spinner.find(Icon).first();
     expect(renderedIcon.props().size).toBe(56);
   });
 
diff --git a/src/spinner/__tests__/spinner-negative.scenario.js b/src/spinner/__tests__/spinner-negative.scenario.js
new file mode 100644
index 000000000..3fc27fa1a
--- /dev/null
+++ b/src/spinner/__tests__/spinner-negative.scenario.js
@@ -0,0 +1,21 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+
+import {Spinner} from '../index.js';
+
+export const name = 'spinner-negative';
+
+export const component = () => (
+  <Spinner
+    overrides={{
+      ActivePath: {style: ({$theme}) => ({fill: $theme.colors.negative})},
+    }}
+  />
+);
diff --git a/src/spinner/__tests__/styled-components.test.js b/src/spinner/__tests__/styled-components.test.js
index 844a5058c..a18b07d0c 100644
--- a/src/spinner/__tests__/styled-components.test.js
+++ b/src/spinner/__tests__/styled-components.test.js
@@ -7,7 +7,7 @@ LICENSE file in the root directory of this source tree.
 // @flow
 import React from 'react';
 import {shallow} from 'enzyme';
-import {StyledSvg} from '../index.js';
+import {StyledSvg, StyledTrackPath, StyledActivePath} from '../index.js';
 
 describe('Spinner styled components', () => {
   test('StyledSvg - basic render', () => {
@@ -28,4 +28,18 @@ describe('Spinner styled components', () => {
       'StyledSvg has correct fill value',
     );
   });
+
+  test('StyledTrackPath - basic render', () => {
+    const component = shallow(<StyledTrackPath />);
+    expect(component.instance().getStyles()).toMatchSnapshot(
+      'StyledSvg has correct fill value',
+    );
+  });
+
+  test('StyledActivePath - basic render', () => {
+    const component = shallow(<StyledActivePath />);
+    expect(component.instance().getStyles()).toMatchSnapshot(
+      'StyledSvg has correct fill value',
+    );
+  });
 });
diff --git a/src/table/__tests__/sortable-head-cell.test.js b/src/table/__tests__/sortable-head-cell.test.js
new file mode 100644
index 000000000..4e789943d
--- /dev/null
+++ b/src/table/__tests__/sortable-head-cell.test.js
@@ -0,0 +1,48 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+import {mount} from 'enzyme';
+
+import TriangleDown from '../../icon/triangle-down.js';
+import TriangleUp from '../../icon/triangle-up.js';
+import {SortableHeadCell} from '../index.js';
+
+describe('sortable-head-cell', () => {
+  it('displays triangle down when direction is ASC', () => {
+    const wrapper = mount(<SortableHeadCell direction="ASC" title="test" />);
+    const down = wrapper.find(TriangleDown);
+    expect(down).toHaveLength(1);
+  });
+
+  it('displays triangle up when direction is DESC', () => {
+    const wrapper = mount(<SortableHeadCell direction="DESC" title="test" />);
+    const up = wrapper.find(TriangleUp);
+    expect(up).toHaveLength(1);
+  });
+
+  it('calls provided onSort fn on click', () => {
+    const spy = jest.fn();
+    const wrapper = mount(
+      <SortableHeadCell onSort={spy} direction={null} title="test" />,
+    );
+    const button = wrapper.find('button');
+    button.simulate('click');
+    expect(spy).toHaveBeenCalledTimes(1);
+  });
+
+  it('does not call provided onSort fn on click if disbled', () => {
+    const spy = jest.fn();
+    const wrapper = mount(
+      <SortableHeadCell onSort={spy} direction={null} title="test" disabled />,
+    );
+    const button = wrapper.find('button');
+    button.simulate('click');
+    expect(spy).toHaveBeenCalledTimes(0);
+  });
+});
diff --git a/src/table/__tests__/table-pagination.scenario.js b/src/table/__tests__/table-pagination.scenario.js
new file mode 100644
index 000000000..543a33b53
--- /dev/null
+++ b/src/table/__tests__/table-pagination.scenario.js
@@ -0,0 +1,111 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+
+import {Block} from '../../block/index.js';
+import {Button, KIND} from '../../button/index.js';
+import TriangleDown from '../../icon/triangle-down.js';
+import {StatefulMenu} from '../../menu/index.js';
+import {Pagination} from '../../pagination/index.js';
+import {StatefulPopover, PLACEMENT} from '../../popover/index.js';
+
+import {Table} from '../index.js';
+
+export const name = 'table-pagination';
+
+const COLUMNS = [...new Array(5)].map(() => 'Label');
+const DATA = [...new Array(45)].map((_, i) =>
+  [...new Array(5)].map(() => `row: ${i + 1}`),
+);
+
+// eslint-disable-next-line flowtype/no-weak-types
+class PaginatedTable extends React.Component<any, any> {
+  state = {page: 1, limit: 12};
+
+  handlePageChange = (nextPage: number) => {
+    if (nextPage < 1) {
+      return;
+    }
+    if (nextPage > Math.ceil(this.props.data.length / this.state.limit)) {
+      return;
+    }
+    this.setState({page: nextPage});
+  };
+
+  handleLimitChange = (nextLimit: number) => {
+    const nextPageNum = Math.ceil(this.props.data.length / nextLimit);
+    if (nextPageNum < this.state.page) {
+      this.setState({limit: nextLimit, page: nextPageNum});
+    } else {
+      this.setState({limit: nextLimit});
+    }
+  };
+
+  window = () => {
+    const min = (this.state.page - 1) * this.state.limit;
+    return this.props.data.slice(min, min + this.state.limit);
+  };
+
+  render() {
+    return (
+      <Block width="968px">
+        <Block
+          display="flex"
+          justifyContent="space-between"
+          paddingTop="scale600"
+          paddingBottom="scale600"
+        >
+          <Block font="font700">Table Example</Block>
+          <Button>
+            <Block paddingLeft="scale1200" paddingRight="scale1200">
+              Action
+            </Block>
+          </Button>
+        </Block>
+        <Block height="500px">
+          <Table columns={this.props.columns} data={this.window()} />
+        </Block>
+        <Block
+          paddingTop="scale600"
+          paddingRight="scale800"
+          paddingBottom="scale600"
+          paddingLeft="scale800"
+          display="flex"
+          justifyContent="space-between"
+        >
+          <StatefulPopover
+            content={({close}) => (
+              <StatefulMenu
+                items={[...new Array(100)].map((_, i) => ({label: i + 1}))}
+                onItemSelect={({item}) => {
+                  this.handleLimitChange(item.label);
+                  close();
+                }}
+                overrides={{List: {style: {height: '150px', width: '100px'}}}}
+              />
+            )}
+            placement={PLACEMENT.bottom}
+          >
+            <Button kind={KIND.tertiary} endEnhancer={TriangleDown}>
+              {`${this.state.limit} Rows`}
+            </Button>
+          </StatefulPopover>
+
+          <Pagination
+            currentPage={this.state.page}
+            numPages={Math.ceil(this.props.data.length / this.state.limit)}
+            onPageChange={({nextPage}) => this.handlePageChange(nextPage)}
+          />
+        </Block>
+      </Block>
+    );
+  }
+}
+
+export const component = () => <PaginatedTable columns={COLUMNS} data={DATA} />;
diff --git a/src/table/__tests__/table-sortable.scenario.js b/src/table/__tests__/table-sortable.scenario.js
new file mode 100644
index 000000000..44f7c23c8
--- /dev/null
+++ b/src/table/__tests__/table-sortable.scenario.js
@@ -0,0 +1,132 @@
+/*
+Copyright (c) 2018 Uber Technologies, Inc.
+
+This source code is licensed under the MIT license found in the
+LICENSE file in the root directory of this source tree.
+*/
+// @flow
+
+import React from 'react';
+
+import {
+  StyledTable,
+  StyledHead,
+  StyledBody,
+  StyledRow,
+  StyledCell,
+  SortableHeadCell,
+} from '../index.js';
+
+export const name = 'table-sortable';
+
+const DATA = [
+  ['Marlyn', 10],
+  ['Luther', 15],
+  ['Kiera', 13],
+  ['Edna', 20],
+  ['Soraya', 18],
+  ['Dorris', 32],
+  ['Astrid', 26],
+  ['Wendie', 17],
+  ['Marna', 11],
+  ['Malka', 14],
+  ['Jospeh', 10],
+  ['Roselee', 12],
+  ['Justine', 35],
+  ['Marlon', 30],
+  ['Mellissa', 32],
+  ['Fausto', 21],
+  ['Alfredia', 22],
+  ['Abel', 18],
+  ['Winford', 19],
+  ['Neil', 27],
+];
+
+// eslint-disable-next-line flowtype/no-weak-types
+class SortableTable extends React.Component<any, any> {
+  state = {nameDirection: null, ageDirection: null};
+
+  handleSort = (title: string, prevDirection: ?string) => {
+    let nextDirection = null;
+    if (prevDirection === 'ASC') {
+      nextDirection = 'DESC';
+    }
+    if (prevDirection === 'DESC') {
+      nextDirection = null;
+    }
+    if (prevDirection === null) {
+      nextDirection = 'ASC';
+    }
+
+    if (title === 'name') {
+      this.setState({nameDirection: nextDirection, ageDirection: null});
+      return;
+    }
+
+    if (title === 'age') {
+      this.setState({nameDirection: null, ageDirection: nextDirection});
+      return;
+    }
+  };
+
+  getSortedData = () => {
+    if (this.state.nameDirection) {
+      // $FlowFixMe
+      const sorted = DATA.slice(0).sort((a, b) => a[0].localeCompare(b[0]));
+
+      if (this.state.nameDirection === 'ASC') {
+        return sorted;
+      }
+
+      if (this.state.nameDirection === 'DESC') {
+        return sorted.reverse();
+      }
+    }
+
+    if (this.state.ageDirection) {
+      const sorted = DATA.slice(0).sort((a, b) => a[1] - b[1]);
+      if (this.state.ageDirection === 'ASC') {
+        return sorted;
+      }
+
+      if (this.state.ageDirection === 'DESC') {
+        return sorted.reverse();
+      }
+    }
+
+    return DATA;
+  };
+
+  render() {
+    return (
+      <div style={{height: '500px', width: '400px'}}>
+        <StyledTable>
+          <StyledHead>
+            <SortableHeadCell
+              title="Name"
+              direction={this.state.nameDirection}
+              onSort={() => this.handleSort('name', this.state.nameDirection)}
+            />
+            <SortableHeadCell
+              disabled
+              title="Age"
+              direction={this.state.ageDirection}
+              onSort={() => this.handleSort('age', this.state.ageDirection)}
+            />
+          </StyledHead>
+          <StyledBody>
+            {this.getSortedData().map((row, index) => (
+              <StyledRow key={index}>
+                {row.map((cell, cellIndex) => (
+                  <StyledCell key={cellIndex}>{cell}</StyledCell>
+                ))}
+              </StyledRow>
+            ))}
+          </StyledBody>
+        </StyledTable>
+      </div>
+    );
+  }
+}
+
+export const component = () => <SortableTable />;