diff --git a/package-lock.json b/package-lock.json
index 7f7c9169e..9cf345ffd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1772,7 +1772,6 @@
"version": "16.4.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.4.2.tgz",
"integrity": "sha512-oVcVteCDNiVc/fkDjowRfAZQDEOR76j3CJ3FvwXNvfV6zJguhghy1lMgpAzYox+9AZsWch+JPV6Imml3wvIUeg==",
- "dev": true,
"requires": {
"csstype": "^2.2.0"
}
@@ -1792,6 +1791,14 @@
"from": "github:coralproject/patched#types/react-relay",
"dev": true
},
+ "@types/react-responsive": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/react-responsive/-/react-responsive-3.0.1.tgz",
+ "integrity": "sha512-ti7MKe0AuWCGpNkRzRsRB8ExS7NZotXtDtD27rX4HiZML37SetbIp3cw5FDXIeqIC7a3TzWuPjv/I9Qcu4YL3w==",
+ "requires": {
+ "@types/react": "*"
+ }
+ },
"@types/react-test-renderer": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.0.1.tgz",
@@ -6183,6 +6190,11 @@
}
}
},
+ "css-mediaquery": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz",
+ "integrity": "sha1-aiw3NEkoYYYxxUvTPO3TAdoYvqA="
+ },
"css-modules-loader-core": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/css-modules-loader-core/-/css-modules-loader-core-1.1.0.tgz",
@@ -6472,8 +6484,7 @@
"csstype": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.5.5.tgz",
- "integrity": "sha512-EGMjeoiN3aqEX5u/cyH5mSdGBDGdLcCQvcEcBWNGFSPXKd9uOTIeVG91YQ22OxI44DKpvI+4C7VUSmEpsHWJaA==",
- "dev": true
+ "integrity": "sha512-EGMjeoiN3aqEX5u/cyH5mSdGBDGdLcCQvcEcBWNGFSPXKd9uOTIeVG91YQ22OxI44DKpvI+4C7VUSmEpsHWJaA=="
},
"currently-unhandled": {
"version": "0.4.1",
@@ -10086,6 +10097,11 @@
"decamelize": "^1.0.0"
}
},
+ "hyphenate-style-name": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.2.tgz",
+ "integrity": "sha1-MRYKNpMK2vH8BMYHT360FGXU7Es="
+ },
"iconv-lite": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
@@ -11709,8 +11725,7 @@
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
- "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
- "dev": true
+ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"js-yaml": {
"version": "3.12.0",
@@ -12472,7 +12487,6 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
"integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
- "dev": true,
"requires": {
"js-tokens": "^3.0.0"
}
@@ -12583,6 +12597,14 @@
"integrity": "sha1-iQwsGzv+g/sA5BKbjkz+ZFJw+dE=",
"dev": true
},
+ "matchmediaquery": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/matchmediaquery/-/matchmediaquery-0.2.1.tgz",
+ "integrity": "sha1-IjxwBXk94D5HzpKxMoWnLEStos8=",
+ "requires": {
+ "css-mediaquery": "^0.1.2"
+ }
+ },
"math-expression-evaluator": {
"version": "1.2.17",
"resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz",
@@ -13421,8 +13443,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
- "dev": true
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-copy": {
"version": "0.1.0",
@@ -16371,7 +16392,6 @@
"version": "15.6.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
- "dev": true,
"requires": {
"loose-envify": "^1.3.1",
"object-assign": "^4.1.1"
@@ -16958,6 +16978,16 @@
"prop-types": "^15.5.8"
}
},
+ "react-responsive": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-4.1.0.tgz",
+ "integrity": "sha512-ZuDraf0qsJlyiTwzeva+foHx83IP6SIhru9o7BvMwQ4ZHjRIL5WjdgVNNrKSRbmeWO9rEJoMpabei/5lJn8KaA==",
+ "requires": {
+ "hyphenate-style-name": "^1.0.0",
+ "matchmediaquery": "^0.2.1",
+ "prop-types": "^15.6.1"
+ }
+ },
"react-router": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz",
diff --git a/package.json b/package.json
index 944a521f6..2d312573d 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"author": "",
"license": "Apache-2.0",
"dependencies": {
+ "@types/react-responsive": "^3.0.1",
"apollo-server-express": "^1.3.6",
"bunyan": "^1.8.12",
"convict": "^4.3.0",
@@ -45,6 +46,7 @@
"mongodb": "^3.0.10",
"passport": "^0.4.0",
"performance-now": "^2.1.0",
+ "react-responsive": "^4.1.0",
"subscriptions-transport-ws": "^0.9.11",
"uuid": "^3.2.1"
},
diff --git a/src/core/client/ui/components/Center/Center.tsx b/src/core/client/ui/components/Center/Center.tsx
index 892277ac6..369d9cc8e 100644
--- a/src/core/client/ui/components/Center/Center.tsx
+++ b/src/core/client/ui/components/Center/Center.tsx
@@ -1,5 +1,5 @@
import cn from "classnames";
-import * as React from "react";
+import React from "react";
import { ReactNode, StatelessComponent } from "react";
import { withStyles } from "talk-ui/hocs";
diff --git a/src/core/client/ui/components/Flex/Flex.css b/src/core/client/ui/components/Flex/Flex.css
new file mode 100644
index 000000000..07559d116
--- /dev/null
+++ b/src/core/client/ui/components/Flex/Flex.css
@@ -0,0 +1,37 @@
+.root {
+}
+
+.justifyFlexStart {
+ justify-content: "flex-start";
+}
+.justifyFlexEnd {
+ justify-content: "flex-end";
+}
+.justifyCenter {
+ justify-content: "center";
+}
+.justifySpaceBetween {
+ justify-content: "space-between";
+}
+.justifySpaceAround {
+ justify-content: "space-around";
+}
+.justifySpaceEvenly {
+ justify-content: "space-evenly";
+}
+
+.alignFlexStart {
+ align-items: "flex-start";
+}
+.alignFlexEnd {
+ align-items: "flex-end";
+}
+.alignCenter {
+ align-items: "center";
+}
+.alignBaseline {
+ align-items: "baseline";
+}
+.alignStretch {
+ align-items: "stretch";
+}
diff --git a/src/core/client/ui/components/Flex/Flex.spec.tsx b/src/core/client/ui/components/Flex/Flex.spec.tsx
new file mode 100644
index 000000000..485e5bd9f
--- /dev/null
+++ b/src/core/client/ui/components/Flex/Flex.spec.tsx
@@ -0,0 +1,17 @@
+import { shallow } from "enzyme";
+import React from "react";
+
+import Flex from "./Flex";
+
+it("renders correctly", () => {
+ const props = {
+ justifyContent: "center",
+ alignItems: "center",
+ };
+ const wrapper = shallow(
+
+ Hello World
+
+ );
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/src/core/client/ui/components/Flex/Flex.tsx b/src/core/client/ui/components/Flex/Flex.tsx
new file mode 100644
index 000000000..b2e0e69ea
--- /dev/null
+++ b/src/core/client/ui/components/Flex/Flex.tsx
@@ -0,0 +1,38 @@
+import cn from "classnames";
+import React from "react";
+import { StatelessComponent } from "react";
+
+import { pascalCase } from "talk-common/utils";
+
+import * as styles from "./Flex.css";
+
+interface InnerProps {
+ justifyContent?:
+ | "flex-start"
+ | "flex-end"
+ | "center"
+ | "space-around"
+ | "space-between"
+ | "space-evenly";
+ alignItems?: "flex-start" | "flex-end" | "center" | "baseline" | "stretch";
+}
+
+const Flex: StatelessComponent = props => {
+ const { justifyContent, alignItems } = props;
+
+ const classObject: Record = {};
+
+ if (justifyContent) {
+ classObject[`justify${pascalCase(justifyContent)}`] = true;
+ }
+
+ if (alignItems) {
+ classObject[`align${pascalCase(alignItems)}`] = true;
+ }
+
+ const classNames: string = cn(styles.root, classObject);
+
+ return ;
+};
+
+export default Flex;
diff --git a/src/core/client/ui/components/Flex/__snapshots__/Flex.spec.tsx.snap b/src/core/client/ui/components/Flex/__snapshots__/Flex.spec.tsx.snap
new file mode 100644
index 000000000..e9dd085c5
--- /dev/null
+++ b/src/core/client/ui/components/Flex/__snapshots__/Flex.spec.tsx.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders correctly 1`] = `
+
+`;
diff --git a/src/core/client/ui/components/Flex/index.ts b/src/core/client/ui/components/Flex/index.ts
new file mode 100644
index 000000000..27e09f37b
--- /dev/null
+++ b/src/core/client/ui/components/Flex/index.ts
@@ -0,0 +1,2 @@
+export * from "./Flex";
+export { default } from "./Flex";
diff --git a/src/core/client/ui/components/MatchMedia/MatchMedia.spec.tsx b/src/core/client/ui/components/MatchMedia/MatchMedia.spec.tsx
new file mode 100644
index 000000000..527e5921f
--- /dev/null
+++ b/src/core/client/ui/components/MatchMedia/MatchMedia.spec.tsx
@@ -0,0 +1,31 @@
+import { shallow } from "enzyme";
+import React from "react";
+
+import MatchMedia from "./MatchMedia";
+
+it("renders correctly", () => {
+ const props = {
+ minWidth: "xs",
+ maxWidth: "sm",
+ component: "div",
+ screen: true,
+ };
+ const wrapper = shallow(
+
+ Hello World
+
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it("map new speech prop to older aural prop", () => {
+ const props = {
+ speech: true,
+ };
+ const wrapper = shallow(
+
+ Hello World
+
+ );
+ expect(wrapper).toMatchSnapshot();
+});
diff --git a/src/core/client/ui/components/MatchMedia/MediaMedia.tsx b/src/core/client/ui/components/MatchMedia/MediaMedia.tsx
new file mode 100644
index 000000000..ebd4ed210
--- /dev/null
+++ b/src/core/client/ui/components/MatchMedia/MediaMedia.tsx
@@ -0,0 +1,28 @@
+import React from "react";
+import { ReactNode, StatelessComponent } from "react";
+import Responsive from "react-responsive";
+
+interface InnerProps {
+ minWidth?: string;
+ maxWidth?: string;
+ children: ReactNode;
+ className?: string;
+ component?:
+ | string
+ | React.SFC
+ | React.ClassType
+ | React.ComponentClass;
+ all?: boolean;
+ print?: boolean;
+ screen?: boolean;
+ speech?: boolean;
+}
+
+const MatchMedia: StatelessComponent = props => {
+ // TODO: Temporarily map newer speech to older aural type until
+ // react-responsive supports the speech prop.
+ const { speech, ...rest } = props;
+ return ;
+};
+
+export default MatchMedia;
diff --git a/src/core/client/ui/components/MatchMedia/__snapshots__/MatchMedia.spec.tsx.snap b/src/core/client/ui/components/MatchMedia/__snapshots__/MatchMedia.spec.tsx.snap
new file mode 100644
index 000000000..4ce9b2fd8
--- /dev/null
+++ b/src/core/client/ui/components/MatchMedia/__snapshots__/MatchMedia.spec.tsx.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`map new speech prop to older aural prop 1`] = `
+
+
+ Hello World
+
+
+`;
+
+exports[`renders correctly 1`] = `
+
+
+ Hello World
+
+
+`;
diff --git a/src/core/client/ui/components/MatchMedia/index.ts b/src/core/client/ui/components/MatchMedia/index.ts
new file mode 100644
index 000000000..71f16f6f8
--- /dev/null
+++ b/src/core/client/ui/components/MatchMedia/index.ts
@@ -0,0 +1,2 @@
+export * from "./MatchMedia";
+export { default } from "./MatchMedia";
diff --git a/src/core/client/ui/theme/variables.ts b/src/core/client/ui/theme/variables.ts
index 4c4a54332..2e9499883 100644
--- a/src/core/client/ui/theme/variables.ts
+++ b/src/core/client/ui/theme/variables.ts
@@ -68,6 +68,15 @@ const variables = {
fontWeightLight: 300,
fontWeightRegular: 400,
fontWeightMedium: 550,
+ /* Breakpoints */
+ breakpoints: {
+ xs: 0,
+ sm: 320,
+ md: 640,
+ lg: 1024,
+ xl: 1400,
+ xxl: 1600,
+ },
};
module.exports = variables;
diff --git a/src/core/common/utils/index.ts b/src/core/common/utils/index.ts
index 88c1f3fec..e91489017 100644
--- a/src/core/common/utils/index.ts
+++ b/src/core/common/utils/index.ts
@@ -1 +1,2 @@
export { default as timeout } from "./timeout";
+export { default as pascalCase } from "./pascalCase";
diff --git a/src/core/common/utils/pascalCase.ts b/src/core/common/utils/pascalCase.ts
new file mode 100644
index 000000000..14b036ccc
--- /dev/null
+++ b/src/core/common/utils/pascalCase.ts
@@ -0,0 +1,5 @@
+import { camelCase } from "lodash";
+/** pascalCase */
+export default function pascalCase(value: string) {
+ return value[0].toUpperCase() + camelCase(value).slice(1);
+}
diff --git a/src/locales/es/common.ftl b/src/locales/es/common.ftl
new file mode 100644
index 000000000..e69de29bb