diff --git a/.csscomb.json b/.csscomb.json
new file mode 100644
index 00000000..9b710499
--- /dev/null
+++ b/.csscomb.json
@@ -0,0 +1,323 @@
+{
+ "exclude": [
+ ".git/**",
+ "node_modules/**",
+ "bower_components/**"
+ ],
+ "always-semicolon": true,
+ "block-indent": " ",
+ "color-case": "lower",
+ "color-shorthand": true,
+ "element-case": "lower",
+ "eof-newline": true,
+ "leading-zero": false,
+ "quotes": "single",
+ "remove-empty-rulesets": true,
+ "space-after-colon": " ",
+ "space-after-combinator": " ",
+ "space-after-opening-brace": "\n",
+ "space-after-selector-delimiter": " ",
+ "space-before-closing-brace": "\n",
+ "space-before-colon": "",
+ "space-before-combinator": " ",
+ "space-before-opening-brace": " ",
+ "space-before-selector-delimiter": "",
+ "strip-spaces": true,
+ "unitless-zero": true,
+ "vendor-prefix-align": true,
+ "sort-order": [
+ [
+ "font",
+ "font-family",
+ "font-size",
+ "font-weight",
+ "font-style",
+ "font-variant",
+ "font-size-adjust",
+ "font-stretch",
+ "font-effect",
+ "font-emphasize",
+ "font-emphasize-position",
+ "font-emphasize-style",
+ "font-smooth",
+ "src",
+ "line-height",
+ "position",
+ "z-index",
+ "top",
+ "right",
+ "bottom",
+ "left",
+ "display",
+ "visibility",
+ "float",
+ "clear",
+ "overflow",
+ "overflow-x",
+ "overflow-y",
+ "-ms-overflow-x",
+ "-ms-overflow-y",
+ "clip",
+ "zoom",
+ "flex",
+ "flex-direction",
+ "flex-order",
+ "flex-pack",
+ "flex-align",
+ "flex-wrap",
+ "flex-grow",
+ "flex-shrink",
+ "align-items",
+ "justify-content",
+ "-webkit-box-sizing",
+ "-moz-box-sizing",
+ "box-sizing",
+ "width",
+ "min-width",
+ "max-width",
+ "height",
+ "min-height",
+ "max-height",
+ "margin",
+ "margin-top",
+ "margin-right",
+ "margin-bottom",
+ "margin-left",
+ "padding",
+ "padding-top",
+ "padding-right",
+ "padding-bottom",
+ "padding-left",
+ "table-layout",
+ "empty-cells",
+ "caption-side",
+ "border-spacing",
+ "border-collapse",
+ "list-style",
+ "list-style-position",
+ "list-style-type",
+ "list-style-image",
+ "content",
+ "quotes",
+ "counter-reset",
+ "counter-increment",
+ "resize",
+ "cursor",
+ "-webkit-user-select",
+ "-moz-user-select",
+ "-ms-user-select",
+ "user-select",
+ "nav-index",
+ "nav-up",
+ "nav-right",
+ "nav-down",
+ "nav-left",
+ "-webkit-transition",
+ "-moz-transition",
+ "-ms-transition",
+ "-o-transition",
+ "transition",
+ "-webkit-transition-delay",
+ "-moz-transition-delay",
+ "-ms-transition-delay",
+ "-o-transition-delay",
+ "transition-delay",
+ "-webkit-transition-timing-function",
+ "-moz-transition-timing-function",
+ "-ms-transition-timing-function",
+ "-o-transition-timing-function",
+ "transition-timing-function",
+ "-webkit-transition-duration",
+ "-moz-transition-duration",
+ "-ms-transition-duration",
+ "-o-transition-duration",
+ "transition-duration",
+ "-webkit-transition-property",
+ "-moz-transition-property",
+ "-ms-transition-property",
+ "-o-transition-property",
+ "transition-property",
+ "-webkit-transform",
+ "-moz-transform",
+ "-ms-transform",
+ "-o-transform",
+ "transform",
+ "-webkit-transform-origin",
+ "-moz-transform-origin",
+ "-ms-transform-origin",
+ "-o-transform-origin",
+ "transform-origin",
+ "-webkit-animation",
+ "-moz-animation",
+ "-ms-animation",
+ "-o-animation",
+ "animation",
+ "-webkit-animation-name",
+ "-moz-animation-name",
+ "-ms-animation-name",
+ "-o-animation-name",
+ "animation-name",
+ "-webkit-animation-duration",
+ "-moz-animation-duration",
+ "-ms-animation-duration",
+ "-o-animation-duration",
+ "animation-duration",
+ "-webkit-animation-play-state",
+ "-moz-animation-play-state",
+ "-ms-animation-play-state",
+ "-o-animation-play-state",
+ "animation-play-state",
+ "-webkit-animation-timing-function",
+ "-moz-animation-timing-function",
+ "-ms-animation-timing-function",
+ "-o-animation-timing-function",
+ "animation-timing-function",
+ "-webkit-animation-delay",
+ "-moz-animation-delay",
+ "-ms-animation-delay",
+ "-o-animation-delay",
+ "animation-delay",
+ "-webkit-animation-iteration-count",
+ "-moz-animation-iteration-count",
+ "-ms-animation-iteration-count",
+ "-o-animation-iteration-count",
+ "animation-iteration-count",
+ "-webkit-animation-direction",
+ "-moz-animation-direction",
+ "-ms-animation-direction",
+ "-o-animation-direction",
+ "animation-direction",
+ "text-align",
+ "-webkit-text-align-last",
+ "-moz-text-align-last",
+ "-ms-text-align-last",
+ "text-align-last",
+ "vertical-align",
+ "white-space",
+ "text-decoration",
+ "text-emphasis",
+ "text-emphasis-color",
+ "text-emphasis-style",
+ "text-emphasis-position",
+ "text-indent",
+ "-ms-text-justify",
+ "text-justify",
+ "letter-spacing",
+ "word-spacing",
+ "-ms-writing-mode",
+ "text-outline",
+ "text-transform",
+ "text-wrap",
+ "text-overflow",
+ "-ms-text-overflow",
+ "text-overflow-ellipsis",
+ "text-overflow-mode",
+ "-ms-word-wrap",
+ "word-wrap",
+ "word-break",
+ "-ms-word-break",
+ "-moz-tab-size",
+ "-o-tab-size",
+ "tab-size",
+ "-webkit-hyphens",
+ "-moz-hyphens",
+ "hyphens",
+ "pointer-events",
+ "opacity",
+ "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity",
+ "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha",
+ "-ms-interpolation-mode",
+ "color",
+ "border",
+ "border-width",
+ "border-style",
+ "border-color",
+ "border-top",
+ "border-top-width",
+ "border-top-style",
+ "border-top-color",
+ "border-right",
+ "border-right-width",
+ "border-right-style",
+ "border-right-color",
+ "border-bottom",
+ "border-bottom-width",
+ "border-bottom-style",
+ "border-bottom-color",
+ "border-left",
+ "border-left-width",
+ "border-left-style",
+ "border-left-color",
+ "-webkit-border-radius",
+ "-moz-border-radius",
+ "border-radius",
+ "-webkit-border-top-left-radius",
+ "-moz-border-radius-topleft",
+ "border-top-left-radius",
+ "-webkit-border-top-right-radius",
+ "-moz-border-radius-topright",
+ "border-top-right-radius",
+ "-webkit-border-bottom-right-radius",
+ "-moz-border-radius-bottomright",
+ "border-bottom-right-radius",
+ "-webkit-border-bottom-left-radius",
+ "-moz-border-radius-bottomleft",
+ "border-bottom-left-radius",
+ "-webkit-border-image",
+ "-moz-border-image",
+ "-o-border-image",
+ "border-image",
+ "-webkit-border-image-source",
+ "-moz-border-image-source",
+ "-o-border-image-source",
+ "border-image-source",
+ "-webkit-border-image-slice",
+ "-moz-border-image-slice",
+ "-o-border-image-slice",
+ "border-image-slice",
+ "-webkit-border-image-width",
+ "-moz-border-image-width",
+ "-o-border-image-width",
+ "border-image-width",
+ "-webkit-border-image-outset",
+ "-moz-border-image-outset",
+ "-o-border-image-outset",
+ "border-image-outset",
+ "-webkit-border-image-repeat",
+ "-moz-border-image-repeat",
+ "-o-border-image-repeat",
+ "border-image-repeat",
+ "outline",
+ "outline-width",
+ "outline-style",
+ "outline-color",
+ "outline-offset",
+ "background",
+ "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader",
+ "background-color",
+ "background-image",
+ "background-repeat",
+ "background-attachment",
+ "background-position",
+ "background-position-x",
+ "-ms-background-position-x",
+ "background-position-y",
+ "-ms-background-position-y",
+ "-webkit-background-clip",
+ "-moz-background-clip",
+ "background-clip",
+ "background-origin",
+ "-webkit-background-size",
+ "-moz-background-size",
+ "-o-background-size",
+ "background-size",
+ "box-decoration-break",
+ "-webkit-box-shadow",
+ "-moz-box-shadow",
+ "box-shadow",
+ "filter:progid:DXImageTransform.Microsoft.gradient",
+ "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient",
+ "text-shadow"
+ ]
+ ]
+}
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000..de829399
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,38 @@
+{
+ "parser": "babel-eslint",
+ "env": {
+ "browser": true,
+ "node": true,
+ "jest": true
+ },
+ "plugins": [
+ "import",
+ "react"
+ ],
+ "extends": [
+ "eslint:recommended",
+ "plugin:import/errors",
+ "plugin:react/recommended"
+ ],
+ "settings": {
+ "import/resolver": "webpack"
+ },
+ "rules": {
+ "array-bracket-spacing": [1, "always"],
+ "comma-dangle": [1, "never"],
+ "eqeqeq": [2, "smart"],
+ "jsx-quotes": [1, "prefer-double"],
+ "no-unused-vars": 0,
+ "object-curly-spacing": [1, "always"],
+ "quotes": [1, "single", "avoid-escape"],
+ "react/jsx-space-before-closing": [1, "never"],
+ "react/no-did-mount-set-state": 0,
+ "react/prop-types": 1,
+ "semi": [1, "never"],
+ "space-before-blocks": [1, "always"]
+ },
+ "globals": {
+ "describe": false,
+ "it": false
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..41b0c8ab
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/node_modules
+/packages/fragment-chat/node_modules
+/packages/fragment-common/node_modules
+/packages/fragment-contacts/node_modules
+/packages/fragment-header/node_modules
diff --git a/lerna.json b/lerna.json
new file mode 100644
index 00000000..f19b9681
--- /dev/null
+++ b/lerna.json
@@ -0,0 +1,7 @@
+{
+ "lerna": "2.5.1",
+ "packages": [
+ "packages/*"
+ ],
+ "version": "0.0.0"
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..e4671d49
--- /dev/null
+++ b/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "mosaic-tailor-react-example",
+ "version": "1.0.0",
+ "description": "mosaic-tailor-react-example",
+ "main": "index.js",
+ "scripts": {
+ "start": "node tailor.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/tsnolan23/mosaic-tailor-react-example.git"
+ },
+ "author": "",
+ "license": "ISC",
+ "bugs": {
+ "url": "https://github.com/tsnolan23/mosaic-tailor-react-example/issues"
+ },
+ "homepage": "https://github.com/tsnolan23/mosaic-tailor-react-example#readme",
+ "devDependencies": {
+ "babel-eslint": "^8.0.2",
+ "eslint": "^4.11.0",
+ "eslint-import-resolver-webpack": "^0.8.3",
+ "eslint-plugin-import": "^2.8.0",
+ "eslint-plugin-react": "^7.5.1",
+ "lerna": "^2.5.1",
+ "node-tailor": "^3.4.0",
+ "webpack": "^3.8.1"
+ }
+}
diff --git a/packages/fragment-chat/.babelrc b/packages/fragment-chat/.babelrc
new file mode 100644
index 00000000..4ffef06d
--- /dev/null
+++ b/packages/fragment-chat/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["env", "react"]
+}
diff --git a/packages/fragment-chat/.gitignore b/packages/fragment-chat/.gitignore
new file mode 100644
index 00000000..c75eeccc
--- /dev/null
+++ b/packages/fragment-chat/.gitignore
@@ -0,0 +1 @@
+/public
diff --git a/packages/fragment-chat/app/Chat/index.js b/packages/fragment-chat/app/Chat/index.js
new file mode 100644
index 00000000..5197770f
--- /dev/null
+++ b/packages/fragment-chat/app/Chat/index.js
@@ -0,0 +1,30 @@
+import React, { Component } from 'react'
+import classNames from 'classnames'
+import './styles.scss'
+
+class Chat extends Component {
+
+ constructor(props) {
+ super(props)
+ this.state ={
+ isExpanded: false
+ }
+ }
+
+ toggleExpansion() {
+ this.setState({ isExpanded: !this.state.isExpanded })
+ }
+
+ render() {
+ const classes = classNames({
+ chat: true,
+ expanded: this.state.isExpanded
+ })
+ return(
+
this.toggleExpansion() } className={classes}>
+ )
+ }
+
+}
+
+export default Chat
diff --git a/packages/fragment-chat/app/Chat/styles.scss b/packages/fragment-chat/app/Chat/styles.scss
new file mode 100644
index 00000000..ba01c9c4
--- /dev/null
+++ b/packages/fragment-chat/app/Chat/styles.scss
@@ -0,0 +1,15 @@
+.chat {
+ position: fixed;
+ right: 30px;
+ bottom: 0;
+ width: 250px;
+ height: 30px;
+ cursor: pointer;
+ transition: height .3s;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ background: #5e81ac;
+ &.expanded {
+ height: 250px;
+ }
+}
diff --git a/packages/fragment-chat/app/index.js b/packages/fragment-chat/app/index.js
new file mode 100644
index 00000000..fe26a125
--- /dev/null
+++ b/packages/fragment-chat/app/index.js
@@ -0,0 +1,6 @@
+import React, { Component } from 'react'
+import { render } from 'react-dom'
+
+import Chat from './Chat'
+
+render(, document.getElementById('chat'))
diff --git a/packages/fragment-chat/fragment.js b/packages/fragment-chat/fragment.js
new file mode 100644
index 00000000..2cfb30ad
--- /dev/null
+++ b/packages/fragment-chat/fragment.js
@@ -0,0 +1,23 @@
+const http = require('http')
+const url = require('url')
+const fs = require('fs')
+
+const server = http.createServer((req, res) => {
+ const pathname = url.parse(req.url).pathname
+ const jsHeader = { 'Content-Type': 'application/javascript' }
+ switch(pathname) {
+ case '/public/bundle.js':
+ res.writeHead(200, jsHeader)
+ return fs.createReadStream('./public/bundle.js').pipe(res)
+ default:
+ res.writeHead(200, {
+ 'Content-Type': 'text/html',
+ 'Link': '; rel="fragment-script"'
+ })
+ return res.end('')
+ }
+})
+
+server.listen(3000, () => {
+ console.log('SPA Fragment Server started at 3000')
+})
diff --git a/packages/fragment-chat/package.json b/packages/fragment-chat/package.json
new file mode 100644
index 00000000..0d0fae76
--- /dev/null
+++ b/packages/fragment-chat/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "fragment-chat",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "build": "webpack -p",
+ "start": "node fragment.js"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "classnames": "^2.2.5",
+ "react": "^16.1.1",
+ "react-dom": "^16.1.1"
+ },
+ "devDependencies": {
+ "babel": "^6.23.0",
+ "babel-core": "^6.26.0",
+ "babel-loader": "^7.1.2",
+ "babel-preset-env": "^1.6.1",
+ "babel-preset-react": "^6.24.1",
+ "css-loader": "^0.28.7",
+ "node-sass": "^4.7.2",
+ "path": "^0.12.7",
+ "sass-loader": "^6.0.6",
+ "style-loader": "^0.19.0",
+ "webpack": "^3.8.1"
+ }
+}
diff --git a/packages/fragment-chat/webpack.config.js b/packages/fragment-chat/webpack.config.js
new file mode 100644
index 00000000..7c87d3dc
--- /dev/null
+++ b/packages/fragment-chat/webpack.config.js
@@ -0,0 +1,29 @@
+var webpack = require('webpack')
+
+module.exports = {
+ entry: './app/index.js',
+ output: {
+ path: __dirname + '/public',
+ publicPath: 'http://localhost:8081/public/',
+ filename: 'bundle.js',
+ libraryTarget: 'amd'
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ loader: 'babel-loader'
+ },
+ {
+ test: /\.scss$/,
+ loader: 'style-loader!css-loader!sass-loader'
+ }
+ ]
+ },
+ externals: {
+ 'react': 'react',
+ 'react-dom': 'react-dom',
+ 'classnames': 'classnames'
+ }
+}
diff --git a/packages/fragment-common/.babelrc b/packages/fragment-common/.babelrc
new file mode 100644
index 00000000..4ffef06d
--- /dev/null
+++ b/packages/fragment-common/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["env", "react"]
+}
diff --git a/packages/fragment-common/.gitignore b/packages/fragment-common/.gitignore
new file mode 100644
index 00000000..c75eeccc
--- /dev/null
+++ b/packages/fragment-common/.gitignore
@@ -0,0 +1 @@
+/public
diff --git a/packages/fragment-common/common.js b/packages/fragment-common/common.js
new file mode 100644
index 00000000..358da683
--- /dev/null
+++ b/packages/fragment-common/common.js
@@ -0,0 +1,3 @@
+exports.react = require('react')
+exports['react-dom'] = require('react-dom')
+exports.classnames = require('classnames')
diff --git a/packages/fragment-common/fragment.js b/packages/fragment-common/fragment.js
new file mode 100644
index 00000000..d19eaba2
--- /dev/null
+++ b/packages/fragment-common/fragment.js
@@ -0,0 +1,23 @@
+const http = require('http')
+const url = require('url')
+const fs = require('fs')
+
+const server = http.createServer((req, res) => {
+ const pathname = url.parse(req.url).pathname
+ const jsHeader = { 'Content-Type': 'application/javascript' }
+ switch(pathname) {
+ case '/public/bundle.js':
+ res.writeHead(200, jsHeader)
+ return fs.createReadStream('./public/bundle.js').pipe(res)
+ default:
+ res.writeHead(200, {
+ 'Content-Type': 'text/html',
+ 'Link': '; rel="fragment-script"'
+ })
+ return res.end('')
+ }
+})
+
+server.listen(6006, () => {
+ console.log('SPA Fragment Server started at 6006')
+})
diff --git a/packages/fragment-common/package.json b/packages/fragment-common/package.json
new file mode 100644
index 00000000..9ab3d021
--- /dev/null
+++ b/packages/fragment-common/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "fragment-common",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "build": "webpack -p",
+ "start": "node fragment.js"
+ },
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "babel": "^6.23.0",
+ "babel-core": "^6.26.0",
+ "babel-loader": "^7.1.2",
+ "babel-preset-env": "^1.6.1",
+ "babel-preset-react": "^6.24.1",
+ "webpack": "^3.8.1"
+ },
+ "dependencies": {
+ "classnames": "^2.2.5",
+ "react": "^16.1.1",
+ "react-dom": "^16.1.1"
+ }
+}
diff --git a/packages/fragment-common/webpack.config.js b/packages/fragment-common/webpack.config.js
new file mode 100644
index 00000000..ba2ac805
--- /dev/null
+++ b/packages/fragment-common/webpack.config.js
@@ -0,0 +1,11 @@
+var webpack = require('webpack')
+
+module.exports = {
+ entry: './common.js',
+ output: {
+ path: __dirname + '/public',
+ publicPath: 'http://localhost:8081/public/',
+ filename: 'bundle.js',
+ libraryTarget: 'umd'
+ }
+}
diff --git a/packages/fragment-contacts/.babelrc b/packages/fragment-contacts/.babelrc
new file mode 100644
index 00000000..4ffef06d
--- /dev/null
+++ b/packages/fragment-contacts/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["env", "react"]
+}
diff --git a/packages/fragment-contacts/.gitignore b/packages/fragment-contacts/.gitignore
new file mode 100644
index 00000000..c75eeccc
--- /dev/null
+++ b/packages/fragment-contacts/.gitignore
@@ -0,0 +1 @@
+/public
diff --git a/packages/fragment-contacts/app/Contact/index.js b/packages/fragment-contacts/app/Contact/index.js
new file mode 100644
index 00000000..9bce57e3
--- /dev/null
+++ b/packages/fragment-contacts/app/Contact/index.js
@@ -0,0 +1,16 @@
+import React, { Component } from 'react'
+import './styles.scss'
+
+class Contact extends Component {
+ render() {
+ return(
+
+ )
+ }
+}
+
+export default Contact
diff --git a/packages/fragment-contacts/app/Contact/styles.scss b/packages/fragment-contacts/app/Contact/styles.scss
new file mode 100644
index 00000000..f276df7e
--- /dev/null
+++ b/packages/fragment-contacts/app/Contact/styles.scss
@@ -0,0 +1,15 @@
+.contact {
+ display: flex;
+ flex: 1;
+ box-sizing: border-box;
+ min-width: 25%;
+ max-width: 25%;
+ height: 250px;
+ padding: 10px;
+ cursor: pointer;
+ .contact-details {
+ flex: 1;
+ border-radius: 3px;
+ background: #d8dee9;
+ }
+}
diff --git a/packages/fragment-contacts/app/Contacts/index.js b/packages/fragment-contacts/app/Contacts/index.js
new file mode 100644
index 00000000..82894cf6
--- /dev/null
+++ b/packages/fragment-contacts/app/Contacts/index.js
@@ -0,0 +1,40 @@
+import React, { Component } from 'react'
+import axios from 'axios'
+import Contact from '../Contact'
+import './styles.scss'
+
+class Contacts extends Component {
+
+ constructor(props) {
+ super(props)
+ this.state ={
+ contacts: []
+ }
+ }
+
+ componentWillMount() {
+ this.fetchContacts()
+ }
+
+ fetchContacts() {
+ axios.get('https://randomuser.me/api/?results=20').then((response) => {
+ this.setState({ contacts: response.data.results })
+ })
+ }
+
+ render() {
+ const { contacts } = this.state
+ return(
+
+ {
+ contacts.length > 0 && contacts.map((contact) => {
+ return
+ })
+ }
+
+ )
+ }
+
+}
+
+export default Contacts
diff --git a/packages/fragment-contacts/app/Contacts/styles.scss b/packages/fragment-contacts/app/Contacts/styles.scss
new file mode 100644
index 00000000..d7146dc0
--- /dev/null
+++ b/packages/fragment-contacts/app/Contacts/styles.scss
@@ -0,0 +1,8 @@
+.contacts {
+ position: relative;
+ display: flex;
+ flex-wrap: wrap;
+ box-sizing: border-box;
+ width: 100%;
+ padding: 100px 20px 20px 20px;
+}
diff --git a/packages/fragment-contacts/app/index.js b/packages/fragment-contacts/app/index.js
new file mode 100644
index 00000000..2ab05c5e
--- /dev/null
+++ b/packages/fragment-contacts/app/index.js
@@ -0,0 +1,6 @@
+import React, { Component } from 'react'
+import { render } from 'react-dom'
+
+import Contacts from './Contacts'
+
+render(, document.getElementById('contacts'))
diff --git a/packages/fragment-contacts/fragment.js b/packages/fragment-contacts/fragment.js
new file mode 100644
index 00000000..f5943e6d
--- /dev/null
+++ b/packages/fragment-contacts/fragment.js
@@ -0,0 +1,23 @@
+const http = require('http')
+const url = require('url')
+const fs = require('fs')
+
+const server = http.createServer((req, res) => {
+ const pathname = url.parse(req.url).pathname
+ const jsHeader = { 'Content-Type': 'application/javascript' }
+ switch(pathname) {
+ case '/public/bundle.js':
+ res.writeHead(200, jsHeader)
+ return fs.createReadStream('./public/bundle.js').pipe(res)
+ default:
+ res.writeHead(200, {
+ 'Content-Type': 'text/html',
+ 'Link': '; rel="fragment-script"'
+ })
+ return res.end('')
+ }
+})
+
+server.listen(5000, () => {
+ console.log('SPA Fragment Server started at 5000')
+})
diff --git a/packages/fragment-contacts/package.json b/packages/fragment-contacts/package.json
new file mode 100644
index 00000000..9f9edb7a
--- /dev/null
+++ b/packages/fragment-contacts/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "fragment-contacts",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "build": "webpack -p",
+ "start": "node fragment.js"
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "axios": "^0.17.1",
+ "classnames": "^2.2.5",
+ "react": "^16.1.1",
+ "react-dom": "^16.1.1"
+ },
+ "devDependencies": {
+ "babel": "^6.23.0",
+ "babel-core": "^6.26.0",
+ "babel-loader": "^7.1.2",
+ "babel-preset-env": "^1.6.1",
+ "babel-preset-react": "^6.24.1",
+ "css-loader": "^0.28.7",
+ "node-sass": "^4.7.2",
+ "sass-loader": "^6.0.6",
+ "style-loader": "^0.19.0",
+ "webpack": "^3.8.1"
+ }
+}
diff --git a/packages/fragment-contacts/webpack.config.js b/packages/fragment-contacts/webpack.config.js
new file mode 100644
index 00000000..7c87d3dc
--- /dev/null
+++ b/packages/fragment-contacts/webpack.config.js
@@ -0,0 +1,29 @@
+var webpack = require('webpack')
+
+module.exports = {
+ entry: './app/index.js',
+ output: {
+ path: __dirname + '/public',
+ publicPath: 'http://localhost:8081/public/',
+ filename: 'bundle.js',
+ libraryTarget: 'amd'
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ loader: 'babel-loader'
+ },
+ {
+ test: /\.scss$/,
+ loader: 'style-loader!css-loader!sass-loader'
+ }
+ ]
+ },
+ externals: {
+ 'react': 'react',
+ 'react-dom': 'react-dom',
+ 'classnames': 'classnames'
+ }
+}
diff --git a/packages/fragment-header/.babelrc b/packages/fragment-header/.babelrc
new file mode 100644
index 00000000..4ffef06d
--- /dev/null
+++ b/packages/fragment-header/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["env", "react"]
+}
diff --git a/packages/fragment-header/.gitignore b/packages/fragment-header/.gitignore
new file mode 100644
index 00000000..c75eeccc
--- /dev/null
+++ b/packages/fragment-header/.gitignore
@@ -0,0 +1 @@
+/public
diff --git a/packages/fragment-header/app/Header/index.js b/packages/fragment-header/app/Header/index.js
new file mode 100644
index 00000000..f44f832e
--- /dev/null
+++ b/packages/fragment-header/app/Header/index.js
@@ -0,0 +1,48 @@
+import React, { Component } from 'react'
+import './styles.scss'
+import NavItem from '../NavItem'
+import Logo from '../Logo'
+
+const items = [ 0, 1, 2, 3 ]
+
+class Header extends Component {
+
+ constructor(props) {
+ super(props)
+ this.state = {
+ name: 'Test',
+ active: 1
+ }
+ }
+
+ toggle() {
+ this.setState({ name: this.state.name === 'Test' ? 'New' : 'Test' })
+ }
+
+ selectNavItem(index) {
+ this.setState({ active: index })
+ }
+
+ render() {
+ return(
+ { this.toggle()}} className="header">
+
+ {
+ items.map((item, index) => {
+ return(
+ this.selectNavItem(index)}
+ key={index}
+ />
+ )
+ })
+ }
+
+ )
+ }
+
+}
+
+export default Header
diff --git a/packages/fragment-header/app/Header/styles.scss b/packages/fragment-header/app/Header/styles.scss
new file mode 100644
index 00000000..6ea6f3d5
--- /dev/null
+++ b/packages/fragment-header/app/Header/styles.scss
@@ -0,0 +1,13 @@
+.header {
+ position: fixed;
+ z-index: 10;
+ top: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+ width: 100%;
+ height: 80px;
+ padding: 0 30px;
+ background: #5e81ac;
+}
diff --git a/packages/fragment-header/app/Logo/index.js b/packages/fragment-header/app/Logo/index.js
new file mode 100644
index 00000000..55f9fcce
--- /dev/null
+++ b/packages/fragment-header/app/Logo/index.js
@@ -0,0 +1,12 @@
+import React, { PureComponent } from 'react'
+import './styles.scss'
+
+class Logo extends PureComponent {
+ render() {
+ return(
+
+ )
+ }
+}
+
+export default Logo
diff --git a/packages/fragment-header/app/Logo/styles.scss b/packages/fragment-header/app/Logo/styles.scss
new file mode 100644
index 00000000..1bc15f59
--- /dev/null
+++ b/packages/fragment-header/app/Logo/styles.scss
@@ -0,0 +1,7 @@
+.logo {
+ width: 100px;
+ height: 40px;
+ margin-right: 100px;
+ border-radius: 2px;
+ background: #8fbcbb;
+}
diff --git a/packages/fragment-header/app/NavItem/index.js b/packages/fragment-header/app/NavItem/index.js
new file mode 100644
index 00000000..416cc536
--- /dev/null
+++ b/packages/fragment-header/app/NavItem/index.js
@@ -0,0 +1,30 @@
+import React, { Component } from 'react'
+import { bool, func, number } from 'prop-types'
+import './styles.scss'
+import classNames from 'classnames'
+
+class NavItem extends Component {
+
+ handleClick() {
+ this.props.onClick(this.props.index)
+ }
+
+ render() {
+ const classes = classNames({
+ 'nav-item': true,
+ 'current': this.props.active
+ })
+ return(
+ this.handleClick()}>
+ )
+ }
+
+}
+
+NavItem.propTypes = {
+ active: bool,
+ index: number,
+ onClick: func
+}
+
+export default NavItem
diff --git a/packages/fragment-header/app/NavItem/styles.scss b/packages/fragment-header/app/NavItem/styles.scss
new file mode 100644
index 00000000..9a690795
--- /dev/null
+++ b/packages/fragment-header/app/NavItem/styles.scss
@@ -0,0 +1,15 @@
+.nav-item {
+ width: 100px;
+ height: 18px;
+ margin-right: 20px;
+ cursor: pointer;
+ transition: all .3s;
+ border-radius: 3px;
+ background: #81a1c1;
+ &:not(.current):hover {
+ background: lighten(#81a1c1, 10%);
+ }
+ &.current {
+ background: #ebcb8b;
+ }
+}
diff --git a/packages/fragment-header/app/index.js b/packages/fragment-header/app/index.js
new file mode 100644
index 00000000..f426cc17
--- /dev/null
+++ b/packages/fragment-header/app/index.js
@@ -0,0 +1,6 @@
+import React, { Component } from 'react'
+import { render } from 'react-dom'
+
+import Header from './Header'
+
+render(, document.getElementById('header'))
diff --git a/packages/fragment-header/fragment.js b/packages/fragment-header/fragment.js
new file mode 100644
index 00000000..d826e48f
--- /dev/null
+++ b/packages/fragment-header/fragment.js
@@ -0,0 +1,23 @@
+const http = require('http')
+const url = require('url')
+const fs = require('fs')
+
+const server = http.createServer((req, res) => {
+ const pathname = url.parse(req.url).pathname
+ const jsHeader = { 'Content-Type': 'application/javascript' }
+ switch(pathname) {
+ case '/public/bundle.js':
+ res.writeHead(200, jsHeader)
+ return fs.createReadStream('./public/bundle.js').pipe(res)
+ default:
+ res.writeHead(200, {
+ 'Content-Type': 'text/html',
+ 'Link': '; rel="fragment-script"'
+ })
+ return res.end('')
+ }
+})
+
+server.listen(8081, () => {
+ console.log('SPA Fragment Server started at 8081')
+})
diff --git a/packages/fragment-header/package.json b/packages/fragment-header/package.json
new file mode 100644
index 00000000..e0c2c8fb
--- /dev/null
+++ b/packages/fragment-header/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "fragment-header",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "dependencies": {
+ "classnames": "^2.2.5",
+ "prop-types": "^15.6.0",
+ "react": "^16.1.1",
+ "react-dom": "^16.1.1"
+ },
+ "scripts": {
+ "build": "webpack -p",
+ "start": "node fragment.js"
+ },
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "babel": "^6.23.0",
+ "babel-core": "^6.26.0",
+ "babel-loader": "^7.1.2",
+ "babel-preset-env": "^1.6.1",
+ "babel-preset-react": "^6.24.1",
+ "css-loader": "^0.28.7",
+ "node-sass": "^4.7.2",
+ "sass-loader": "^6.0.6",
+ "style-loader": "^0.19.0",
+ "webpack": "^3.8.1"
+ }
+}
diff --git a/packages/fragment-header/webpack.config.js b/packages/fragment-header/webpack.config.js
new file mode 100644
index 00000000..7c87d3dc
--- /dev/null
+++ b/packages/fragment-header/webpack.config.js
@@ -0,0 +1,29 @@
+var webpack = require('webpack')
+
+module.exports = {
+ entry: './app/index.js',
+ output: {
+ path: __dirname + '/public',
+ publicPath: 'http://localhost:8081/public/',
+ filename: 'bundle.js',
+ libraryTarget: 'amd'
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.js$/,
+ exclude: /node_modules/,
+ loader: 'babel-loader'
+ },
+ {
+ test: /\.scss$/,
+ loader: 'style-loader!css-loader!sass-loader'
+ }
+ ]
+ },
+ externals: {
+ 'react': 'react',
+ 'react-dom': 'react-dom',
+ 'classnames': 'classnames'
+ }
+}
diff --git a/tailor.js b/tailor.js
new file mode 100644
index 00000000..2c2b9012
--- /dev/null
+++ b/tailor.js
@@ -0,0 +1,19 @@
+'use strict'
+
+const http = require('http')
+const Tailor = require('node-tailor')
+const tailor = new Tailor({
+ templatesPath: __dirname + '/templates'
+})
+
+http
+ .createServer((req, res) => {
+ if (req.url === '/favicon.ico') {
+ res.writeHead(200, { 'Content-Type': 'image/x-icon' })
+ return res.end('')
+ }
+ tailor.requestHandler(req, res)
+ })
+ .listen(8080, function() {
+ console.log('Tailor server listening on port 8080')
+ })
diff --git a/templates/contacts.html b/templates/contacts.html
new file mode 100644
index 00000000..61f31250
--- /dev/null
+++ b/templates/contacts.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+