diff --git a/.eslintrc.js b/.eslintrc.js index 66c611c4..c40f9d30 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,6 +27,7 @@ module.exports = { { js: 'never', ts: 'never', + hbs: 'never', }, ], // allow debugger during development diff --git a/.gitignore b/.gitignore index d29b39e2..0246a295 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,3 @@ node_modules/ ### Project /dist/ -bundlesize-profile.* diff --git a/README.md b/README.md index ca22e460..ced47553 100644 --- a/README.md +++ b/README.md @@ -97,10 +97,9 @@ yarn build html # or yarn compile:html * `src/app/bundle.js` Webpack entry that will include all js and css files referenced from all template files. * `src/app/polyfills.js` List of polyfills to include in the bundles. -* `src/app/component/layout/index/index.hbs` Template file to list all the pages, used during - development. +* `src/app/component/layout/index/index.hbs` Template file to list all the pages. * `src/app/component/layout/app/app.hbs` Template file that is used for all pages, contains basic - page layout. + page layout (e.g. header, footer and wrapper). * `src/app/component/` Contains all components, each folder is made up of: * `component-name.hbs` The template file, can import a stylesheet using the html `link` tag, and a script using the html `script` tag. @@ -110,7 +109,7 @@ yarn build html # or yarn compile:html DOM attribute. * `src/app/component/blocks/` Contains all _block_ components. They are dynamically rendered based on the blocks entry in the json data file. -* `src/app/util/components.ts` Helper function for registering, updating and initializing +* `src/app/muban/componentUtils.ts` Helper function for registering, updating and initializing components. * `src/app/style` Folder containing global styles. All components will include their own stylesheet. * `src/app/style/main.scss` Main stylesheet file, only for setting up global styles. @@ -121,3 +120,12 @@ yarn build html # or yarn compile:html * `.modernizrrc` config file for Modernizrrc used by `modernizr-loader`, config rules can be found [here](https://github.com/Modernizr/Modernizr/blob/master/lib/config-all.json). * `template/*` Template files for seng-generator, for creating pages, blocks and components. + +## Storybook + +Storybook is a web-app that lets you preview and interact with the components in your project. +You can create presets that render your component with custom HTML, and pass different properties +by providing a json object. + +Please read the +[extended documentation](docs/storybook.md) for more information. diff --git a/build-tools/config/index.js b/build-tools/config/index.js index 3f89038f..626827e2 100644 --- a/build-tools/config/index.js +++ b/build-tools/config/index.js @@ -56,6 +56,19 @@ module.exports = { enableTSLintLoader: false, enableStyleLintPlugin: false, }, + storybook: { + env: { + NODE_ENV: JSON.stringify('development'), + }, + port: 9002, + publicPath: '/', + staticPath: path.join(projectRoot, 'src/storybook/static'), + buildPath: path.join(distPath, 'storybook'), + // enables specific linters during webpack compilation, which will error your compile + enableESLintLoader: false, + enableTSLintLoader: false, + enableStyleLintPlugin: false, + }, distPath, buildPath: path.join(distPath, 'site'), // enable for local HTTPS dev-server diff --git a/build-tools/config/storybook/config.js b/build-tools/config/storybook/config.js new file mode 100644 index 00000000..44110e6c --- /dev/null +++ b/build-tools/config/storybook/config.js @@ -0,0 +1,9 @@ +import { configure } from 'storybook/utils/utils'; + +const context = require.context('app/component/', true, /preset\.js$/); + +function loadStories() { + context.keys().forEach(context); +} + +configure(loadStories); diff --git a/build-tools/config/storybook/webpack.config.base.js b/build-tools/config/storybook/webpack.config.base.js new file mode 100644 index 00000000..72c4775b --- /dev/null +++ b/build-tools/config/storybook/webpack.config.base.js @@ -0,0 +1,67 @@ +/** + * Webpack config used during development + */ +const path = require('path'); +const merge = require('webpack-merge'); +const config = require('../index'); + +const { + getBabelLoaderConfig, + getHbsInlineLoaderConfig, + getESLintLoader, + getTSLintLoader, + getStyleLintPlugin, +} = require('../webpack/webpack-helpers'); + +const projectRoot = path.resolve(__dirname, '../../../'); +const port = process.env.PORT || config.storybook.port; + +module.exports = merge(require('../webpack/webpack.config.base'), { + entry: { + storybook: [ + './src/app/polyfills.js', + './src/storybook/storybook.js', + './build-tools/config/storybook/config.js', + ], + story: [ + './src/app/polyfills.js', + './src/storybook/story.js', + './build-tools/config/storybook/config.js', + ], + }, + output: { + path: config.storybook.buildPath, + publicPath: config.storybook.publicPath, + }, + resolve: { + extensions: ['.hbs', '.ts', '.js', '.json'], + }, + module: { + rules: [ + { + test: /preset\.js$/, + include: [ + /src[\/\\]app/, + ], + use: [ + { + loader :'preset-loader', + options: {}, + }, + getHbsInlineLoaderConfig(), + getBabelLoaderConfig(), + ] + }, + getESLintLoader(config.storybook.enableESLintLoader), + getTSLintLoader(config.storybook.enableTSLintLoader), + { + test: /\.js$/, + enforce: 'pre', + loader: 'source-map-loader' + }, + ] + }, + plugins: [ + getStyleLintPlugin(config.storybook.enableStyleLintPlugin), + ].filter(_ => _), +}); diff --git a/build-tools/config/storybook/webpack.config.dist.js b/build-tools/config/storybook/webpack.config.dist.js new file mode 100644 index 00000000..4d2e611e --- /dev/null +++ b/build-tools/config/storybook/webpack.config.dist.js @@ -0,0 +1,78 @@ +/** + * Webpack config used during development + */ +const path = require('path'); +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const ImageminPlugin = require('imagemin-webpack-plugin').default; +const imageminMozjpeg = require('imagemin-mozjpeg'); + +const config = require('../index'); + +const { + getStyleRules, + getCodeRules, + getHandlebarsRules, +} = require('../webpack/webpack-helpers'); + +const projectRoot = path.resolve(__dirname, '../../../'); +const port = process.env.PORT || config.storybook.port; + +module.exports = merge(require('./webpack.config.base'), { + output: { + path: config.storybook.buildPath, + publicPath: config.storybook.publicPath, + }, + module: { + rules: [ + ...getHandlebarsRules({ development: true }), + ...getCodeRules(), + ...getStyleRules({ development: false }), + ] + }, + plugins: [ + // enable HMR globally + new webpack.HotModuleReplacementPlugin(), + + new webpack.DefinePlugin({ + 'process.env': config.dist.env, + }), + + new CopyWebpackPlugin([ + { + // copy files to public root (not versioned) + context: config.dist.staticPath, + from: '**/*', + to: config.storybook.buildPath, + }, + { + // copy files to public root (not versioned) + context: config.storybook.staticPath, + from: '**/*', + to: config.storybook.buildPath, + }, + ]), + + new ExtractTextPlugin({ + filename: 'asset/[name].css', + allChunks : true, + }), + + new ImageminPlugin({ + disable: !config.dist.enableImageOptimization, + svgo: null, + gifsicle: null, + jpegtran: null, + optipng: !config.dist.enablePNGQuant ? { optimizationLevel: 3 } : null, + pngquant: config.dist.enablePNGQuant ? { quality: '65' } : null, + plugins: [ + imageminMozjpeg({ + quality: 85, + progressive: true + }) + ], + }), + ].filter(_ => _), +}); diff --git a/build-tools/config/storybook/webpack.config.js b/build-tools/config/storybook/webpack.config.js new file mode 100644 index 00000000..a808569b --- /dev/null +++ b/build-tools/config/storybook/webpack.config.js @@ -0,0 +1,72 @@ +/** + * Webpack config used during development + */ +const fs = require('fs'); +const path = require('path'); +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const config = require('../index'); + +const { + getStyleRules, + getCodeRules, + getHandlebarsRules, +} = require('../webpack/webpack-helpers'); + +const projectRoot = path.resolve(__dirname, '../../../'); +const port = process.env.PORT || config.storybook.port; + +module.exports = merge(require('./webpack.config.base'), { + output: { + path: config.storybook.buildPath, + publicPath: config.storybook.publicPath, + }, + plugins: [ + // enable HMR globally + new webpack.HotModuleReplacementPlugin(), + + new webpack.DefinePlugin({ + 'process.env': config.dist.env, + }), + ].filter(_ => _), + module: { + rules: [ + ...getHandlebarsRules({ development: true }), + ...getCodeRules(), + ...getStyleRules({ development: true }), + ] + }, + devServer: { + hotOnly: true, + publicPath: config.storybook.publicPath, + contentBase: config.storybook.staticPath, + compress: true, + host: '0.0.0.0', + port, + disableHostCheck: true, + overlay: true, + noInfo: true, + before(app) { + // render basic default index.html for all html files (path will be picked by JS) + app.use((req, res, next) => { + if (req.path.includes('storybook.html')) { + res.send(fs.readFileSync(path.resolve(projectRoot, 'src/storybook/storybook.html'), 'utf-8')); + } else if (req.path.includes('story.html')) { + res.send(fs.readFileSync(path.resolve(projectRoot, 'src/storybook/story.html'), 'utf-8')); + } else { + next(); + } + }); + + // also render index.html on / + app.get('/', function(req, res) { + res.send(fs.readFileSync(path.resolve(projectRoot, 'src/storybook/storybook.html'), 'utf-8')); + }); + }, + https: (config.useHttps ? { + key: fs.readFileSync(path.resolve(projectRoot, 'build-tools/ssl/key.pem')), + cert: fs.readFileSync(path.resolve(projectRoot, 'build-tools/ssl/cert.pem')), + } : false), + }, + devtool: 'eval-source-map' +}); diff --git a/build-tools/config/webpack/webpack-helpers.js b/build-tools/config/webpack/webpack-helpers.js index 6716580e..64b7b408 100644 --- a/build-tools/config/webpack/webpack-helpers.js +++ b/build-tools/config/webpack/webpack-helpers.js @@ -6,6 +6,28 @@ const StyleLintPlugin = require('stylelint-webpack-plugin'); const projectRoot = path.resolve(__dirname, '../../../'); +function getHbsInlineLoaderConfig() { + return { + loader :'hbs-inline-loader', + options: { + hbsBuildOptions: { + removeScript: false, + removeStyle: false, + removeTemplate: false, + hot: true, + }, + hbsOptions: { + extensions: ['.hbs', ''], + partialDirs: [ + path.resolve(projectRoot, 'src/app/component'), + ], + ignoreHelpers: true, + debug: false, + }, + }, + } +} + function getBabelLoaderConfig() { return { loader :'babel-loader', @@ -118,6 +140,7 @@ function getCodeRules() { test: /\.js$/, include: [ /src[\/\\]app/, + /src[\/\\]storybook/, ], use: [ getBabelLoaderConfig() @@ -127,6 +150,7 @@ function getCodeRules() { test: /\.ts$/, include: [ /src[\/\\]app/, + /src[\/\\]storybook/, ], use: [ getBabelLoaderConfig(), @@ -138,24 +162,22 @@ function getCodeRules() { } ] }, - { - test: /\.modernizrrc$/, - use: [ - { loader: "modernizr-loader" }, - { loader: "json-loader" } - ] - }, ] } -function getStyleRules(development) { +/** + * @param options + * @param options.development + * @return {*[]} + */ +function getStyleRules(options) { // used in both dev and dist var cssRules = [ { loader: 'css-loader', options: { sourceMap: true, - minimize: !development + minimize: !options.development } }, { @@ -173,7 +195,7 @@ function getStyleRules(development) { } ]; - if (development) { + if (options.development) { cssRules.unshift({ loader: 'style-loader', options: { @@ -189,7 +211,7 @@ function getStyleRules(development) { include: path.resolve(projectRoot, 'src/app/font'), loader: 'file-loader', options: { - name: 'asset/font/[name].' + (development ? '' : '[hash:7].') + '[ext]', + name: 'asset/font/[name].' + (options.development ? '' : '[hash:7].') + '[ext]', }, }, { @@ -199,7 +221,7 @@ function getStyleRules(development) { loader: 'url-loader', options: { limit: 2000, - name: 'asset/image/[name].' + (development ? '' : '[hash:7].') + '[ext]', + name: 'asset/image/[name].' + (options.development ? '' : '[hash:7].') + '[ext]', }, }, ], @@ -230,35 +252,67 @@ function getStyleRules(development) { }, ]; - if (development) { + if (options.development) { // dev uses 'use' styleRules.unshift({ test: /\.scss$/, use: cssRules, }); + styleRules.unshift({ + test: /\.css$/, + use: [{ + loader: 'style-loader', + options: { + sourceMap: true, + }, + }, { + loader: 'css-loader', + options: { + sourceMap: true, + minimize: !options.development, + }, + }], + }); } else { // dust uses single ExtractTextPlugin loader styleRules.unshift({ test: /\.scss$/, loader: ExtractTextPlugin.extract(cssRules), }); + styleRules.unshift({ + test: /\.css$/, + loader: ExtractTextPlugin.extract([{ + loader: 'css-loader', + options: { + sourceMap: true, + minimize: !options.development, + }, + }]), + }); } return styleRules; } -function getHandlebarsRules(development, buildType) { +/** + * + * @param options + * @param options.development + * @param options.buildType + * @return {*[]} + */ +function getHandlebarsRules(options) { return [ { test: /\.hbs/, use: [ { - loader: path.resolve(__dirname, '../../hbs-build-loader'), + loader: 'hbs-build-loader', options: { - removeScript: development ? false : buildType !== 'code', - removeStyle: development ? false : buildType !== 'code', - removeTemplate: development ? false : buildType === 'code', - hot: development, + removeScript: options.development ? false : options.buildType !== 'code', + removeStyle: options.development ? false : options.buildType !== 'code', + removeTemplate: options.development ? false : options.buildType === 'code', + hot: options.development, } }, { @@ -272,8 +326,8 @@ function getHandlebarsRules(development, buildType) { } }, { - loader: path.resolve(__dirname, '../../partial-comment-loader'), - } + loader: 'partial-comment-loader', + }, ] } ]; @@ -284,7 +338,8 @@ function getDirectoryNamedWebpackPlugin() { honorIndex: false, // defaults to false ignoreFn: function(webpackResolveRequest) { - return !webpackResolveRequest.path.includes(path.join('app', 'component')); + return !(webpackResolveRequest.path.includes(path.join('app', 'component')) || + webpackResolveRequest.path.includes(path.join('storybook'))); // custom logic to decide whether request should be ignored // return true if request should be ignored, false otherwise @@ -329,7 +384,7 @@ function getESLintLoader(enabled) { }, ], include: [ - path.join(projectRoot, 'src') + path.join(projectRoot, 'src'), ], } : {}; }; @@ -351,7 +406,7 @@ function getTSLintLoader(enabled) { }, ], include: [ - path.join(projectRoot, 'src') + path.join(projectRoot, 'src'), ], exclude: /node_modules|vendor/ } : {}; @@ -364,6 +419,8 @@ function getStyleLintPlugin(enabled) { } module.exports = { + getBabelLoaderConfig, + getHbsInlineLoaderConfig, getCodeRules, getStyleRules, getHandlebarsRules, diff --git a/build-tools/config/webpack/webpack.config.base.js b/build-tools/config/webpack/webpack.config.base.js index c168918a..34d60940 100644 --- a/build-tools/config/webpack/webpack.config.base.js +++ b/build-tools/config/webpack/webpack.config.base.js @@ -32,6 +32,30 @@ const webpackConfig = { TweenLite: path.resolve(projectRoot, 'node_modules/gsap/src/uncompressed/TweenLite'), }, }, + resolveLoader: { + modules: [ + 'node_modules', + path.resolve(__dirname, '../../loaders'), + ], + }, + module: { + rules: [ + { + test: /\.modernizrrc$/, + use: [ + { loader: "modernizr-loader" }, + { loader: "json-loader" } + ] + }, + { + test: /\.json$/, + use: [ + { loader: "json-partial-loader" }, + { loader: "json-loader" } + ] + }, + ] + }, plugins: [ // Friendly webpack errors new FriendlyErrorsWebpackPlugin(), diff --git a/build-tools/config/webpack/webpack.config.code.base.js b/build-tools/config/webpack/webpack.config.code.base.js index d9b334d7..b4c387cf 100644 --- a/build-tools/config/webpack/webpack.config.code.base.js +++ b/build-tools/config/webpack/webpack.config.code.base.js @@ -33,9 +33,9 @@ module.exports = merge(require('./webpack.config.base'), { }, module: { rules: [ - ...getHandlebarsRules(false, 'code'), + ...getHandlebarsRules({ development: false, buildType: 'code'}), ...getCodeRules(), - ...getStyleRules(false), + ...getStyleRules({ development: false }), ] }, plugins: [ diff --git a/build-tools/config/webpack/webpack.config.code.dist.js b/build-tools/config/webpack/webpack.config.code.dist.js index 23733235..9932e58d 100644 --- a/build-tools/config/webpack/webpack.config.code.dist.js +++ b/build-tools/config/webpack/webpack.config.code.dist.js @@ -34,7 +34,7 @@ const webpackConfig = merge(require('./webpack.config.code.base'), { new BundleAnalyzerPlugin({ analyzerMode: 'disabled', generateStatsFile: true, - statsFilename: path.resolve(projectRoot, 'bundlesize-profile.json'), + statsFilename: path.resolve(config.distPath, 'bundlesize-profile.json'), }), new ImageminPlugin({ disable: !config.dist.enableImageOptimization, diff --git a/build-tools/config/webpack/webpack.config.js b/build-tools/config/webpack/webpack.config.js index db6bab6e..7e7a19bd 100644 --- a/build-tools/config/webpack/webpack.config.js +++ b/build-tools/config/webpack/webpack.config.js @@ -44,9 +44,9 @@ module.exports = merge(require('./webpack.config.base'), { ].filter(_ => _), module: { rules: [ - ...getHandlebarsRules(true), + ...getHandlebarsRules({ development: true }), ...getCodeRules(), - ...getStyleRules(true), + ...getStyleRules({ development: true }), getESLintLoader(config.dev.enableESLintLoader), getTSLintLoader(config.dev.enableTSLintLoader), { diff --git a/build-tools/config/webpack/webpack.config.partials.js b/build-tools/config/webpack/webpack.config.partials.js index bcfefce0..1f5cc0d0 100644 --- a/build-tools/config/webpack/webpack.config.partials.js +++ b/build-tools/config/webpack/webpack.config.partials.js @@ -23,7 +23,7 @@ module.exports = merge(require('./webpack.config.base'), { }, module: { rules: [ - ...getHandlebarsRules(false, 'partials'), + ...getHandlebarsRules({ development: false, buildType: 'partials' }), { test: /\.scss$/, use: [{ diff --git a/build-tools/loaders/extract-template-loader.js b/build-tools/loaders/extract-template-loader.js new file mode 100644 index 00000000..4f27de67 --- /dev/null +++ b/build-tools/loaders/extract-template-loader.js @@ -0,0 +1,45 @@ +const loaderUtils = require('loader-utils'); + +/** + * Processes handlebar templates to import script and style files. + * Also has an option to remove al the template code itself to only extract the scripts out of it + * + * For scripts: + * - Changes the html script include to a js file require + * - Also registers the class to be initialized + * - Has support for hot reloading + * + * For styles: + * - Changes the html style link to a css file require + */ +module.exports = function(content) { + const loaderContext = this; + const done = this.async(); + this.cacheable(); + + const options = loaderUtils.getOptions(this); + + let newContent = ''; + let match; + let index = 0; + const regex = /(["'`])([\s\S]*?)<\/hbs>\1/gi; + do { + match = regex.exec(content); + if (match && index === parseInt(options.target, 10)) { + newContent = match[2].replace(/\\"/gi, '"').replace(/^\n/, ''); + + // strip leading tabs + const match2 = /^([\t]*)/gi.exec(newContent); + + if (match2 && match2[0].length) { + newContent = newContent.replace(new RegExp(`^\\t{${match2[0].length}}`, 'gmi'), ''); + } + + done(null, newContent); + return; + } + ++index; + } while (match); + + done(null, newContent); +}; diff --git a/build-tools/hbs-build-loader.js b/build-tools/loaders/hbs-build-loader.js similarity index 95% rename from build-tools/hbs-build-loader.js rename to build-tools/loaders/hbs-build-loader.js index 16204755..b2cf7ea7 100644 --- a/build-tools/hbs-build-loader.js +++ b/build-tools/loaders/hbs-build-loader.js @@ -43,9 +43,9 @@ module.exports = function(content) { newContent = ` ${scripts.map(script => ` var component = require(${loaderUtils.stringifyRequest(loaderContext, script)}).default; -var registerComponent = require(${loaderUtils.stringifyRequest(loaderContext, 'app/util/components.ts')}).registerComponent; +var registerComponent = require(${loaderUtils.stringifyRequest(loaderContext, 'app/muban/componentUtils.ts')}).registerComponent; registerComponent(component); -${hot ? `var updateComponent = require(${loaderUtils.stringifyRequest(loaderContext, 'app/util/components.ts')}).updateComponent; +${hot ? `var updateComponent = require(${loaderUtils.stringifyRequest(loaderContext, 'app/muban/componentUtils.ts')}).updateComponent; // Hot Module Replacement API if (module.hot) { diff --git a/build-tools/loaders/hbs-inline-loader.js b/build-tools/loaders/hbs-inline-loader.js new file mode 100644 index 00000000..8d5cb07a --- /dev/null +++ b/build-tools/loaders/hbs-inline-loader.js @@ -0,0 +1,49 @@ +const path = require('path'); +const loaderUtils = require('loader-utils'); + +/** + * Processes handlebar templates to import script and style files. + * Also has an option to remove al the template code itself to only extract the scripts out of it + * + * For scripts: + * - Changes the html script include to a js file require + * - Also registers the class to be initialized + * - Has support for hot reloading + * + * For styles: + * - Changes the html style link to a css file require + */ +module.exports = function(content) { + const loaderContext = this; + const done = this.async(); + this.cacheable(); + + const options = loaderUtils.getOptions(this); + + const currentModuleName = './' + this.resourcePath.split(path.sep).pop(); + + const hbsBuildLoaderParams = JSON.stringify(options.hbsBuildOptions); + const hbsLoaderParams = JSON.stringify(options.hbsOptions); + + let index = 0; + let newContent = content.replace( + /(["'])(.*?)<\/hbs>\1/gi, + (match, quote, template) => { + + // strip leading tabs + let content = template.replace(/(^\\n|(\\n|\\t)+$)/g, ''); + const match2 = /^([\\t]*)/gi.exec(content); + + if (match2 && match2[0].length) { + content = content.replace(new RegExp(`(\\\\n|^)(\\\\t){${match2[0].length / 2}}`, 'gmi'), '$1'); + } + + return `{ + compiled: require('!!hbs-build-loader?${hbsBuildLoaderParams}!handlebars-loader?${hbsLoaderParams}!extract-template-loader?target=${index++}!${currentModuleName}'), + raw: '${content.replace(/'/gi, '\\\'')}', + }`; + } + ); + + done(null, newContent); +}; diff --git a/build-tools/loaders/hbs-source-loader.js b/build-tools/loaders/hbs-source-loader.js new file mode 100644 index 00000000..8409a92e --- /dev/null +++ b/build-tools/loaders/hbs-source-loader.js @@ -0,0 +1,52 @@ +const path = require('path'); +const loaderUtils = require('loader-utils'); + +/** + * Processes handlebar templates to import script and style files. + * Also has an option to remove al the template code itself to only extract the scripts out of it + * + * For scripts: + * - Changes the html script include to a js file require + * - Also registers the class to be initialized + * - Has support for hot reloading + * + * For styles: + * - Changes the html style link to a css file require + */ +module.exports = function(content) { + const loaderContext = this; + const done = this.async(); + this.cacheable(); + + const options = loaderUtils.getOptions(this) || {}; + + const scripts = []; + const styles = []; + + content = content.replace(/ + +
+ Muban + + + + + + {{> ../search-results/search-results.hbs }} + +
diff --git a/src/storybook/component/header/header.scss b/src/storybook/component/header/header.scss new file mode 100644 index 00000000..bd56b218 --- /dev/null +++ b/src/storybook/component/header/header.scss @@ -0,0 +1,132 @@ +header { + position: fixed; + width: 100%; + height: 50px; + padding: 0 20px; + background-color: #546e7a; + box-shadow: 0 5px 10px 0px rgba(0, 0, 0, 0.25); + z-index: 2; + box-sizing: border-box; + + .header-title { + font-size: 24px; + font-weight: 300; + line-height: 50px; + color: white; + } + + .search { + position: absolute; + top: 7px; + left: 50%; + transform: translate(-50%, 0); + width: 600px; + + input { + box-sizing: border-box; + font-size: 18px; + font-weight: 300; + color: #607d8b; + background-color: #eceff1; + padding: 7px 15px; + width: 100%; + border: 0; + box-shadow: inset 0 2px 1px rgba(0, 0, 0, 0.3); + + &::placeholder { + color: #b0bec5; + } + + &:focus { + outline: none; + background-color: white; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5); + } + } + } + + .header-nav { + float: right; + margin: 0; + padding: 0; + list-style: none; + font-size: 0; + + li { + display: inline-block; + line-height: 50px; + transition: background-color 0.3s; + font-size: 16px; + + &:hover, + &.selected { + background-color: #455a64; + + .device-icon { + .phone, + .tablet { + background-color: #455a64; + } + } + } + + &.size-xs { + font-weight: 200; + } + &.size-s { + font-weight: 300; + } + &.size-m { + font-weight: 400; + } + &.size-l { + font-weight: 500; + } + &.size-xl { + font-weight: 600; + } + + a { + padding: 0 15px; + color: white; + display: block; + } + + .device-icon { + left: -8px; + position: relative; + + .phone, + .tablet { + position: absolute; + display: block; + border: 1px solid white; + border-radius: 1px; + background-color: #546e7a; + } + .phone { + top: 6px; + left: 1px; + width: 5px; + height: 10px; + + .bezel { + display: block; + width: 100%; + top: 0; + bottom: 0; + position: absolute; + border-top: 1px solid white; + border-bottom: 1px solid white; + } + } + .tablet { + top: 0; + left: 4px; + width: 11px; + height: 16px; + } + } + } + } +} diff --git a/src/storybook/component/search-results/search-results.hbs b/src/storybook/component/search-results/search-results.hbs new file mode 100644 index 00000000..fbe1377a --- /dev/null +++ b/src/storybook/component/search-results/search-results.hbs @@ -0,0 +1,28 @@ + +
+
+ + +
diff --git a/src/storybook/component/search-results/search-results.scss b/src/storybook/component/search-results/search-results.scss new file mode 100644 index 00000000..10599fca --- /dev/null +++ b/src/storybook/component/search-results/search-results.scss @@ -0,0 +1,160 @@ +.search-results { + background-color: white; + position: absolute; + left: 0; + top: 50px; + width: 100%; + margin: 0; + padding: 20px 0; + box-shadow: 0 0px 10px 0px rgba(0, 0, 0, 0.25); + display: none; + opacity: 0; + visibility: hidden; + transition: visibility 0s linear 0.3s, opacity 0.15s linear; + + &.active { + display: block; + } + &.opened { + opacity: 1; + visibility: visible; + transition: visibility 0s linear 0s, opacity 0.3s linear; + } + + .backdrop { + position: absolute; + left: 0; + right: 0; + top: 100%; + height: 100vh; + background-color: rgba(0, 0, 0, 0.2); + } + + .result-list { + position: relative; + z-index: 1; + max-height: 400px; + overflow-y: scroll; + overflow-x: hidden; + margin: 0; + padding: 0; + list-style: none; + font-size: 14px; + background-color: white; + + li { + width: 100%; + position: relative; + + a { + text-decoration: none; + width: 100%; + height: 100%; + display: flex; + position: relative; + box-sizing: border-box; + max-width: 570px; + margin: 0 auto; + padding: 8px 0; + font-weight: 300; + cursor: pointer; + transition: all 0.2s; + } + + &:before, + &:after { + position: absolute; + width: 200%; + content: ''; + height: 100%; + top: 0; + background-color: white; + transition: all 0.2s; + } + + &:before { + left: -200%; + } + &:after { + right: -200%; + } + + &:hover { + background-color: #eceff1; + &:before, + &:after { + background-color: #eceff1; + } + + .right, + .file, + .description { + color: #90a4ae; + } + + .label, + .name { + color: darken(#263238, 20%); + } + } + + &.group { + margin-top: 7px; + padding-right: 0; + } + + &.variant { + padding-left: 20px; + + .row { + padding-right: 100px; + } + } + + .row { + position: relative; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + box-sizing: border-box; + display: inline-block; + width: 100%; + color: #cfd8dc; + } + + .name, + .file, + .label, + .description { + transition: color 0.2s; + } + + .name { + color: #263238; + font-weight: 400; + } + + .file { + font-family: monospace; + font-size: 12px; + padding-left: 10px; + } + + .label { + color: #37474f; + } + + .description { + padding-left: 10px; + } + + .right { + flex-basis: 100px; + text-align: right; + color: #eceff1; + font-weight: 300; + transition: all 0.2s; + } + } + } +} diff --git a/src/storybook/component/story-info/story-info.hbs b/src/storybook/component/story-info/story-info.hbs new file mode 100644 index 00000000..1254bec2 --- /dev/null +++ b/src/storybook/component/story-info/story-info.hbs @@ -0,0 +1,7 @@ + + + diff --git a/src/storybook/component/story-info/story-info.scss b/src/storybook/component/story-info/story-info.scss new file mode 100644 index 00000000..c379f746 --- /dev/null +++ b/src/storybook/component/story-info/story-info.scss @@ -0,0 +1,27 @@ +.story-info { + width: 100px; + padding: 20px; + + h1 { + font-weight: 300; + color: #607d8b; + } + + > a:hover h1 { + color: darken(#607d8b, 10%); + } + + .path { + margin-top: 0; + + a { + font-family: monospace; + color: #90a4ae; + margin: 0; + + &:hover { + color: darken(#90a4ae, 10%); + } + } + } +} diff --git a/src/storybook/component/story-item/story-item.hbs b/src/storybook/component/story-item/story-item.hbs new file mode 100644 index 00000000..6d2e6dcb --- /dev/null +++ b/src/storybook/component/story-item/story-item.hbs @@ -0,0 +1,11 @@ + + +
+
+ {{> ../story-info/story-info }} + {{> ../story-source/story-source }} +
+ +
{{{rendered}}}
+
+ diff --git a/src/storybook/component/story-item/story-item.scss b/src/storybook/component/story-item/story-item.scss new file mode 100644 index 00000000..1b706c7e --- /dev/null +++ b/src/storybook/component/story-item/story-item.scss @@ -0,0 +1,27 @@ +.story-item { + background-color: white; + margin: 20px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), + 0 3px 1px -2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + + .story-top { + display: flex; + flex-direction: row; + color: #607d8b; + + .story-info { + flex: 4; + } + .story-source { + flex: 6; + width: 200px; + padding: 20px; + } + } + + .story-component { + padding: 20px; + border-top: 1px solid #ddd; + } +} diff --git a/src/storybook/component/story-source/story-source.hbs b/src/storybook/component/story-source/story-source.hbs new file mode 100644 index 00000000..1bfc1083 --- /dev/null +++ b/src/storybook/component/story-source/story-source.hbs @@ -0,0 +1,14 @@ + + +
+ {{> ../tabs/tabs tabData }} + +
+
{{{usage}}}
+
{{{data}}}
+
{{{component}}}
+
{{{templateSource}}}
+
{{{styleSource}}}
+
{{{scriptSource}}}
+
+
diff --git a/src/storybook/component/story-source/story-source.scss b/src/storybook/component/story-source/story-source.scss new file mode 100644 index 00000000..4beb7590 --- /dev/null +++ b/src/storybook/component/story-source/story-source.scss @@ -0,0 +1,24 @@ +.story-source { + display: flex; + flex-direction: column; + + [data-component='tabs'] { + flex-grow: 0; + } + + .tab-container { + flex-grow: 1; + } + + pre { + max-height: 300px; + border: 1px solid #eee; + background-color: #f8f8f8; + margin: 0; + height: auto; + min-height: 55px; + overflow-x: auto; + line-height: 18px; + padding: 15px; + } +} diff --git a/src/storybook/component/storybook/Storybook.ts b/src/storybook/component/storybook/Storybook.ts new file mode 100644 index 00000000..bccc91e6 --- /dev/null +++ b/src/storybook/component/storybook/Storybook.ts @@ -0,0 +1,146 @@ +import $ from 'jquery'; +import ko from 'knockout'; +import AbstractComponent from 'app/component/AbstractComponent'; +import model from '../../model'; + +const RESIZER_WIDTH = 13; + +export default class Storybook extends AbstractComponent { + static displayName: string = 'storybook'; + + private offsetX: number; + private isBefore: boolean; + private content: Element; + + constructor(el: HTMLElement) { + super(el); + + this.content = document.querySelector('.content'); + $('.resizer', el).on('mousedown', this.onMouseDown); + + $('.settings .generic', this.element).on('click', '.size', this.onSizeClick); + + $('.settings .bar', this.element).on('click', this.onMediaBarClick); + + ko.applyBindingsToNode($('body')[0], { + css: { 'device-emulator': model.deviceEmulateEnabled }, + }); + + ko.applyBindingAccessorsToNode( + this.content, + { + style: () => ({ + maxWidth: model.deviceEmulateEnabled() ? model.viewportWidth() + 'px' : '100%', + }), + css: () => ({ resizing: model.isResizingViewport }), + }, + {}, + ); + + ko.applyBindingAccessorsToNode( + $('.current-size .value')[0], + { + text: () => (model.deviceEmulateEnabled() ? model.viewportWidth() + 'px' : '100%'), + }, + {}, + ); + + $('.bar') + .toArray() + .forEach(bar => { + ko.applyBindingAccessorsToNode( + bar, + { + css: () => { + let min: any = bar.getAttribute('data-size-min'); + let max: any = bar.getAttribute('data-size-max'); + min = min === '*' ? min : parseInt(min, 10); + max = max === '*' ? max : parseInt(max, 10); + + return { + active: + (model.viewportWidth() >= min || min === '*') && + (model.viewportWidth() <= max || max === '*'), + }; + }, + }, + {}, + ); + }); + } + + private onMediaBarClick = event => { + const type = event.currentTarget.getAttribute('data-size-type'); + const min = parseInt(event.currentTarget.getAttribute('data-size-min'), 10); + const max = parseInt(event.currentTarget.getAttribute('data-size-max'), 10); + const currentWidth = model.viewportWidth(); + let newWidth = currentWidth; + + if (type === 'max') { + newWidth = max; + } + if (type === 'min') { + newWidth = min; + } + if (type === 'min-max') { + if (min === currentWidth) { + newWidth = max; + } else { + newWidth = min; + } + } + + if (newWidth !== currentWidth) { + model.viewportWidth(newWidth); + } + }; + + private onSizeClick = event => { + const value = parseInt(event.currentTarget.getAttribute('data-size'), 10); + model.viewportWidth(value); + }; + + private onMouseDown = event => { + this.offsetX = event.offsetX; + this.isBefore = $(event.currentTarget).hasClass('before'); + + $(window).on('mousemove', this.onMouseMove); + $(window).on('mouseup', this.onMouseUp); + + model.isResizingViewport(true); + }; + + private onMouseMove = event => { + this.updateView(event.pageX); + }; + + private onMouseUp = event => { + $(window).off('mousemove', this.onMouseMove); + $(window).off('mouseup', this.onMouseUp); + + model.isResizingViewport(false); + }; + + private updateView(pageX) { + const centerX = window.innerWidth / 2; + let sideOffset; + let halfWidth; + if (this.isBefore) { + sideOffset = pageX + (RESIZER_WIDTH - this.offsetX); + halfWidth = centerX - sideOffset; + } else { + sideOffset = pageX - this.offsetX; + halfWidth = sideOffset - centerX; + } + + model.viewportWidth(halfWidth * 2); + } + + public dispose() { + $('.resizer', this.element).off('mousedown'); + $('.settings .generic', this.element).off('click'); + $('.settings .bar', this.element).off('click', this.onMediaBarClick); + + super.dispose(); + } +} diff --git a/src/storybook/component/storybook/storybook.hbs b/src/storybook/component/storybook/storybook.hbs new file mode 100644 index 00000000..56ee4e81 --- /dev/null +++ b/src/storybook/component/storybook/storybook.hbs @@ -0,0 +1,80 @@ + + + +
+ + {{> ../header/header }} + +
+
+
+
123px
+
+
4k - 2650px
+
Laptop L - 1440px
+
Laptop - 1024px
+
Tablet - 768px
+
Mobile L - 425px
+
Mobile M - 375px
+
Mobile S - 320px
+
+
+
+ 1100px + 1100px +
+
+ 700px + 700px +
+
+
+
+ 800px + 1000px +
+
+ 800px + 1000px +
+
+ 900px + 1050px +
+
+ 900px + 1050px +
+
+
+
+ 600px +
+
+ 600px +
+
+ 1200px +
+
+ 1200px +
+
+
+ +
+
+
+ +
+
+ + {{#if path}} +
+ {{> ../story-info/story-info }} + {{> ../story-source/story-source }} + +
+ {{/if}} + +
diff --git a/src/storybook/component/storybook/storybook.scss b/src/storybook/component/storybook/storybook.scss new file mode 100644 index 00000000..dfa91859 --- /dev/null +++ b/src/storybook/component/storybook/storybook.scss @@ -0,0 +1,351 @@ +html, +body { + height: 100%; + font-family: 'Roboto', sans-serif; +} + +.hidden { + display: none; +} + +a { + color: dodgerblue; + text-decoration: none; +} + +@media (min-width: 600px) { + a { + color: red; + } +} +@media (max-width: 700px) { + a { + color: green; + } +} + +@media (min-width: 1200px) { + a { + color: blue; + } +} +@media (max-width: 1100px) { + a { + color: purple; + } +} + +@media (min-width: 800px) and (max-width: 1000px) { + a { + color: pink; + } +} +@media (min-width: 900px) and (max-width: 1050px) { + a { + color: yellow; + } +} + +#app { + height: 100%; + background-color: rgba(0, 0, 0, 0.05); +} + +.settings { + display: none; + top: 60px; + position: relative; + background-color: #fafafa; + + .device-emulator & { + display: block; + } + + .generic { + position: relative; + height: 20px; + width: 100%; + background-color: #ddd; + + .current-size { + position: absolute; + left: 50%; + transform: translate(-50%, 0); + line-height: 16px; + font-size: 12px; + color: #666; + top: 2px; + } + + .size { + position: absolute; + left: 50%; + transform: translate(-50%, 0); + height: 16px; + border: 2px solid #f2f2f2; + font-size: 0; + line-height: 16px; + text-align: center; + color: #666; + cursor: pointer; + + &.mobile-s { + width: 320px; + } + &.mobile-m { + width: 375px; + } + &.mobile-l { + width: 425px; + } + &.tablet { + width: 768px; + } + &.laptop { + width: 1024px; + } + &.laptop-l { + width: 1440px; + } + &.fourk { + width: 2560px; + } + + &:hover { + background-color: #ccc; + font-size: 12px; + //z-index: 1; + } + } + } + + .max-width, + .min-max-width, + .min-width { + position: relative; + height: 16px; + width: 100%; + + .bar { + position: absolute; + top: 2px; + height: 12px; + //border: 2px solid #f2f2f2; + //font-size: 0; + line-height: 16px; + color: #666; + cursor: pointer; + border-left: 2px solid transparent; + border-right: 2px solid transparent; + box-sizing: border-box; + + &:hover { + height: 16px; + top: 0; + + .label { + display: block; + } + } + + .label { + display: none; + line-height: 16px; + font-size: 12px; + color: #333; + top: 2px; + padding: 0 4px; + pointer-events: none; + } + } + } + + .max-width { + .bar { + left: 50%; + transform: translate(-50%, 0); + background-color: #e3f5ff; + border-left-color: #48a6f3; + border-right-color: #48a6f3; + + &:hover, + &.active { + background-color: #92cbf8; + } + + .label { + &.left { + float: left; + } + + &.right { + float: right; + } + } + } + } + + .min-max-width { + .bar { + background-color: #eaf6eb; + border-left-color: #6a9d3f; + border-right-color: #6a9d3f; + + &:hover, + &.active { + background-color: #c4e0a5; + } + &:hover { + z-index: 1; + } + + .label { + position: absolute; + padding: 0; + top: 0; + + &.left { + right: calc(100% + 6px); + } + + &.right { + left: calc(100% + 6px); + } + } + } + } + + .min-width { + .bar { + background-color: #fff4e2; + border-left-color: #f37a22; + border-right-color: #f37a22; + + &:hover, + &.active { + background-color: #fecb84; + } + + &.left { + .label { + float: right; + } + } + } + } +} + +.content { + position: absolute; + font-size: 0; + top: 50px; + bottom: 0; + width: 100%; + left: 50%; + transform: translate(-50%, 0); + + .device-emulator & { + top: 150px; + bottom: 10px; + box-shadow: 0 0 10px 2px rgba(0, 0, 0, 0.14); + + .resizer { + display: block; + } + } + + .resizer { + display: none; + position: absolute; + width: 13px; + height: 100%; + top: 0; + background-color: rgba(0, 0, 0, 0.05); + cursor: col-resize; + user-select: none; + transition: background-color 0.2s; + + &:hover { + background-color: rgba(0, 0, 0, 0.1); + } + + &.before { + left: -12px; + } + &.after { + right: -12px; + } + + &:after { + position: absolute; + content: '|||'; + height: 14px; + top: 50%; + left: 3px; + color: rgba(0, 0, 0, 0.25); + width: 12px; + font-size: 10px; + } + } + + &.single-story { + bottom: 368px; + } + + .story-frame { + //margin-top: 20px; + width: 100%; + height: 100%; + } + + .blocker { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + opacity: 0; + display: none; + } + + &.resizing { + .blocker { + display: block; + } + } +} + +.story-panel { + background-color: white; + border-top: 1px solid #eee; + position: fixed; + z-index: 1; + bottom: 0; + width: 100%; + min-height: 355px; + display: flex; + flex-direction: row; + box-shadow: 0 0px 10px 0px rgba(0, 0, 0, 0.25); + box-sizing: border-box; + color: #607d8b; + + .story-info { + flex: 4; + + .path { + margin-top: 15px; + } + } + + .story-source { + flex: 6; + width: 200px; + margin-top: 20px; + padding: 20px; + + pre { + height: 235px; + min-height: 235px; + } + } +} diff --git a/src/storybook/component/tabs/Tabs.ts b/src/storybook/component/tabs/Tabs.ts new file mode 100644 index 00000000..88affb5e --- /dev/null +++ b/src/storybook/component/tabs/Tabs.ts @@ -0,0 +1,52 @@ +import AbstractComponent from 'app/component/AbstractComponent'; + +export default class Tabs extends AbstractComponent { + static displayName: string = 'tabs'; + + private tabs: Array; + private tabId: string; + private tabContainers: Array; + private contentMore: HTMLParagraphElement; + + constructor(el: HTMLElement) { + super(el); + + this.tabId = this.element.getAttribute('data-tab-id'); + this.tabs = Array.from(this.element.querySelectorAll('li')); + this.tabContainers = Array.from(document.querySelectorAll(`[data-tab="${this.tabId}"]`)); + + for (const tab of this.tabs) { + tab.addEventListener('click', this.onTabClick); + } + + this.selectTab(0); + } + + private onTabClick = event => { + const selectedIndex = this.tabs.indexOf(event.target); + + this.selectTab(selectedIndex); + }; + + public selectTab(selectedIndex: number) { + this.tabContainers.forEach((tabContainer, index) => { + index === selectedIndex + ? tabContainer.classList.remove('hidden') + : tabContainer.classList.add('hidden'); + }); + + this.tabs.forEach((tab, index) => { + index === selectedIndex ? tab.classList.add('selected') : tab.classList.remove('selected'); + }); + } + + public dispose() { + for (const tab of this.tabs) { + tab.removeEventListener('click', this.onTabClick); + } + this.tabs = null; + this.tabContainers = null; + + super.dispose(); + } +} diff --git a/src/storybook/component/tabs/tabs.hbs b/src/storybook/component/tabs/tabs.hbs new file mode 100644 index 00000000..f8b81eb8 --- /dev/null +++ b/src/storybook/component/tabs/tabs.hbs @@ -0,0 +1,8 @@ + + + +
    + {{#each tabs}} +
  • {{label}}
  • + {{/each}} +
diff --git a/src/storybook/component/tabs/tabs.scss b/src/storybook/component/tabs/tabs.scss new file mode 100644 index 00000000..2b46ed77 --- /dev/null +++ b/src/storybook/component/tabs/tabs.scss @@ -0,0 +1,34 @@ +[data-component='tabs'] { + margin: 0; + padding: 0; + list-style: none; + font-size: 0; + position: relative; + top: 1px; + z-index: 1; + display: flex; + flex-direction: row; + + li { + font-size: 16px; + flex-shrink: 1; + cursor: pointer; + padding: 10px 20px; + margin: 0; + border: 1px solid #ddd; + background-color: #eee; + text-align: center; + overflow-x: hidden; + + &.selected { + font-weight: bold; + background-color: #f8f8f8; + border-color: #eee; + border-bottom-color: #f8f8f8; + } + } +} + +.tab-content { + margin-top: 0; +} diff --git a/src/storybook/model.ts b/src/storybook/model.ts new file mode 100644 index 00000000..139476e3 --- /dev/null +++ b/src/storybook/model.ts @@ -0,0 +1,9 @@ +import ko from 'knockout'; + +class Model { + public deviceEmulateEnabled: KnockoutObservable = ko.observable(false); + public viewportWidth: KnockoutObservable = ko.observable(1024); + public isResizingViewport: KnockoutObservable = ko.observable(false); +} + +export default new Model(); diff --git a/src/storybook/static/README.md b/src/storybook/static/README.md new file mode 100644 index 00000000..b7a4f523 --- /dev/null +++ b/src/storybook/static/README.md @@ -0,0 +1 @@ +Files in here will not be processed by webpack, but instead copied over the the build folder. diff --git a/src/storybook/static/index.html b/src/storybook/static/index.html new file mode 100644 index 00000000..461e68a7 --- /dev/null +++ b/src/storybook/static/index.html @@ -0,0 +1,17 @@ + + + + + + + + +
+ + + diff --git a/src/storybook/static/story.html b/src/storybook/static/story.html new file mode 100644 index 00000000..e80df9b2 --- /dev/null +++ b/src/storybook/static/story.html @@ -0,0 +1,17 @@ + + + + + + + + +
+ + + diff --git a/src/storybook/story-list.hbs b/src/storybook/story-list.hbs new file mode 100644 index 00000000..e8247e17 --- /dev/null +++ b/src/storybook/story-list.hbs @@ -0,0 +1,5 @@ + + +{{#each stories}} + {{> component/story-item/story-item}} +{{/each}} diff --git a/src/storybook/story-list.scss b/src/storybook/story-list.scss new file mode 100644 index 00000000..7ffe004b --- /dev/null +++ b/src/storybook/story-list.scss @@ -0,0 +1,94 @@ +html, +body { + font-family: 'Roboto', sans-serif; + background-color: white; +} + +.hidden { + display: none; +} + +a { + color: dodgerblue; + text-decoration: none; +} + +.blue-grey { + background-color: #607d8b !important; +} + +.blue-grey-text { + color: #607d8b !important; +} + +.blue-grey.lighten-5 { + background-color: #eceff1 !important; +} + +.blue-grey-text.text-lighten-5 { + color: #eceff1 !important; +} + +.blue-grey.lighten-4 { + background-color: #cfd8dc !important; +} + +.blue-grey-text.text-lighten-4 { + color: #cfd8dc !important; +} + +.blue-grey.lighten-3 { + background-color: #b0bec5 !important; +} + +.blue-grey-text.text-lighten-3 { + color: #b0bec5 !important; +} + +.blue-grey.lighten-2 { + background-color: #90a4ae !important; +} + +.blue-grey-text.text-lighten-2 { + color: #90a4ae !important; +} + +.blue-grey.lighten-1 { + background-color: #78909c !important; +} + +.blue-grey-text.text-lighten-1 { + color: #78909c !important; +} + +.blue-grey.darken-1 { + background-color: #546e7a !important; +} + +.blue-grey-text.text-darken-1 { + color: #546e7a !important; +} + +.blue-grey.darken-2 { + background-color: #455a64 !important; +} + +.blue-grey-text.text-darken-2 { + color: #455a64 !important; +} + +.blue-grey.darken-3 { + background-color: #37474f !important; +} + +.blue-grey-text.text-darken-3 { + color: #37474f !important; +} + +.blue-grey.darken-4 { + background-color: #263238 !important; +} + +.blue-grey-text.text-darken-4 { + color: #263238 !important; +} diff --git a/src/storybook/story-single.hbs b/src/storybook/story-single.hbs new file mode 100644 index 00000000..dc51787b --- /dev/null +++ b/src/storybook/story-single.hbs @@ -0,0 +1 @@ +{{{component}}} diff --git a/src/storybook/story.html b/src/storybook/story.html new file mode 100644 index 00000000..fc3c0cd1 --- /dev/null +++ b/src/storybook/story.html @@ -0,0 +1,16 @@ + + + + + + + +
+ + + diff --git a/src/storybook/story.js b/src/storybook/story.js new file mode 100644 index 00000000..ec0483f7 --- /dev/null +++ b/src/storybook/story.js @@ -0,0 +1,43 @@ +/** + * This file is only used during development. + * It's set up to render the hbs templates in the DOM using javascript, and supports hot reloading. + */ +import 'modernizr'; +import qs from 'qs'; + +import 'app/style/main.scss'; +import { initComponents } from 'app/muban/componentUtils'; + +import { getStories } from './utils/utils'; +import { getStoryInfo } from './utils/getStoryInfo'; + +import storyListTemplate from './story-list'; +import storySingleTemplate from './story-single'; + +function render() { + const div = document.getElementById('app'); + + const params = qs.parse(document.location.search, { ignoreQueryPrefix: true }); + + let content = 'Please select a component!'; + const storiesInfo = getStories(params.storyName, params.variant); + console.log(storiesInfo); + + const stories = storiesInfo.map(story => getStoryInfo(story)); + + if (params.variant) { + content = storySingleTemplate({ component: stories[0].rendered }); + } else { + content = storyListTemplate({ + stories, + }); + } + + div.innerHTML = content; + + initComponents(div); +} + +document.addEventListener('DOMContentLoaded', () => { + render(); +}); diff --git a/src/storybook/storybook.html b/src/storybook/storybook.html new file mode 100644 index 00000000..e3c4768a --- /dev/null +++ b/src/storybook/storybook.html @@ -0,0 +1,16 @@ + + + + + + + +
+ + + diff --git a/src/storybook/storybook.js b/src/storybook/storybook.js new file mode 100644 index 00000000..a5a4f253 --- /dev/null +++ b/src/storybook/storybook.js @@ -0,0 +1,61 @@ +/** + * This file is only used during development. + * It's set up to render the hbs templates in the DOM using javascript, and supports hot reloading. + */ +import 'modernizr'; +import qs from 'qs'; + +import 'app/style/main.scss'; +import { initComponents } from 'app/muban/componentUtils'; +import { getAllStories, getStory } from './utils/utils'; +import { getStoryInfo } from './utils/getStoryInfo'; + +import storybookTemplate from './component/storybook/storybook'; + +function render() { + const div = document.getElementById('app'); + const params = qs.parse(document.location.search, { ignoreQueryPrefix: true }); + + let data = { + story: params.storyName, + variant: params.variant, + }; + + const stories = getAllStories(); + + data.storyList = Object.keys(stories).map(story => ({ + label: story, + path: stories[story][0].path, + variants: stories[story].map((variant, index) => ({ + ...variant, + story, + variant: index, + })), + })); + + const story = getStory(params.storyName, params.variant); + + if (story) { + const storyData = getStoryInfo(story, params.variant); + + data = { + ...data, + ...storyData, + }; + } + + div.innerHTML = storybookTemplate(data); + + initComponents(div); +} + +document.addEventListener('DOMContentLoaded', () => { + render(); +}); + +// Hot Module Replacement API +if (module.hot) { + module.hot.accept(['./component/storybook/storybook.hbs'], () => { + render(); + }); +} diff --git a/src/storybook/utils/getStoryInfo.ts b/src/storybook/utils/getStoryInfo.ts new file mode 100644 index 00000000..8ee5ab53 --- /dev/null +++ b/src/storybook/utils/getStoryInfo.ts @@ -0,0 +1,67 @@ +import highlightJs from 'highlight.js'; +import 'highlight.js/styles/solarized-light.css'; + +// eslint-disable-next-line import/prefer-default-export +export function getStoryInfo(story) { + // const componentName = /\/([^/]+)\.hbs/gi.exec(story.path)[1]; + + const data = { + story: story.name, + label: story.label, + description: story.description, + templateSource: story.source.template.replace(/(^\s+|\s+$)/gi, ''), + styleSource: story.source.style && story.source.style.replace(/(^\s+|\s+$)/gi, ''), + scriptSource: story.source.script && story.source.script.replace(/(^\s+|\s+$)/gi, ''), + path: story.path, + variant: story.variant, + rendered: story.template.compiled(story.props), + usage: story.template.raw, + tabData: { + tabId: Math.random(), + tabs: [{ label: 'Example' }, { label: 'Data' }, { label: 'HTML' }, { label: 'Handlebars' }], + }, + data: story.props, + component: null, + }; + + // if (variant) { + // data.variant = variant; + // } + + data.component = data.rendered + .replace(/(^\s+|\s+$)/gi, '') + .replace(/^\s*/i, '') + .replace(/\s*$/i, '') + .replace(/(^\s+|\s+$)/gi, ''); + + if (data.styleSource) { + data.tabData.tabs.push({ label: 'Style' }); + data.styleSource = highlightJs + .highlight('scss', data.styleSource.replace(/\t/gi, ' ')) + .value.replace(/\n/gi, '
'); + } + if (data.scriptSource) { + data.tabData.tabs.push({ label: 'Script' }); + data.scriptSource = highlightJs + .highlight('typescript', data.scriptSource.replace(/\t/gi, ' ')) + .value.replace(/\n/gi, '
'); + } + + data.usage = highlightJs + .highlight('handlebars', data.usage.replace(/\t/gi, ' ')) + .value.replace(/\n/gi, '
'); + + data.templateSource = highlightJs + .highlight('handlebars', data.templateSource.replace(/\t/gi, ' ')) + .value.replace(/\n/gi, '
'); + + data.component = highlightJs + .highlight('html', data.component.replace(/\t/gi, ' ')) + .value.replace(/\n/gi, '
'); + + data.data = highlightJs + .highlight('json', JSON.stringify(story.props, null, ' ')) + .value.replace(/\n/gi, '
'); + + return data; +} diff --git a/src/storybook/utils/utils.ts b/src/storybook/utils/utils.ts new file mode 100644 index 00000000..07ca3b2a --- /dev/null +++ b/src/storybook/utils/utils.ts @@ -0,0 +1,47 @@ +declare var require: any; + +const stories = {}; + +export function configure(loadStories) { + loadStories(); +} + +export function storiesOf(name, component) { + stories[name] = []; + + return { + add(label = 'default', description = '', template, props = {}) { + stories[name].push({ + name, + label, + description, + template, + props, + path: component.path, + source: component, + component: component.default, + variant: stories[name].length, + }); + + return this; + }, + }; +} + +export function getAllStories() { + return stories; +} + +export function getStories(name, variant) { + if (!name) { + return Object.keys(stories).reduce((list, storyName) => list.concat(stories[storyName]), []); + } + if (!variant) { + return stories[name]; + } + return [stories[name][variant]]; +} + +export function getStory(name, variant) { + return stories[name] && stories[name][variant]; +} diff --git a/template/block/{name_sc}/preset.js b/template/block/{name_sc}/preset.js new file mode 100644 index 00000000..68413b4f --- /dev/null +++ b/template/block/{name_sc}/preset.js @@ -0,0 +1,11 @@ +/* eslint-disable max-len */ +import { storiesOf } from 'storybook/utils/utils'; + +storiesOf('{{name_sc}}', require('./{{name_sc}}.hbs')).add( + 'default', + 'No description yet...', + ` + \{{> {{name_sc}} @root}} + `, + {}, +); diff --git a/template/component/{name_sc}/preset.js b/template/component/{name_sc}/preset.js new file mode 100644 index 00000000..68413b4f --- /dev/null +++ b/template/component/{name_sc}/preset.js @@ -0,0 +1,11 @@ +/* eslint-disable max-len */ +import { storiesOf } from 'storybook/utils/utils'; + +storiesOf('{{name_sc}}', require('./{{name_sc}}.hbs')).add( + 'default', + 'No description yet...', + ` + \{{> {{name_sc}} @root}} + `, + {}, +); diff --git a/tsconfig.json b/tsconfig.json index 0d5b7a35..f9c02968 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ "DOM", "DOM.Iterable", "ES2017" - ] + ], + "baseUrl": "./src/" }, "include": [ "./src/**/*" diff --git a/yarn.lock b/yarn.lock index 4fa4eb2a..9ff18a93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3686,6 +3686,10 @@ he@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" +highlight.js@^9.12.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -4402,6 +4406,10 @@ jpegtran-bin@^3.0.0: bin-wrapper "^3.0.0" logalot "^2.0.0" +jquery@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787" + js-base64@^2.1.8, js-base64@^2.1.9: version "2.3.2" resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf" @@ -6365,7 +6373,7 @@ q@^1.1.2: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" -qs@6.5.1, qs@~6.5.1: +qs@6.5.1, qs@^6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" @@ -6430,6 +6438,10 @@ raw-body@2.3.2: iconv-lite "0.4.19" unpipe "1.0.0" +raw-loader@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa" + rc@^1.1.2, rc@^1.1.7: version "1.2.2" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077"