bootstrap ui framework

This commit is contained in:
Sérgio Ramos 2016-10-23 05:27:18 +01:00
parent 6b0c89b65b
commit 64a01dff71
35 changed files with 891 additions and 0 deletions

16
ui/.babelrc Normal file
View File

@ -0,0 +1,16 @@
{
"sourceMaps": "both",
"presets": [
"react",
"es2015"
],
"plugins": [
"react-hot-loader/babel",
"add-module-exports",
"transform-exponentiation-operator",
"syntax-async-functions",
["transform-object-rest-spread", {
"useBuiltIns": true
}]
]
}

3
ui/.eslintignore Normal file
View File

@ -0,0 +1,3 @@
/node_modules
coverage
.nyc_output

29
ui/.eslintrc Normal file
View File

@ -0,0 +1,29 @@
{
"extends": "semistandard",
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 7,
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"plugins": [
"babel",
"react"
],
"rules": {
"generator-star-spacing": 0,
"babel/generator-star-spacing": 1,
"space-before-function-paren": [2, "never"],
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/react-in-jsx-scope": 2,
"object-curly-newline": ["error", {
"minProperties": 1
}],
"sort-vars": ["error", {
"ignoreCase": true
}]
}
}

15
ui/.tern-project Normal file
View File

@ -0,0 +1,15 @@
{
"libs": [
"ecmascript",
"browser"
],
"plugins": {
"doc_comment": true,
"local-scope": true,
"jsx": true,
"node": true,
"webpack": {
"configPath": "./webpack/index.js"
}
}
}

1
ui/README.md Normal file
View File

@ -0,0 +1 @@
# Joyent Dashboard UI Framework

0
ui/docs/components.js Normal file
View File

17
ui/docs/index.js Normal file
View File

@ -0,0 +1,17 @@
const ReactDOM = require('react-dom');
const React = require('react');
const render = () => {
const Root = require('./root');
ReactDOM.render(
<Root />,
document.getElementById('root')
);
};
render();
if (module.hot) {
module.hot.accept('./root', render);
}

16
ui/docs/root.js Normal file
View File

@ -0,0 +1,16 @@
const React = require('react');
const ReactHotLoader = require('react-hot-loader');
const Button = require('../src/button/readme.md');
const InnerHTML = require('dangerously-set-inner-html');
const {
AppContainer
} = ReactHotLoader;
module.exports = () => {
return (
<AppContainer>
<InnerHTML html={Button} />
</AppContainer>
);
};

67
ui/package.json Normal file
View File

@ -0,0 +1,67 @@
{
"name": "joyent-dashboard-frontend",
"version": "1.0.0",
"private": true,
"license": "private",
"scripts": {
"start": "webpack-dev-server --open --config webpack/index.js",
"lint": "eslint .",
"test": "NODE_ENV=test nyc ava test/*.js --verbose",
"build": "NODE_ENV=production webpack --config webpack/index.js",
"clean-static": "sh scripts/clean-static.sh",
"build-docs-static": "sh scripts/build-docs-static.sh"
},
"dependencies": {
"react": "^15.3.2"
},
"devDependencies": {
"ava": "^0.16.0",
"babel-core": "^6.17.0",
"babel-eslint": "^7.0.0",
"babel-loader": "^6.2.5",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-syntax-async-functions": "^6.13.0",
"babel-plugin-transform-object-rest-spread": "^6.16.0",
"babel-preset-es2015": "^6.16.0",
"babel-preset-react": "^6.16.0",
"babel-register": "^6.16.3",
"css-loader": "^0.25.0",
"dangerously-set-inner-html": "^1.0.0",
"enzyme": "^2.5.1",
"eslint": "^3.8.1",
"eslint-config-semistandard": "^7.0.0",
"eslint-config-standard": "^6.2.0",
"eslint-plugin-babel": "^3.3.0",
"eslint-plugin-promise": "^3.3.0",
"eslint-plugin-react": "^6.4.1",
"eslint-plugin-standard": "^2.0.1",
"extract-text-webpack-plugin": "^2.0.0-beta.4",
"html-loader": "^0.4.4",
"json-loader": "^0.5.4",
"nyc": "^8.3.1",
"postcss-cssnext": "^2.8.0",
"postcss-loader": "^1.0.0",
"pre-commit": "^1.1.3",
"react-addons-test-utils": "^15.3.2",
"react-dom": "^15.3.2",
"react-hot-loader": "^3.0.0-beta.6",
"react-router": "^4.0.0-alpha.4",
"style-loader": "^0.13.1",
"webpack": "^2.1.0-beta.25",
"webpack-dev-server": "^1.16.2",
"webpack-shell-plugin": "^0.4.3"
},
"ava": {
"failFast": true,
"cache": false,
"require": [
"babel-register"
],
"babel": "inherit"
},
"pre-commit": [
"lint",
"test",
"coverage"
]
}

View File

@ -0,0 +1,3 @@
echo $(pwd)
# cp -r ../static/* ../docs/static/
# mv ../docs/static/index.html ../docs/

View File

5
ui/src/base/index.js Normal file
View File

@ -0,0 +1,5 @@
const React = require('react');
module.exports = () => {
return null;
};

16
ui/src/button/index.js Normal file
View File

@ -0,0 +1,16 @@
const React = require('react');
const styles = require('./style.css');
module.exports = ({
disabled = false,
children
}) => {
return (
<button
className={styles.button}
disabled={disabled}
>
{children}
</button>
);
};

29
ui/src/button/readme.md Normal file
View File

@ -0,0 +1,29 @@
# `<Button>`
## demo
```embed
const React = require('react');
const ReactDOM = require('react-dom/server');
const Button = require('./index.js');
const styles = require('./style.css');
nmodule.exports = ReactDOM.renderToString(
<Button>Hello World</Button>
);
```
## usage
```js
const React = require('react');
const Button = require('ui/button');
module.exports = () => {
return (
<Button disabled={false}>
Hello World
</Button>
);
}
```

4
ui/src/button/style.css Normal file
View File

@ -0,0 +1,4 @@
.button {
background-color: #e500ee;
margin: none;
}

View File

@ -0,0 +1,5 @@
const React = require('react');
module.exports = () => {
return null;
};

3
ui/src/docs.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
Button: require('./button/readme.md')
};

5
ui/src/index.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
Button: require('./button'),
Container: require('./container'),
Base: require('./base')
};

3
ui/static/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!.gitignore
!theme.css

72
ui/static/theme.css Normal file
View File

@ -0,0 +1,72 @@
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
/* Tomorrow Comment */
.hljs-comment,
.hljs-quote {
color: #8e908c;
}
/* Tomorrow Red */
.hljs-variable,
.hljs-template-variable,
.hljs-tag,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-regexp,
.hljs-deletion {
color: #c82829;
}
/* Tomorrow Orange */
.hljs-number,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params,
.hljs-meta,
.hljs-link {
color: #f5871f;
}
/* Tomorrow Yellow */
.hljs-attribute {
color: #eab700;
}
/* Tomorrow Green */
.hljs-string,
.hljs-symbol,
.hljs-bullet,
.hljs-addition {
color: #718c00;
}
/* Tomorrow Blue */
.hljs-title,
.hljs-section {
color: #4271ae;
}
/* Tomorrow Purple */
.hljs-keyword,
.hljs-selector-tag {
color: #8959a8;
}
.hljs {
display: block;
overflow-x: auto;
background: white;
color: #4d4d4c;
padding: 0.5em;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

25
ui/test/index.js Normal file
View File

@ -0,0 +1,25 @@
const test = require('ava');
const enzyme = require('enzyme');
const React = require('react');
const {
shallow
} = enzyme;
test('renders <App> without exploding', (t) => {
const App = require('../src/containers/app');
const wrapper = shallow(<App />);
t.deepEqual(wrapper.length, 1);
});
test('renders <Home> without exploding', (t) => {
const Home = require('../src/containers/home');
const wrapper = shallow(<Home />);
t.deepEqual(wrapper.length, 1);
});
test('renders <NotFound> without exploding', (t) => {
const NotFound = require('../src/containers/not-found');
const wrapper = shallow(<NotFound />);
t.deepEqual(wrapper.length, 1);
});

76
ui/webpack/config.js Normal file
View File

@ -0,0 +1,76 @@
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
module.exports = {
context: path.join(__dirname, '../'),
output: {
path: path.join(__dirname, '../static'),
publicPath: '/static/',
filename: '[name].js'
},
plugins: [
new webpack.NoErrorsPlugin(),
new ExtractTextPlugin({
filename: '[name].css',
allChunks: true
}),
new webpack.LoaderOptionsPlugin({
options: {
postcss: {
plugins: () => {
return [
require('postcss-cssnext')
];
}
},
'embed-markdown-loader': {
// webpackConfigFullpath: path.join(__dirname, 'index.js') don't detach yet (has a bug in the production config)
}
}
})
],
resolveLoader: {
alias: {
'embed-markdown-loader': path.join(__dirname, './embed-markdown-loader')
}
},
module: {
loaders: [{
test: /js?$/,
exclude: /node_modules/,
include: [
path.join(__dirname, '../src'),
path.join(__dirname, '../docs')
],
loader: 'babel'
}, {
test: /\.json?$/,
exclude: /node_modules/,
include: [
path.join(__dirname, '../src'),
path.join(__dirname, '../docs')
],
loader: 'json'
}, {
test: /\.md?$/,
exclude: /node_modules/,
include: [
path.join(__dirname, '../src'),
path.join(__dirname, '../docs')
],
loader: 'html-loader!embed-markdown-loader'
}, {
test: /\.css?$/,
exclude: /node_modules/,
include: [
path.join(__dirname, '../src'),
path.join(__dirname, '../docs')
],
loader: ExtractTextPlugin.extract({
fallbackLoader: 'style-loader',
loader: 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader'
})
}]
}
};

35
ui/webpack/development.js Normal file
View File

@ -0,0 +1,35 @@
const graphql = require('../../cloudapi-graphql/src/endpoint');
const config = require('./config.js');
const entries = require('./entrypoints');
const webpack = require('webpack');
const devServer = {
hot: true,
compress: true,
lazy: false,
publicPath: '/static/',
setup: (app) => {
app.use('/graphql', graphql);
},
historyApiFallback: {
index: './static/index.html'
}
};
module.exports = Object.assign(config, {
entry: entries.reduce((all, entry) => {
all[entry.name] = [
'react-hot-loader/patch',
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/only-dev-server',
entry.path
];
return all;
}, {}),
plugins: config.plugins.concat([
new webpack.HotModuleReplacementPlugin()
]),
devtool: 'source-map',
devServer
});

View File

@ -0,0 +1,28 @@
{
"name": "remarkable-loader",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"lint": "eslint ."
},
"dependencies": {
"async": "^2.1.2",
"clone": "^2.0.0",
"get-stdin": "^5.0.1",
"highlight.js": "^9.7.0",
"loader-utils": "^0.2.16",
"lodash.clonedeep": "^4.5.0",
"memory-fs": "^0.3.0",
"minimist": "^1.2.0",
"remarkable": "^1.7.1",
"uuid": "^2.0.3",
"webpack": "^2.1.0-beta.25"
},
"devDependencies": {
"eslint": "^3.8.1",
"eslint-config-semistandard": "^7.0.0",
"eslint-config-standard": "^6.2.0",
"eslint-plugin-promise": "^3.3.0",
"eslint-plugin-standard": "^2.0.1"
}
}

View File

@ -0,0 +1,100 @@
const webpack = require('webpack');
const MemoryFS = require('memory-fs');
const clone = require('lodash.clonedeep');
const path = require('path');
const getCompiler = ({
filename,
mfs,
config
}) => {
const compiler = webpack(config);
compiler.outputFileSystem = mfs;
compiler.inputFileSystem.stat = function(path, callback) {
this._statStorage.provide(path, (path, callback) => {
if (path === filename) {
return mfs.stat(path, callback);
}
this._stat(path, callback);
}, callback);
};
compiler.inputFileSystem.readFile = function(path, callback) {
this._readFileStorage.provide(path, (path, callback) => {
if (path === filename) {
return mfs.readFile(path, callback);
}
this._readFile(path, callback);
}, callback);
};
return compiler;
};
module.exports = ({
source,
entrypoint,
config
}, fn) => {
const name = path.basename(entrypoint);
const _filename = path.resolve(config.context, entrypoint);
const _dirname = path.dirname(_filename);
const mfs = new MemoryFS();
mfs.mkdirpSync('/static');
mfs.mkdirpSync(_dirname);
mfs.writeFileSync(_filename, source);
const compiler = getCompiler({
filename: _filename,
mfs,
config: Object.assign(clone(config), {
target: 'node',
output: {
path: '/static',
filename: name
},
entry: [
`./${path.relative(config.context, _filename)}`
]
})
});
compiler.run((err, stats) => {
if (err) {
return fn(err);
}
const errors = stats.toJson().errors;
if (errors && errors.length) {
return fn(errors);
}
mfs.readFile(`/static/${name}`, (err, res) => {
if (err) {
return fn(err);
}
let style = mfs.readdirSync('/static').filter((file) => {
return /\.css$/.test(file);
}).map((file) => {
try {
return mfs.readFileSync(`/static/${file}`, 'utf-8');
} catch (err) {
return '';
}
}).concat('\n');
fn(err, {
body: (res && res.toString) ? res.toString() : res,
style
});
});
});
};

View File

@ -0,0 +1,51 @@
const spawn = require('child_process').spawn;
const path = require('path');
const executable = path.join(__dirname, 'detached.js');
module.exports = ({
source,
entrypoint,
configFullpath
}, fn) => {
let out = '';
let err = '';
const child = spawn('node', [
executable,
`--entrypoint=${entrypoint}`,
`--config=${configFullpath}`
]);
child.stdin.write(source);
child.stdin.end();
child.stdout.on('data', (data) => {
out += data;
});
child.stderr.on('data', (data) => {
err += data;
});
child.on('close', (code) => {
if (code !== 0) {
return fn(new Error(err));
}
const res = {
style: '',
body: ''
};
try {
const _res = JSON.parse(out);
res.style = _res.style;
res.body = _res.body;
} catch(err) {
console.error(err);
}
fn(null, res);
});
};

View File

@ -0,0 +1,25 @@
const argv = require('minimist')(process.argv.slice(2));
const getStdin = require('get-stdin');
const compile = require('./compile');
const {
entrypoint,
config
} = argv;
const webpackConfig = require(config);
getStdin().then((source) => {
compile({
source,
entrypoint,
config: webpackConfig
}, (err, res) => {
if (err) {
throw err;
}
console.log(JSON.stringify(res));
});
});

View File

@ -0,0 +1,18 @@
const compile = require('./compile');
const detach = require('./detach');
module.exports = ({
source,
entrypoint,
config
}, fn) => {
return !config.fullpath ? compile({
source,
entrypoint,
config: config.instantiated
}, fn) : detach({
source,
entrypoint,
configFullpath: config.fullpath
}, fn);
};

View File

@ -0,0 +1,29 @@
const Module = require('module');
const vm = require('vm');
const path = require('path');
module.exports = ({
source,
entrypoint
}) => {
const script = vm.createScript(source, entrypoint);
const dirname = path.basename(entrypoint);
const rootName = path.join(dirname, '@root');
const _module = new Module(rootName);
_module.filename = rootName;
_module.paths = Module._nodeModulePaths(dirname);
script.runInNewContext({
nmodule: _module,
nrequire: _module.require,
__filename: entrypoint,
__dirname: dirname,
process,
console,
Buffer
});
return _module.exports;
};

View File

@ -0,0 +1,15 @@
const hljs = require('highlight.js');
module.exports = (str, lang) => {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str).value;
} catch (err) {}
}
try {
return hljs.highlightAuto(str).value;
} catch (err) {}
return '';
};

View File

@ -0,0 +1,28 @@
const loaderUtils = require('loader-utils');
const parse = require('./parse');
const hl = require('./highlight');
module.exports = function(source) {
const fn = this.async();
const config = loaderUtils.getLoaderConfig(this, 'embed-markdown-loader');
const fullname = loaderUtils.getRemainingRequest(this);
const mode = config.mode || 'shadow';
parse({
mode,
fullname,
source,
config: {
webpack: {
instantiated: this._compilation.options,
fullpath: config.webpackConfigFullpath
},
renderer: {
html: true,
breaks: true,
highlight: hl
}
}
}, fn);
};

View File

@ -0,0 +1,98 @@
const async = require('async');
const Remarkable = require('remarkable');
const compile = require('./compile');
const evaluate = require('./eval');
const uuid = require('uuid').v4;
const templates = {
plain: (style, body) => `
<style>${style}</style>
${body}
`,
iframe: (style, body) => {
const _body = body.replace(/"/g, '\'');
return `
<iframe srcdoc="
<html>
<style>${style}</style>
<body>${_body}</body>">
</iframe>
`;
},
shadow: (style, body) => {
const id = uuid();
const script = `(function() {
const element = document.getElementById('${id}').attachShadow({
mode: 'closed'
});
const template = document.getElementById('${id}-template');
element.appendChild(document.importNode(template.content, true));
})();`;
return `
<div id="${id}"></div>
<template id="${id}-template">
${templates.plain(style, body)}
</template>
<script>${script}</script>
`;
}
};
module.exports = ({
mode = 'shadow',
fullname,
source,
config = {}
}, fn) => {
const instance = new Remarkable(config.renderer);
const {
parse,
renderer,
options
} = instance;
const entrypoint = fullname.replace(/\.md$/, '.js');
const tokens = parse.call(instance, source, options, {});
async.map(tokens, (token, fn) => {
if ((token.type !== 'fence') || (token.params !== 'embed')) {
return fn(null, token);
}
compile({
source: token.content,
config: config.webpack,
entrypoint
}, (err, {
body,
style
}) => {
if (err) {
return fn(err);
}
const evaluated = evaluate({
entrypoint,
source: body
});
const content = templates[mode](style, evaluated);
return fn(null, {
type: 'htmlblock',
content
});
});
}, (err, tokens) => {
if (err) {
return fn(err);
}
fn(err, renderer.render.call(instance.renderer, tokens, options, {}));
});
};

21
ui/webpack/entrypoints.js Normal file
View File

@ -0,0 +1,21 @@
const path = require('path');
const fs = require('fs');
const docs = path.join(__dirname, '../docs/index');
const src = path.join(__dirname, '../src/');
const ui = path.join(src, './index.js');
module.exports = fs
.readdirSync(src)
.filter((entry) => fs.statSync(path.join(src, entry)).isDirectory())
.map((entry) => ({
name: entry,
path: path.join(src, entry, './index.js')
}))
.concat([{
name: 'docs',
path: docs
}, {
name: 'ui',
path: ui
}]);

2
ui/webpack/index.js Normal file
View File

@ -0,0 +1,2 @@
const NODE_ENV = process.env['NODE_ENV'] || 'development';
module.exports = require(`./${NODE_ENV}`);

31
ui/webpack/production.js Normal file
View File

@ -0,0 +1,31 @@
const WebpackShellPlugin = require('webpack-shell-plugin');
const config = require('./config.js');
const webpack = require('webpack');
const entries = require('./entrypoints');
const path = require('path');
module.exports = Object.assign(config, {
entry: entries.reduce((all, entry) => {
all[entry.name] = [entry.path];
return all;
}, {}),
plugins: config.plugins.concat([
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurrenceOrderPlugin(true),
new webpack.optimize.UglifyJsPlugin(),
new WebpackShellPlugin({
onBuildEnd: [
'npm run build-docs-static'
]
})
]),
devtool: 'eval'
});
/*
* Maybe add in the future:
* - https://github.com/lettertwo/appcache-webpack-plugin
* - https://github.com/NekR/offline-plugin
* - https://github.com/goldhand/sw-precache-webpack-plugin
* - https://github.com/Klathmon/imagemin-webpack-plugin
*/